diff --git a/README.md b/README.md index f116c157d..356640492 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ MetacatUI is an open source, community project. We [welcome contributions](https Cite this software as: -> Matthew B. Jones, Chris Jones, Lauren Walker, Robyn Thiessen-Bock, Ben Leinfelder, Peter Slaughter, Bryce Mecum, Rushiraj Nenuji, Hesham Elbashandy, Val Hendrix, Ian Nesbitt, Yvonne Shi, Ian Guerin, Doug Hungarter. 2024. MetacatUI: A client-side web interface for DataONE data repositories (version 2.29.0). Arctic Data Center. [doi:10.18739/A2KD1QN1N](https://doi.org/10.18739/A2KD1QN1N) +> Matthew B. Jones, Chris Jones, Lauren Walker, Robyn Thiessen-Bock, Ben Leinfelder, Peter Slaughter, Bryce Mecum, Rushiraj Nenuji, Hesham Elbashandy, Val Hendrix, Ian Nesbitt, Yvonne Shi, Ian Guerin, Doug Hungarter. 2024. MetacatUI: A client-side web interface for DataONE data repositories (version 2.29.1). Arctic Data Center. [doi:10.18739/A2KD1QN1N](https://doi.org/10.18739/A2KD1QN1N) ## Screenshots diff --git a/docs/_config.yml b/docs/_config.yml index a41fe7d73..7a63be208 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,3 +1,3 @@ url: "/metacatui" highlighter: "rouge" -version: "2.29.0" +version: "2.29.1" diff --git a/docs/docs/AccessPolicy.html b/docs/docs/AccessPolicy.html index 7c4f403e0..479a37cde 100644 --- a/docs/docs/AccessPolicy.html +++ b/docs/docs/AccessPolicy.html @@ -121,7 +121,7 @@

Source:
@@ -242,7 +242,7 @@

Type:
Source:
@@ -575,7 +575,7 @@

Source:
@@ -670,7 +670,7 @@

Source:
@@ -833,7 +833,7 @@

Parameters:
Source:
@@ -955,7 +955,7 @@

Source:
@@ -1068,7 +1068,7 @@

Source:
@@ -1181,7 +1181,7 @@

Source:
@@ -1276,7 +1276,7 @@

Source:
@@ -1564,7 +1564,7 @@

Parameters:
Source:
@@ -1659,7 +1659,7 @@

Source:
diff --git a/docs/docs/AccessPolicyView.html b/docs/docs/AccessPolicyView.html index f291a6a8a..fbc02ce93 100644 --- a/docs/docs/AccessPolicyView.html +++ b/docs/docs/AccessPolicyView.html @@ -130,7 +130,7 @@

Source:
@@ -259,7 +259,7 @@

Type:
Source:
@@ -342,7 +342,7 @@
Type:
Source:
@@ -420,7 +420,7 @@
Type:
Source:
@@ -498,7 +498,7 @@
Type:
Source:
@@ -576,7 +576,7 @@
Type:
Source:
@@ -657,7 +657,7 @@
Type:
Source:
@@ -735,7 +735,7 @@
Type:
Source:
@@ -818,7 +818,7 @@
Type:
Source:
@@ -896,7 +896,7 @@
Type:
Source:
@@ -1031,7 +1031,7 @@
Parameters:
Source:
@@ -1125,7 +1125,7 @@

Source:
@@ -1268,7 +1268,7 @@

Parameters:
Source:
@@ -1367,7 +1367,7 @@

Source:
@@ -1528,7 +1528,7 @@

Parameters:
Source:
@@ -1671,7 +1671,7 @@
Parameters:
Source:
@@ -1765,7 +1765,7 @@

Source:
@@ -1859,7 +1859,7 @@

Source:
@@ -1953,7 +1953,7 @@

Source:
@@ -2052,7 +2052,7 @@

Source:
@@ -2146,7 +2146,7 @@

Source:
@@ -2240,7 +2240,7 @@

Source:
@@ -2335,7 +2335,7 @@

Source:
@@ -2478,7 +2478,7 @@

Parameters:
Source:
@@ -2573,7 +2573,7 @@

Source:
@@ -2667,7 +2667,7 @@

Source:
diff --git a/docs/docs/AccessRule.html b/docs/docs/AccessRule.html index 65f49bbc2..867d4ef50 100644 --- a/docs/docs/AccessRule.html +++ b/docs/docs/AccessRule.html @@ -120,7 +120,7 @@

Source:
@@ -245,7 +245,7 @@

Source:
@@ -339,7 +339,7 @@

Source:
@@ -500,7 +500,7 @@

Parameters:
Source:
@@ -617,7 +617,7 @@

Source:
diff --git a/docs/docs/AccessRuleView.html b/docs/docs/AccessRuleView.html index 2a6655167..b388ba4b5 100644 --- a/docs/docs/AccessRuleView.html +++ b/docs/docs/AccessRuleView.html @@ -252,7 +252,7 @@

Type:
Source:
@@ -330,7 +330,7 @@
Type:
Source:
@@ -408,7 +408,7 @@
Type:
Source:
@@ -486,7 +486,7 @@
Type:
Source:
@@ -564,7 +564,7 @@
Type:
Source:
@@ -642,7 +642,7 @@
Type:
Source:
@@ -720,7 +720,7 @@
Type:
Source:
@@ -855,7 +855,7 @@
Parameters:
Source:
@@ -998,7 +998,7 @@
Parameters:
Source:
@@ -1092,7 +1092,7 @@

Source:
@@ -1186,7 +1186,7 @@

Source:
@@ -1280,7 +1280,7 @@

Source:
@@ -1374,7 +1374,7 @@

Source:
@@ -1468,7 +1468,7 @@

Source:
@@ -1562,7 +1562,7 @@

Source:
@@ -1656,7 +1656,7 @@

Source:
diff --git a/docs/docs/AccountSelectView.html b/docs/docs/AccountSelectView.html index 914c7903f..adedc6366 100644 --- a/docs/docs/AccountSelectView.html +++ b/docs/docs/AccountSelectView.html @@ -134,7 +134,7 @@

Source:
diff --git a/docs/docs/Analytics.html b/docs/docs/Analytics.html index c24c36144..7f57b88c7 100644 --- a/docs/docs/Analytics.html +++ b/docs/docs/Analytics.html @@ -125,7 +125,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -324,7 +324,7 @@
Type:
Source:
@@ -483,7 +483,7 @@
Parameters:
Source:
@@ -599,7 +599,7 @@

Source:
@@ -712,7 +712,7 @@

Source:
@@ -828,7 +828,7 @@

Source:
@@ -944,7 +944,7 @@

Source:
@@ -1038,7 +1038,7 @@

Source:
@@ -1155,7 +1155,7 @@

Source:
@@ -1368,7 +1368,7 @@

Parameters:
Source:
@@ -1560,7 +1560,7 @@
Parameters:
Source:
@@ -1727,7 +1727,7 @@
Parameters:
Source:
diff --git a/docs/docs/AnnotationFilter.html b/docs/docs/AnnotationFilter.html index 3658d08e8..c4a9b4e82 100644 --- a/docs/docs/AnnotationFilter.html +++ b/docs/docs/AnnotationFilter.html @@ -134,7 +134,7 @@

Source:
diff --git a/docs/docs/AnnotationView.html b/docs/docs/AnnotationView.html index eed554323..2ca6597b3 100644 --- a/docs/docs/AnnotationView.html +++ b/docs/docs/AnnotationView.html @@ -380,7 +380,7 @@

Parameters:
Source:
@@ -523,7 +523,7 @@
Parameters:
Source:
@@ -666,7 +666,7 @@
Parameters:
Source:
@@ -978,7 +978,7 @@
Parameters:
Source:
@@ -1144,7 +1144,7 @@
Parameters:
Source:
@@ -1294,7 +1294,7 @@
Parameters:
Source:
diff --git a/docs/docs/AppConfig.html b/docs/docs/AppConfig.html index 5c1f0db92..ba4ec5d54 100644 --- a/docs/docs/AppConfig.html +++ b/docs/docs/AppConfig.html @@ -95,7 +95,7 @@

AppConfig

Source:
@@ -196,7 +196,7 @@
Type:
Source:
@@ -371,7 +371,7 @@
Properties:
Source:
@@ -564,7 +564,7 @@
Properties:
Source:
@@ -655,7 +655,7 @@
Type:
Source:
@@ -735,7 +735,7 @@
Type:
Source:
@@ -818,7 +818,7 @@
Type:
Source:
@@ -911,7 +911,7 @@
Type:
Source:
@@ -997,7 +997,7 @@
Type:
Source:
@@ -1085,7 +1085,7 @@
Type:
Source:
@@ -1171,7 +1171,7 @@
Type:
Source:
@@ -1259,7 +1259,7 @@
Type:
Source:
@@ -1345,7 +1345,7 @@
Type:
Source:
@@ -1429,7 +1429,7 @@
Type:
Source:
@@ -1522,7 +1522,7 @@
Type:
Source:
@@ -1602,7 +1602,7 @@
Type:
Source:
@@ -1688,7 +1688,7 @@
Type:
Source:
@@ -1771,7 +1771,7 @@
Type:
Source:
@@ -1856,7 +1856,7 @@
Type:
Source:
@@ -1934,7 +1934,7 @@
Type:
Source:
@@ -2017,7 +2017,7 @@
Type:
Source:
@@ -2100,7 +2100,7 @@
Type:
Source:
@@ -2182,7 +2182,7 @@
Type:
Source:
@@ -2264,7 +2264,7 @@
Type:
Source:
@@ -2346,7 +2346,7 @@
Type:
Source:
@@ -2428,7 +2428,7 @@
Type:
Source:
@@ -2513,7 +2513,7 @@
Type:
Source:
@@ -2595,7 +2595,7 @@
Type:
Source:
@@ -2680,7 +2680,7 @@
Type:
Source:
@@ -2762,7 +2762,7 @@
Type:
Source:
@@ -2852,7 +2852,7 @@
Type:
Source:
@@ -2937,7 +2937,7 @@
Type:
Source:
@@ -3022,7 +3022,7 @@
Type:
Source:
@@ -3106,7 +3106,7 @@
Type:
Source:
@@ -3194,7 +3194,7 @@
Type:
Source:
@@ -3276,7 +3276,7 @@
Type:
Source:
@@ -3359,7 +3359,7 @@
Type:
Source:
@@ -3442,7 +3442,7 @@
Type:
Source:
@@ -3534,7 +3534,7 @@
Type:
Source:
@@ -3620,7 +3620,7 @@
Type:
Source:
@@ -3701,7 +3701,7 @@
Type:
Source:
@@ -3788,7 +3788,7 @@
Type:
Source:
@@ -3866,7 +3866,7 @@
Type:
Source:
@@ -3950,7 +3950,7 @@
Type:
Source:
@@ -4037,7 +4037,7 @@
Type:
Source:
@@ -4127,7 +4127,7 @@
Type:
Source:
@@ -4216,7 +4216,7 @@
Type:
Source:
@@ -4307,7 +4307,7 @@
Type:
Source:
@@ -4406,7 +4406,7 @@
Type:
Source:
@@ -4491,7 +4491,7 @@
Type:
Source:
@@ -4572,7 +4572,7 @@
Type:
Source:
@@ -4662,7 +4662,7 @@
Type:
Source:
@@ -4748,7 +4748,7 @@
Type:
Source:
@@ -4828,7 +4828,7 @@
Type:
Source:
@@ -4911,7 +4911,7 @@
Type:
Source:
@@ -4995,7 +4995,7 @@
Type:
Source:
@@ -5078,7 +5078,7 @@
Type:
Source:
@@ -5162,7 +5162,7 @@
Type:
Source:
@@ -5245,7 +5245,7 @@
Type:
Source:
@@ -5329,7 +5329,7 @@
Type:
Source:
@@ -5413,7 +5413,7 @@
Type:
Source:
@@ -5496,7 +5496,7 @@
Type:
Source:
@@ -5580,7 +5580,7 @@
Type:
Source:
@@ -5663,7 +5663,7 @@
Type:
Source:
@@ -5747,7 +5747,7 @@
Type:
Source:
@@ -5833,7 +5833,7 @@
Type:
Source:
@@ -5916,7 +5916,7 @@
Type:
Source:
@@ -6002,7 +6002,7 @@
Type:
Source:
@@ -6092,7 +6092,7 @@
Type:
Source:
@@ -6186,7 +6186,7 @@
Type:
Source:
@@ -6264,7 +6264,7 @@
Type:
Source:
@@ -6342,7 +6342,7 @@
Type:
Source:
@@ -6429,7 +6429,7 @@
Type:
Source:
@@ -6515,7 +6515,7 @@
Type:
Source:
@@ -6599,7 +6599,7 @@
Type:
Source:
@@ -7050,7 +7050,7 @@
Properties:
Source:
@@ -7162,7 +7162,7 @@
Type:
Source:
@@ -7336,7 +7336,7 @@
Properties:
Source:
@@ -7431,7 +7431,7 @@
Type:
Source:
@@ -7514,7 +7514,7 @@
Type:
Source:
@@ -7599,7 +7599,7 @@
Type:
Source:
@@ -7686,7 +7686,7 @@
Type:
Source:
@@ -7769,7 +7769,7 @@
Type:
Source:
@@ -7855,7 +7855,7 @@
Type:
Source:
@@ -7943,7 +7943,7 @@
Type:
Source:
@@ -8031,7 +8031,7 @@
Type:
Source:
@@ -8117,7 +8117,7 @@
Type:
Source:
@@ -8202,7 +8202,7 @@
Type:
Source:
@@ -8283,7 +8283,7 @@
Type:
Source:
@@ -8368,7 +8368,7 @@
Type:
Source:
@@ -8452,7 +8452,7 @@
Type:
Source:
@@ -8536,7 +8536,7 @@
Type:
Source:
@@ -8616,7 +8616,7 @@
Type:
Source:
@@ -8699,7 +8699,7 @@
Type:
Source:
@@ -8785,7 +8785,7 @@
Type:
Source:
@@ -8876,7 +8876,7 @@
Type:
Source:
@@ -8956,7 +8956,7 @@
Type:
Source:
@@ -9039,7 +9039,7 @@
Type:
Source:
@@ -9125,7 +9125,7 @@
Type:
Source:
@@ -9205,7 +9205,7 @@
Type:
Source:
@@ -9301,7 +9301,7 @@
Type:
Source:
@@ -9389,7 +9389,7 @@
Type:
Source:
@@ -9477,7 +9477,7 @@
Type:
Source:
@@ -9565,7 +9565,7 @@
Type:
Source:
@@ -9653,7 +9653,7 @@
Type:
Source:
@@ -9741,7 +9741,7 @@
Type:
Source:
@@ -9820,7 +9820,7 @@
Type:
Source:
@@ -9904,7 +9904,7 @@
Type:
Source:
@@ -9983,7 +9983,7 @@
Type:
Source:
@@ -10072,7 +10072,7 @@
Type:
Source:
@@ -10163,7 +10163,7 @@
Type:
Source:
@@ -10246,7 +10246,7 @@
Type:
Source:
@@ -10329,7 +10329,7 @@
Type:
Source:
@@ -10412,7 +10412,7 @@
Type:
Source:
@@ -10495,7 +10495,7 @@
Type:
Source:
@@ -10578,7 +10578,7 @@
Type:
Source:
@@ -10661,7 +10661,7 @@
Type:
Source:
@@ -10742,7 +10742,7 @@
Type:
Source:
@@ -10825,7 +10825,7 @@
Type:
Source:
@@ -10908,7 +10908,7 @@
Type:
Source:
@@ -10986,7 +10986,7 @@
Type:
Source:
@@ -11071,7 +11071,7 @@
Type:
Source:
@@ -11158,7 +11158,7 @@
Type:
Source:
@@ -11243,7 +11243,7 @@
Type:
Source:
@@ -11324,7 +11324,7 @@
Type:
Source:
@@ -11407,7 +11407,7 @@
Type:
Source:
@@ -11487,7 +11487,7 @@
Type:
Source:
@@ -11572,7 +11572,7 @@
Type:
Source:
@@ -11657,7 +11657,7 @@
Type:
Source:
@@ -11737,7 +11737,7 @@
Type:
Source:
@@ -12103,7 +12103,7 @@
Properties:
Source:
@@ -12203,7 +12203,7 @@
Type:
Source:
@@ -12282,7 +12282,7 @@
Type:
Source:
@@ -12361,7 +12361,7 @@
Type:
Source:
@@ -12439,7 +12439,7 @@
Type:
Source:
@@ -12685,7 +12685,7 @@
Properties:
Source:
@@ -12771,7 +12771,7 @@
Type:
Source:
@@ -12858,7 +12858,7 @@
Type:
Source:
@@ -12944,7 +12944,7 @@
Type:
Source:
@@ -13031,7 +13031,7 @@
Type:
Source:
@@ -13118,7 +13118,7 @@
Type:
Source:
@@ -13207,7 +13207,7 @@
Type:
Source:
@@ -13291,7 +13291,7 @@
Type:
Source:
@@ -13370,7 +13370,7 @@
Type:
Source:
@@ -13454,7 +13454,7 @@
Type:
Source:
@@ -13524,7 +13524,7 @@

Source:
@@ -13594,7 +13594,7 @@

Source:
@@ -13678,7 +13678,7 @@

Type:
Source:
@@ -13758,7 +13758,7 @@
Type:
Source:
@@ -13849,7 +13849,7 @@
Type:
Source:
@@ -13951,7 +13951,7 @@
Type:
Source:
@@ -14038,7 +14038,7 @@
Type:
Source:
@@ -14118,7 +14118,7 @@
Type:
Source:
@@ -14206,7 +14206,7 @@
Type:
Source:
@@ -14278,7 +14278,7 @@

Source:
@@ -14356,7 +14356,7 @@

Type:
Source:
@@ -14442,7 +14442,7 @@
Type:
Source:
@@ -14525,7 +14525,7 @@
Type:
Source:
@@ -14608,7 +14608,7 @@
Type:
Source:
@@ -14694,7 +14694,7 @@
Type:
Source:
@@ -14777,7 +14777,7 @@
Type:
Source:
@@ -14863,7 +14863,7 @@
Type:
Source:
@@ -14946,7 +14946,7 @@
Type:
Source:
@@ -15025,7 +15025,7 @@
Type:
Source:
@@ -15104,7 +15104,7 @@
Type:
Source:
@@ -15183,7 +15183,7 @@
Type:
Source:
@@ -15262,7 +15262,7 @@
Type:
Source:
@@ -15345,7 +15345,7 @@
Type:
Source:
@@ -15435,7 +15435,7 @@
Type:
Source:
@@ -15521,7 +15521,7 @@
Type:
Source:
@@ -15607,7 +15607,7 @@
Type:
Source:
@@ -15694,7 +15694,7 @@
Type:
Source:
@@ -15786,7 +15786,7 @@
Type:
Source:
@@ -15873,7 +15873,7 @@
Type:
Source:
@@ -15961,7 +15961,7 @@
Type:
Source:
@@ -16039,7 +16039,7 @@
Type:
Source:
@@ -16118,7 +16118,7 @@
Type:
Source:
@@ -16205,7 +16205,7 @@
Type:
Source:
@@ -16300,7 +16300,7 @@
Type:
Source:
@@ -16384,7 +16384,7 @@
Type:
Source:
@@ -16464,7 +16464,7 @@
Type:
Source:
@@ -16651,7 +16651,7 @@
Properties:
Source:
diff --git a/docs/docs/AppModel.html b/docs/docs/AppModel.html index a2fd22604..c611e5f68 100644 --- a/docs/docs/AppModel.html +++ b/docs/docs/AppModel.html @@ -120,7 +120,7 @@

Source:
@@ -300,7 +300,7 @@

Parameters:
Source:
@@ -471,7 +471,7 @@
Parameters:
Source:
@@ -666,7 +666,7 @@
Parameters:
Source:
@@ -760,7 +760,7 @@

Source:
@@ -933,7 +933,7 @@

Parameters:
Source:
@@ -1047,7 +1047,7 @@

Source:
@@ -1193,7 +1193,7 @@

Parameters:
Source:
@@ -1362,7 +1362,7 @@
Parameters:
Source:
@@ -1530,7 +1530,7 @@
Parameters:
Source:
@@ -1627,7 +1627,7 @@

Source:
@@ -1724,7 +1724,7 @@

Source:
@@ -1820,7 +1820,7 @@

Source:
diff --git a/docs/docs/AppView.html b/docs/docs/AppView.html index 895105ac8..fbba7a810 100644 --- a/docs/docs/AppView.html +++ b/docs/docs/AppView.html @@ -120,7 +120,7 @@

Source:
@@ -250,7 +250,7 @@

Type:
Source:
@@ -339,7 +339,7 @@

Source:
@@ -433,7 +433,7 @@

Source:
@@ -529,7 +529,7 @@

Source:
@@ -623,7 +623,7 @@

Source:
@@ -735,7 +735,7 @@

Source:
@@ -830,7 +830,7 @@

Source:
@@ -927,7 +927,7 @@

Source:
@@ -1032,7 +1032,7 @@

Source:
@@ -1462,7 +1462,7 @@

Properties:
Source:
@@ -1940,7 +1940,7 @@
Properties
Source:
@@ -2034,7 +2034,7 @@

Source:
@@ -2131,7 +2131,7 @@

Source:
diff --git a/docs/docs/AssetCategories.html b/docs/docs/AssetCategories.html index c07167246..03fb1d14c 100644 --- a/docs/docs/AssetCategories.html +++ b/docs/docs/AssetCategories.html @@ -125,7 +125,7 @@

Source:
@@ -232,7 +232,7 @@

Source:
@@ -318,7 +318,7 @@

Source:
@@ -482,7 +482,7 @@

Parameters:
Source:
diff --git a/docs/docs/AssetCategory.html b/docs/docs/AssetCategory.html index dfaf2b975..81c96d3db 100644 --- a/docs/docs/AssetCategory.html +++ b/docs/docs/AssetCategory.html @@ -780,7 +780,7 @@
Parameters:
Source:
@@ -924,7 +924,7 @@
Parameters:
Source:
diff --git a/docs/docs/AssetColor.html b/docs/docs/AssetColor.html index a30746c63..cf15ad4d9 100644 --- a/docs/docs/AssetColor.html +++ b/docs/docs/AssetColor.html @@ -125,7 +125,7 @@

Source:
@@ -373,7 +373,7 @@

Properties:
Source:
@@ -451,7 +451,7 @@
Type:
Source:
@@ -589,7 +589,7 @@
Parameters:
Source:
@@ -767,7 +767,7 @@
Parameters:
Source:
@@ -1050,7 +1050,7 @@
Properties:
Source:
diff --git a/docs/docs/AssetColors.html b/docs/docs/AssetColors.html index 96f21a7d3..45052054d 100644 --- a/docs/docs/AssetColors.html +++ b/docs/docs/AssetColors.html @@ -125,7 +125,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -337,7 +337,7 @@

Source:
@@ -484,7 +484,7 @@

Parameters:
Source:
@@ -597,7 +597,7 @@

Source:
diff --git a/docs/docs/BooleanFilter.html b/docs/docs/BooleanFilter.html index 0f1ba2dd9..ad54206a7 100644 --- a/docs/docs/BooleanFilter.html +++ b/docs/docs/BooleanFilter.html @@ -120,7 +120,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -771,7 +771,7 @@
Properties:
Source:
@@ -938,7 +938,7 @@
Parameters:
Source:
@@ -1306,7 +1306,7 @@
Parameters:
Source:
@@ -1433,7 +1433,7 @@

Source:
@@ -1554,7 +1554,7 @@

Source:
@@ -1719,7 +1719,7 @@

Parameters:
Source:
@@ -1890,7 +1890,7 @@
Parameters:
Source:
@@ -2007,7 +2007,7 @@

Source:
@@ -2132,7 +2132,7 @@

Source:
@@ -2296,7 +2296,7 @@

Parameters:
Source:
@@ -2482,7 +2482,7 @@
Parameters:
Source:
@@ -2652,7 +2652,7 @@
Parameters:
Source:
@@ -2822,7 +2822,7 @@
Parameters:
Source:
@@ -3038,7 +3038,7 @@
Parameters:
Source:
@@ -3237,7 +3237,7 @@
Parameters:
Source:
@@ -3368,7 +3368,7 @@

Source:
@@ -3528,7 +3528,7 @@

Parameters:
Source:
@@ -3710,7 +3710,7 @@
Parameters:
Source:
@@ -3833,7 +3833,7 @@

Source:
diff --git a/docs/docs/BooleanFilterView.html b/docs/docs/BooleanFilterView.html index bec9660f2..c0ab336a9 100644 --- a/docs/docs/BooleanFilterView.html +++ b/docs/docs/BooleanFilterView.html @@ -120,7 +120,7 @@

Source:
@@ -254,7 +254,7 @@

Type:
Source:
@@ -347,7 +347,7 @@
Type:
Source:
@@ -438,7 +438,7 @@
Type:
Source:
@@ -525,7 +525,7 @@
Type:
Source:
@@ -611,7 +611,7 @@
Type:
Source:
@@ -703,7 +703,7 @@
Type:
Source:
@@ -786,7 +786,7 @@
Type:
Source:
@@ -873,7 +873,7 @@
Type:
Source:
@@ -957,7 +957,7 @@
Type:
Source:
@@ -1043,7 +1043,7 @@
Type:
Source:
@@ -1132,7 +1132,7 @@
Type:
Source:
@@ -1223,7 +1223,7 @@

Source:
@@ -1389,7 +1389,7 @@

Parameters:
Source:
@@ -1533,7 +1533,7 @@
Parameters:
Source:
@@ -1706,7 +1706,7 @@
Parameters:
Source:
@@ -1866,7 +1866,7 @@
Parameters:
Source:
@@ -2016,7 +2016,7 @@
Parameters:
Source:
@@ -2212,7 +2212,7 @@
Parameters:
Source:
@@ -2361,7 +2361,7 @@
Parameters:
Source:
@@ -2527,7 +2527,7 @@
Parameters:
Source:
@@ -2621,7 +2621,7 @@

Source:
@@ -2720,7 +2720,7 @@

Source:
@@ -2872,7 +2872,7 @@

Parameters:
Source:
diff --git a/docs/docs/CatalogSearchView.html b/docs/docs/CatalogSearchView.html index c60327aa3..76b35dcd3 100644 --- a/docs/docs/CatalogSearchView.html +++ b/docs/docs/CatalogSearchView.html @@ -134,7 +134,7 @@

Source:
@@ -263,7 +263,7 @@

Type:
Source:
@@ -344,7 +344,7 @@
Type:
Source:
@@ -426,7 +426,7 @@
Type:
Source:
@@ -507,7 +507,7 @@
Type:
Source:
@@ -591,7 +591,7 @@
Type:
Source:
@@ -677,7 +677,7 @@
Type:
Source:
@@ -759,7 +759,7 @@
Type:
Source:
@@ -841,7 +841,7 @@
Type:
Source:
@@ -930,7 +930,7 @@
Type:
Source:
@@ -1018,7 +1018,7 @@
Type:
Source:
@@ -1099,7 +1099,7 @@
Type:
Source:
@@ -1181,7 +1181,7 @@
Type:
Source:
@@ -1267,7 +1267,7 @@
Type:
Source:
@@ -1348,7 +1348,7 @@
Type:
Source:
@@ -1432,7 +1432,7 @@
Type:
Source:
@@ -1513,7 +1513,7 @@
Type:
Source:
@@ -1597,7 +1597,7 @@
Type:
Source:
@@ -1678,7 +1678,7 @@
Type:
Source:
@@ -1762,7 +1762,7 @@
Type:
Source:
@@ -1843,7 +1843,7 @@
Type:
Source:
@@ -1924,7 +1924,7 @@
Type:
Source:
@@ -2005,7 +2005,7 @@
Type:
Source:
@@ -2087,7 +2087,7 @@
Type:
Source:
@@ -2174,7 +2174,7 @@
Type:
Source:
@@ -2256,7 +2256,7 @@
Type:
Source:
@@ -2343,7 +2343,7 @@
Type:
Source:
@@ -2424,7 +2424,7 @@
Type:
Source:
@@ -2513,7 +2513,7 @@

Source:
@@ -2610,7 +2610,7 @@

Source:
@@ -2708,7 +2708,7 @@

Source:
@@ -2806,7 +2806,7 @@

Source:
@@ -3055,7 +3055,7 @@

Properties
Source:
@@ -3152,7 +3152,7 @@

Source:
@@ -3249,7 +3249,7 @@

Source:
@@ -3347,7 +3347,7 @@

Source:
@@ -3444,7 +3444,7 @@

Source:
@@ -3541,7 +3541,7 @@

Source:
@@ -3638,7 +3638,7 @@

Source:
@@ -3735,7 +3735,7 @@

Source:
@@ -3832,7 +3832,7 @@

Source:
@@ -3929,7 +3929,7 @@

Source:
@@ -4027,7 +4027,7 @@

Source:
@@ -4126,7 +4126,7 @@

Source:
@@ -4223,7 +4223,7 @@

Source:
@@ -4320,7 +4320,7 @@

Source:
@@ -4512,7 +4512,7 @@

Parameters:
Source:
@@ -4678,7 +4678,7 @@
Parameters:
Source:
@@ -4827,7 +4827,7 @@
Parameters:
Source:
@@ -4976,7 +4976,7 @@
Parameters:
Source:
@@ -5074,7 +5074,7 @@

Source:
@@ -5172,7 +5172,7 @@

Source:
diff --git a/docs/docs/Cesium3DTileset.html b/docs/docs/Cesium3DTileset.html index 81931e365..5706b338a 100644 --- a/docs/docs/Cesium3DTileset.html +++ b/docs/docs/Cesium3DTileset.html @@ -125,7 +125,7 @@

Source:
@@ -465,7 +465,7 @@

Properties:
Source:
@@ -548,7 +548,7 @@
Type:
Source:
@@ -693,7 +693,7 @@
Parameters:
Source:
@@ -869,7 +869,7 @@
Parameters:
Source:
@@ -1040,7 +1040,7 @@
Parameters:
Source:
@@ -1191,7 +1191,7 @@
Parameters:
Source:
@@ -1364,7 +1364,7 @@
Parameters:
Source:
@@ -1565,7 +1565,7 @@
Parameters:
Source:
@@ -1768,7 +1768,7 @@
Parameters:
Source:
@@ -1886,7 +1886,7 @@

Source:
@@ -2055,7 +2055,7 @@

Parameters:
Source:
@@ -2181,7 +2181,7 @@

Source:
@@ -2302,7 +2302,7 @@

Source:
@@ -2475,7 +2475,7 @@

Parameters:
Source:
@@ -2597,7 +2597,7 @@

Source:
@@ -2769,7 +2769,7 @@

Parameters:
Source:
@@ -2939,7 +2939,7 @@
Parameters:
Source:
@@ -3107,7 +3107,7 @@
Parameters:
Source:
@@ -3273,7 +3273,7 @@
Parameters:
Source:
@@ -3398,7 +3398,7 @@

Source:
@@ -3559,7 +3559,7 @@

Parameters:
Source:
@@ -3659,7 +3659,7 @@

Source:
@@ -3784,7 +3784,7 @@

Source:
@@ -3887,7 +3887,7 @@

Source:
@@ -4042,7 +4042,7 @@

Parameters:
Source:
@@ -4141,7 +4141,7 @@

Source:
@@ -4243,7 +4243,7 @@

Source:
@@ -4343,7 +4343,7 @@

Source:
@@ -4438,7 +4438,7 @@

Source:
@@ -4534,7 +4534,7 @@

Source:
@@ -4683,7 +4683,7 @@

Parameters:
Source:
@@ -4841,7 +4841,7 @@
Parameters:
Source:
@@ -4963,7 +4963,7 @@

Source:
@@ -5137,7 +5137,7 @@

Parameters:
Source:
@@ -5319,7 +5319,7 @@
Properties:
Source:
diff --git a/docs/docs/CesiumGeohash.html b/docs/docs/CesiumGeohash.html index 2b7f14d5a..19512dd65 100644 --- a/docs/docs/CesiumGeohash.html +++ b/docs/docs/CesiumGeohash.html @@ -574,7 +574,7 @@
Type:
Source:
@@ -719,7 +719,7 @@
Parameters:
Source:
@@ -894,7 +894,7 @@
Parameters:
Source:
@@ -1068,7 +1068,7 @@
Parameters:
Source:
@@ -1245,7 +1245,7 @@
Parameters:
Source:
@@ -1425,7 +1425,7 @@
Parameters:
Source:
@@ -1621,7 +1621,7 @@
Parameters:
Source:
@@ -1772,7 +1772,7 @@
Parameters:
Source:
@@ -1945,7 +1945,7 @@
Parameters:
Source:
@@ -2146,7 +2146,7 @@
Parameters:
Source:
@@ -2349,7 +2349,7 @@
Parameters:
Source:
@@ -2524,7 +2524,7 @@
Parameters:
Source:
@@ -2696,7 +2696,7 @@
Parameters:
Source:
@@ -2825,7 +2825,7 @@

Source:
@@ -2999,7 +2999,7 @@

Parameters:
Source:
@@ -3124,7 +3124,7 @@

Source:
@@ -3248,7 +3248,7 @@

Source:
@@ -3423,7 +3423,7 @@

Parameters:
Source:
@@ -3595,7 +3595,7 @@
Parameters:
Source:
@@ -3767,7 +3767,7 @@
Parameters:
Source:
@@ -3937,7 +3937,7 @@
Parameters:
Source:
@@ -4108,7 +4108,7 @@
Parameters:
Source:
@@ -4282,7 +4282,7 @@
Parameters:
Source:
@@ -4455,7 +4455,7 @@
Parameters:
Source:
@@ -4579,7 +4579,7 @@

Source:
@@ -4740,7 +4740,7 @@

Parameters:
Source:
@@ -4840,7 +4840,7 @@

Source:
@@ -4965,7 +4965,7 @@

Source:
@@ -5120,7 +5120,7 @@

Parameters:
Source:
@@ -5245,7 +5245,7 @@

Source:
@@ -5348,7 +5348,7 @@

Source:
@@ -5452,7 +5452,7 @@

Source:
@@ -5614,7 +5614,7 @@

Parameters:
Source:
@@ -5714,7 +5714,7 @@

Source:
@@ -5816,7 +5816,7 @@

Source:
@@ -5916,7 +5916,7 @@

Source:
@@ -6090,7 +6090,7 @@

Parameters:
Source:
@@ -6241,7 +6241,7 @@
Parameters:
Source:
@@ -6415,7 +6415,7 @@
Parameters:
Source:
@@ -6589,7 +6589,7 @@
Parameters:
Source:
@@ -6763,7 +6763,7 @@
Parameters:
Source:
@@ -6937,7 +6937,7 @@
Parameters:
Source:
@@ -7040,7 +7040,7 @@

Source:
@@ -7140,7 +7140,7 @@

Source:
@@ -7239,7 +7239,7 @@

Source:
@@ -7388,7 +7388,7 @@

Parameters:
Source:
@@ -7546,7 +7546,7 @@
Parameters:
Source:
@@ -7670,7 +7670,7 @@

Source:
@@ -7792,7 +7792,7 @@

Source:
@@ -8063,7 +8063,7 @@

Parameters:
Source:
@@ -8215,7 +8215,7 @@
Parameters:
Source:
@@ -8379,7 +8379,7 @@
Properties:
Source:
diff --git a/docs/docs/CesiumImagery.html b/docs/docs/CesiumImagery.html index 2cd103b8d..76c58f553 100644 --- a/docs/docs/CesiumImagery.html +++ b/docs/docs/CesiumImagery.html @@ -127,7 +127,7 @@

Source:
@@ -367,7 +367,7 @@

Properties:
Source:
@@ -450,7 +450,7 @@
Type:
Source:
@@ -595,7 +595,7 @@
Parameters:
Source:
@@ -771,7 +771,7 @@
Parameters:
Source:
@@ -941,7 +941,7 @@
Parameters:
Source:
@@ -1092,7 +1092,7 @@
Parameters:
Source:
@@ -1265,7 +1265,7 @@
Parameters:
Source:
@@ -1466,7 +1466,7 @@
Parameters:
Source:
@@ -1669,7 +1669,7 @@
Parameters:
Source:
@@ -1787,7 +1787,7 @@

Source:
@@ -1914,7 +1914,7 @@

Source:
@@ -2088,7 +2088,7 @@

Parameters:
Source:
@@ -2254,7 +2254,7 @@
Parameters:
Source:
@@ -2426,7 +2426,7 @@
Parameters:
Source:
@@ -2547,7 +2547,7 @@

Source:
@@ -2649,7 +2649,7 @@

Source:
@@ -2805,7 +2805,7 @@

Parameters:
Source:
@@ -2961,7 +2961,7 @@
Parameters:
Source:
@@ -3122,7 +3122,7 @@
Parameters:
Source:
@@ -3222,7 +3222,7 @@

Source:
@@ -3347,7 +3347,7 @@

Source:
@@ -3450,7 +3450,7 @@

Source:
@@ -3605,7 +3605,7 @@

Parameters:
Source:
@@ -3704,7 +3704,7 @@

Source:
@@ -3806,7 +3806,7 @@

Source:
@@ -3906,7 +3906,7 @@

Source:
@@ -4055,7 +4055,7 @@

Parameters:
Source:
@@ -4213,7 +4213,7 @@
Parameters:
Source:
@@ -4335,7 +4335,7 @@

Source:
@@ -4509,7 +4509,7 @@

Parameters:
Source:
@@ -4750,7 +4750,7 @@
Properties:
Source:
diff --git a/docs/docs/CesiumTerrain.html b/docs/docs/CesiumTerrain.html index f9969971b..09b0a9ce8 100644 --- a/docs/docs/CesiumTerrain.html +++ b/docs/docs/CesiumTerrain.html @@ -126,7 +126,7 @@

Source:
@@ -356,7 +356,7 @@

Properties:
Source:
@@ -439,7 +439,7 @@
Type:
Source:
@@ -584,7 +584,7 @@
Parameters:
Source:
@@ -760,7 +760,7 @@
Parameters:
Source:
@@ -930,7 +930,7 @@
Parameters:
Source:
@@ -1081,7 +1081,7 @@
Parameters:
Source:
@@ -1254,7 +1254,7 @@
Parameters:
Source:
@@ -1455,7 +1455,7 @@
Parameters:
Source:
@@ -1658,7 +1658,7 @@
Parameters:
Source:
@@ -1784,7 +1784,7 @@

Source:
@@ -1905,7 +1905,7 @@

Source:
@@ -2078,7 +2078,7 @@

Parameters:
Source:
@@ -2251,7 +2251,7 @@
Parameters:
Source:
@@ -2377,7 +2377,7 @@

Source:
@@ -2571,7 +2571,7 @@

Parameters:
Source:
@@ -2671,7 +2671,7 @@

Source:
@@ -2796,7 +2796,7 @@

Source:
@@ -2899,7 +2899,7 @@

Source:
@@ -3054,7 +3054,7 @@

Parameters:
Source:
@@ -3156,7 +3156,7 @@

Source:
@@ -3258,7 +3258,7 @@

Source:
@@ -3358,7 +3358,7 @@

Source:
@@ -3507,7 +3507,7 @@

Parameters:
Source:
@@ -3665,7 +3665,7 @@
Parameters:
Source:
@@ -3787,7 +3787,7 @@

Source:
@@ -3961,7 +3961,7 @@

Parameters:
Source:
@@ -4119,7 +4119,7 @@
Properties:
Source:
diff --git a/docs/docs/CesiumVectorData.html b/docs/docs/CesiumVectorData.html index 869e695a2..1c6a93434 100644 --- a/docs/docs/CesiumVectorData.html +++ b/docs/docs/CesiumVectorData.html @@ -129,7 +129,7 @@

Source:
@@ -512,7 +512,7 @@

Properties:
Source:
@@ -595,7 +595,7 @@
Type:
Source:
@@ -740,7 +740,7 @@
Parameters:
Source:
@@ -910,7 +910,7 @@
Parameters:
Source:
@@ -1079,7 +1079,7 @@
Parameters:
Source:
@@ -1251,7 +1251,7 @@
Parameters:
Source:
@@ -1431,7 +1431,7 @@
Parameters:
Source:
@@ -1622,7 +1622,7 @@
Parameters:
Source:
@@ -1773,7 +1773,7 @@
Parameters:
Source:
@@ -1946,7 +1946,7 @@
Parameters:
Source:
@@ -2147,7 +2147,7 @@
Parameters:
Source:
@@ -2350,7 +2350,7 @@
Parameters:
Source:
@@ -2520,7 +2520,7 @@
Parameters:
Source:
@@ -2687,7 +2687,7 @@
Parameters:
Source:
@@ -2816,7 +2816,7 @@

Source:
@@ -2990,7 +2990,7 @@

Parameters:
Source:
@@ -3110,7 +3110,7 @@

Source:
@@ -3229,7 +3229,7 @@

Source:
@@ -3399,7 +3399,7 @@

Parameters:
Source:
@@ -3571,7 +3571,7 @@
Parameters:
Source:
@@ -3738,7 +3738,7 @@
Parameters:
Source:
@@ -3903,7 +3903,7 @@
Parameters:
Source:
@@ -4069,7 +4069,7 @@
Parameters:
Source:
@@ -4238,7 +4238,7 @@
Parameters:
Source:
@@ -4406,7 +4406,7 @@
Parameters:
Source:
@@ -4530,7 +4530,7 @@

Source:
@@ -4691,7 +4691,7 @@

Parameters:
Source:
@@ -4791,7 +4791,7 @@

Source:
@@ -4916,7 +4916,7 @@

Source:
@@ -5066,7 +5066,7 @@

Parameters:
Source:
@@ -5191,7 +5191,7 @@

Source:
@@ -5289,7 +5289,7 @@

Source:
@@ -5388,7 +5388,7 @@

Source:
@@ -5550,7 +5550,7 @@

Parameters:
Source:
@@ -5650,7 +5650,7 @@

Source:
@@ -5752,7 +5752,7 @@

Source:
@@ -5852,7 +5852,7 @@

Source:
@@ -6021,7 +6021,7 @@

Parameters:
Source:
@@ -6167,7 +6167,7 @@
Parameters:
Source:
@@ -6336,7 +6336,7 @@
Parameters:
Source:
@@ -6505,7 +6505,7 @@
Parameters:
Source:
@@ -6674,7 +6674,7 @@
Parameters:
Source:
@@ -6843,7 +6843,7 @@
Parameters:
Source:
@@ -6941,7 +6941,7 @@

Source:
@@ -7036,7 +7036,7 @@

Source:
@@ -7130,7 +7130,7 @@

Source:
@@ -7279,7 +7279,7 @@

Parameters:
Source:
@@ -7437,7 +7437,7 @@
Parameters:
Source:
@@ -7556,7 +7556,7 @@

Source:
@@ -7678,7 +7678,7 @@

Source:
@@ -7944,7 +7944,7 @@

Parameters:
Source:
@@ -8096,7 +8096,7 @@
Parameters:
Source:
@@ -8255,7 +8255,7 @@
Properties:
Source:
diff --git a/docs/docs/CesiumWidgetView.html b/docs/docs/CesiumWidgetView.html index 015450f0e..5d7b5e115 100644 --- a/docs/docs/CesiumWidgetView.html +++ b/docs/docs/CesiumWidgetView.html @@ -682,7 +682,7 @@

Source:
@@ -974,7 +974,7 @@

Parameters:
Source:
@@ -1122,7 +1122,7 @@
Parameters:
Source:
@@ -1266,7 +1266,7 @@
Parameters:
Source:
@@ -1364,7 +1364,7 @@

Source:
@@ -1508,7 +1508,7 @@

Parameters:
Source:
@@ -1704,7 +1704,7 @@
Parameters:
Source:
@@ -1897,7 +1897,7 @@
Parameters:
Source:
@@ -2018,7 +2018,7 @@

Source:
@@ -2207,7 +2207,7 @@

Parameters:
Source:
@@ -2405,7 +2405,7 @@
Parameters:
Source:
@@ -2581,7 +2581,7 @@
Parameters:
Source:
@@ -2773,7 +2773,7 @@
Parameters:
Source:
@@ -2867,7 +2867,7 @@

Source:
@@ -3035,7 +3035,7 @@

Parameters:
Source:
@@ -3215,7 +3215,7 @@
Parameters:
Source:
@@ -3334,7 +3334,7 @@

Source:
@@ -3609,7 +3609,7 @@

Source:
@@ -3805,7 +3805,7 @@

Parameters:
Source:
@@ -3902,7 +3902,7 @@

Source:
@@ -4049,7 +4049,7 @@

Parameters:
Source:
@@ -4195,7 +4195,7 @@
Parameters:
Source:
@@ -4292,7 +4292,7 @@

Source:
@@ -4439,7 +4439,7 @@

Parameters:
Source:
@@ -4536,7 +4536,7 @@

Source:
@@ -4633,7 +4633,7 @@

Source:
@@ -4780,7 +4780,7 @@

Parameters:
Source:
@@ -5112,7 +5112,7 @@

Source:
@@ -5208,7 +5208,7 @@

Source:
@@ -5410,7 +5410,7 @@

Parameters:
Source:
@@ -5507,7 +5507,7 @@

Source:
@@ -5601,7 +5601,7 @@

Source:
@@ -5700,7 +5700,7 @@

Source:
@@ -5847,7 +5847,7 @@

Parameters:
Source:
@@ -5947,7 +5947,7 @@

Source:
@@ -6046,7 +6046,7 @@

Source:
@@ -6166,7 +6166,7 @@

Source:
@@ -6377,7 +6377,7 @@

Parameters:
Source:
@@ -6475,7 +6475,7 @@

Source:
@@ -6573,7 +6573,7 @@

Source:
@@ -6670,7 +6670,7 @@

Source:
@@ -6841,7 +6841,7 @@

Parameters:
Source:
@@ -6937,7 +6937,7 @@

Source:
@@ -7083,7 +7083,7 @@

Parameters:
Source:
@@ -7178,7 +7178,7 @@

Source:
diff --git a/docs/docs/ChoiceFilter.html b/docs/docs/ChoiceFilter.html index 4cb1a5c81..31a9a517b 100644 --- a/docs/docs/ChoiceFilter.html +++ b/docs/docs/ChoiceFilter.html @@ -120,7 +120,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -435,7 +435,7 @@
Properties:
Source:
@@ -584,7 +584,7 @@
Parameters:
Source:
@@ -952,7 +952,7 @@
Parameters:
Source:
@@ -1079,7 +1079,7 @@

Source:
@@ -1200,7 +1200,7 @@

Source:
@@ -1365,7 +1365,7 @@

Parameters:
Source:
@@ -1536,7 +1536,7 @@
Parameters:
Source:
@@ -1653,7 +1653,7 @@

Source:
@@ -1778,7 +1778,7 @@

Source:
@@ -1942,7 +1942,7 @@

Parameters:
Source:
@@ -2128,7 +2128,7 @@
Parameters:
Source:
@@ -2298,7 +2298,7 @@
Parameters:
Source:
@@ -2468,7 +2468,7 @@
Parameters:
Source:
@@ -2684,7 +2684,7 @@
Parameters:
Source:
@@ -2883,7 +2883,7 @@
Parameters:
Source:
@@ -3014,7 +3014,7 @@

Source:
@@ -3174,7 +3174,7 @@

Parameters:
Source:
@@ -3356,7 +3356,7 @@
Parameters:
Source:
@@ -3477,7 +3477,7 @@

Source:
diff --git a/docs/docs/ChoiceFilterView.html b/docs/docs/ChoiceFilterView.html index 096fd2222..42317670d 100644 --- a/docs/docs/ChoiceFilterView.html +++ b/docs/docs/ChoiceFilterView.html @@ -120,7 +120,7 @@

Source:
@@ -242,7 +242,7 @@

Type:
Source:
@@ -333,7 +333,7 @@
Type:
Source:
@@ -426,7 +426,7 @@
Type:
Source:
@@ -517,7 +517,7 @@
Type:
Source:
@@ -604,7 +604,7 @@
Type:
Source:
@@ -690,7 +690,7 @@
Type:
Source:
@@ -782,7 +782,7 @@
Type:
Source:
@@ -865,7 +865,7 @@
Type:
Source:
@@ -952,7 +952,7 @@
Type:
Source:
@@ -1034,7 +1034,7 @@
Type:
Source:
@@ -1118,7 +1118,7 @@
Type:
Source:
@@ -1204,7 +1204,7 @@
Type:
Source:
@@ -1293,7 +1293,7 @@
Type:
Source:
@@ -1382,7 +1382,7 @@

Source:
@@ -1479,7 +1479,7 @@

Source:
@@ -1577,7 +1577,7 @@

Source:
@@ -1676,7 +1676,7 @@

Source:
@@ -1842,7 +1842,7 @@

Parameters:
Source:
@@ -1986,7 +1986,7 @@
Parameters:
Source:
@@ -2159,7 +2159,7 @@
Parameters:
Source:
@@ -2319,7 +2319,7 @@
Parameters:
Source:
@@ -2417,7 +2417,7 @@

Source:
@@ -2564,7 +2564,7 @@

Parameters:
Source:
@@ -2714,7 +2714,7 @@
Parameters:
Source:
@@ -2910,7 +2910,7 @@
Parameters:
Source:
@@ -3059,7 +3059,7 @@
Parameters:
Source:
@@ -3225,7 +3225,7 @@
Parameters:
Source:
@@ -3320,7 +3320,7 @@

Source:
@@ -3420,7 +3420,7 @@

Source:
@@ -3518,7 +3518,7 @@

Source:
@@ -3670,7 +3670,7 @@

Parameters:
Source:
diff --git a/docs/docs/CitationListView.html b/docs/docs/CitationListView.html index 58da0cdb5..e7e4aad6e 100644 --- a/docs/docs/CitationListView.html +++ b/docs/docs/CitationListView.html @@ -120,7 +120,7 @@

Source:
@@ -245,7 +245,7 @@

Type:
Source:
diff --git a/docs/docs/CitationModalView.html b/docs/docs/CitationModalView.html index a577e5997..1c23f8e8f 100644 --- a/docs/docs/CitationModalView.html +++ b/docs/docs/CitationModalView.html @@ -122,7 +122,7 @@

Source:
@@ -243,7 +243,7 @@

Type:
Source:
@@ -322,7 +322,7 @@
Type:
Source:
@@ -499,7 +499,7 @@
Properties:
Source:
@@ -654,7 +654,7 @@
Properties:
Source:
@@ -734,7 +734,7 @@
Type:
Source:
@@ -812,7 +812,7 @@
Type:
Source:
@@ -895,7 +895,7 @@
Type:
Source:
@@ -980,7 +980,7 @@
Type:
Source:
@@ -1158,7 +1158,7 @@
Properties:
Source:
@@ -1368,7 +1368,7 @@
Properties
Source:
@@ -1462,7 +1462,7 @@

Source:
@@ -1557,7 +1557,7 @@

Source:
@@ -1651,7 +1651,7 @@

Source:
@@ -1745,7 +1745,7 @@

Source:
@@ -1839,7 +1839,7 @@

Source:
@@ -1955,7 +1955,7 @@

Source:
@@ -2049,7 +2049,7 @@

Source:
diff --git a/docs/docs/CitationModel.html b/docs/docs/CitationModel.html index f5dbfa58c..fd2dac454 100644 --- a/docs/docs/CitationModel.html +++ b/docs/docs/CitationModel.html @@ -124,7 +124,7 @@

Source:
@@ -729,7 +729,7 @@

Properties:
Source:
@@ -807,7 +807,7 @@
Type:
Source:
@@ -961,7 +961,7 @@
Parameters:
Source:
@@ -1132,7 +1132,7 @@
Parameters:
Source:
@@ -1251,7 +1251,7 @@

Source:
@@ -1378,7 +1378,7 @@

Source:
@@ -1498,7 +1498,7 @@

Source:
@@ -1667,7 +1667,7 @@

Parameters:
Source:
@@ -1838,7 +1838,7 @@
Parameters:
Source:
@@ -2009,7 +2009,7 @@
Parameters:
Source:
@@ -2131,7 +2131,7 @@

Source:
@@ -2305,7 +2305,7 @@

Parameters:
Source:
@@ -2477,7 +2477,7 @@
Parameters:
Source:
@@ -2645,7 +2645,7 @@
Parameters:
Source:
@@ -2814,7 +2814,7 @@
Parameters:
Source:
@@ -2982,7 +2982,7 @@
Parameters:
Source:
@@ -3103,7 +3103,7 @@

Source:
@@ -3224,7 +3224,7 @@

Source:
@@ -3403,7 +3403,7 @@

Parameters:
Source:
@@ -3574,7 +3574,7 @@
Parameters:
Source:
@@ -3693,7 +3693,7 @@

Source:
@@ -3813,7 +3813,7 @@

Source:
@@ -3934,7 +3934,7 @@

Source:
@@ -4110,7 +4110,7 @@

Parameters:
Source:
@@ -4279,7 +4279,7 @@
Parameters:
Source:
@@ -4448,7 +4448,7 @@
Parameters:
Source:
@@ -4623,7 +4623,7 @@
Parameters:
Source:
@@ -4794,7 +4794,7 @@
Parameters:
Source:
@@ -4891,7 +4891,7 @@

Source:
@@ -5062,7 +5062,7 @@

Parameters:
Source:
@@ -5251,7 +5251,7 @@
Parameters:
Source:
@@ -5426,7 +5426,7 @@
Parameters:
Source:
@@ -5573,7 +5573,7 @@
Parameters:
Source:
@@ -5802,7 +5802,7 @@
Parameters:
Source:
@@ -5958,7 +5958,7 @@
Parameters:
Source:
@@ -6110,7 +6110,7 @@
Parameters:
Source:
diff --git a/docs/docs/Citations.html b/docs/docs/Citations.html index 4b7121d45..c867f7fce 100644 --- a/docs/docs/Citations.html +++ b/docs/docs/Citations.html @@ -122,7 +122,7 @@

Source:
diff --git a/docs/docs/CollectionModel.html b/docs/docs/CollectionModel.html index 6bb4addab..534ab51e2 100644 --- a/docs/docs/CollectionModel.html +++ b/docs/docs/CollectionModel.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -390,7 +390,7 @@
Parameters:
Source:
@@ -511,7 +511,7 @@

Source:
@@ -561,7 +561,7 @@

- Converts the number of bytes into a human readable format and + Converts the number of bytes into a human readable format and updates the `sizeStr` attribute
@@ -611,7 +611,7 @@

Source:
@@ -755,7 +755,7 @@

Parameters:
Source:
@@ -915,7 +915,7 @@
Parameters:
Source:
@@ -1219,7 +1219,7 @@
Properties:
Source:
@@ -1399,7 +1399,7 @@
Parameters:
Source:
@@ -1519,7 +1519,7 @@

Source:
@@ -1637,7 +1637,7 @@

Source:
@@ -1736,7 +1736,7 @@

Source:
@@ -1848,7 +1848,7 @@

Source:
@@ -2127,7 +2127,7 @@

Properties:
Source:
@@ -2177,7 +2177,7 @@

- This method will download this object while + This method will download this object while sending the user's auth token in the request.
@@ -2227,7 +2227,7 @@

Source:
@@ -2337,7 +2337,7 @@

Source:
@@ -2432,7 +2432,7 @@

Source:
@@ -2626,7 +2626,7 @@

Parameters:
Source:
@@ -2777,7 +2777,7 @@
Parameters:
Source:
@@ -2898,7 +2898,7 @@

Source:
@@ -3023,7 +3023,7 @@

Source:
@@ -3151,7 +3151,7 @@

Source:
@@ -3260,7 +3260,7 @@

Source:
@@ -3362,7 +3362,7 @@

Source:
@@ -3468,7 +3468,7 @@

Source:
@@ -3567,7 +3567,7 @@

Source:
@@ -3727,7 +3727,7 @@

Parameters:
Source:
@@ -3993,7 +3993,7 @@
Properties:
Source:
@@ -4092,7 +4092,7 @@

Source:
@@ -4204,7 +4204,7 @@

Source:
@@ -4352,7 +4352,7 @@

Parameters:
Source:
@@ -4476,7 +4476,7 @@

Source:
@@ -4646,7 +4646,7 @@

Parameters:
Source:
@@ -4763,7 +4763,7 @@

Source:
@@ -4885,7 +4885,7 @@

Source:
@@ -5002,7 +5002,7 @@

Source:
@@ -5126,7 +5126,7 @@

Source:
@@ -5296,7 +5296,7 @@

Parameters:
Source:
@@ -5415,7 +5415,7 @@

Source:
@@ -5643,7 +5643,7 @@

Parameters:
Source:
@@ -5792,7 +5792,7 @@
Parameters:
Source:
@@ -5957,7 +5957,7 @@
Parameters:
Source:
@@ -6168,7 +6168,7 @@
Parameters:
Source:
@@ -6344,7 +6344,7 @@
Parameters:
Source:
@@ -6438,7 +6438,7 @@

Source:
@@ -6537,7 +6537,7 @@

Source:
@@ -6636,7 +6636,7 @@

Source:
@@ -6735,7 +6735,7 @@

Source:
@@ -6852,7 +6852,7 @@

Source:
@@ -6952,7 +6952,7 @@

Source:
@@ -7105,7 +7105,7 @@

Parameters:
Source:
@@ -7204,7 +7204,7 @@

Source:
@@ -7375,7 +7375,7 @@

Parameters:
Source:
@@ -7540,7 +7540,7 @@
Parameters:
Source:
@@ -7720,7 +7720,7 @@
Parameters:
Source:
@@ -7868,7 +7868,7 @@
Parameters:
Source:
@@ -7967,7 +7967,7 @@

Source:
@@ -8142,7 +8142,7 @@

Parameters:
Source:
@@ -8241,7 +8241,7 @@

Source:
@@ -8386,7 +8386,7 @@

Parameters:
Source:
@@ -8503,7 +8503,7 @@

Source:
@@ -8693,7 +8693,7 @@

Parameters:
Source:
@@ -8861,7 +8861,7 @@
Parameters:
Source:
diff --git a/docs/docs/ColorPaletteView.html b/docs/docs/ColorPaletteView.html index d779519a9..c01333622 100644 --- a/docs/docs/ColorPaletteView.html +++ b/docs/docs/ColorPaletteView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -319,7 +319,7 @@
Type:
Source:
@@ -397,7 +397,7 @@
Type:
Source:
@@ -532,7 +532,7 @@
Parameters:
Source:
@@ -626,7 +626,7 @@

Source:
diff --git a/docs/docs/DataCatalogViewWithFilters.html b/docs/docs/DataCatalogViewWithFilters.html index cd949fb4a..04daa49c6 100644 --- a/docs/docs/DataCatalogViewWithFilters.html +++ b/docs/docs/DataCatalogViewWithFilters.html @@ -487,7 +487,7 @@

Type:
Source:
@@ -658,7 +658,7 @@
Type:
Source:
@@ -750,7 +750,7 @@
Type:
Source:
@@ -1083,7 +1083,7 @@

Source:
@@ -1182,7 +1182,7 @@

Source:
@@ -1282,7 +1282,7 @@

Source:
@@ -1381,7 +1381,7 @@

Source:
@@ -1481,7 +1481,7 @@

Source:
@@ -1626,7 +1626,7 @@

Parameters:
Source:
@@ -1916,7 +1916,7 @@
Parameters:
Source:
@@ -2015,7 +2015,7 @@

Source:
@@ -2163,7 +2163,7 @@

Parameters:
Source:
@@ -2407,7 +2407,7 @@
Parameters:
Source:
@@ -2506,7 +2506,7 @@

Source:
@@ -2605,7 +2605,7 @@

Source:
@@ -3155,7 +3155,7 @@

Parameters:
Source:
@@ -3304,7 +3304,7 @@
Parameters:
Source:
diff --git a/docs/docs/DataItemView.html b/docs/docs/DataItemView.html index c9616db0e..1c2a2c1c3 100644 --- a/docs/docs/DataItemView.html +++ b/docs/docs/DataItemView.html @@ -134,7 +134,7 @@

Source:
@@ -234,7 +234,7 @@

Source:
@@ -302,7 +302,7 @@

Source:
@@ -380,7 +380,7 @@

Type:
Source:
@@ -461,7 +461,7 @@
Type:
Source:
@@ -529,7 +529,7 @@

Source:
@@ -665,7 +665,7 @@

Parameters:
Source:
@@ -770,7 +770,7 @@

Source:
@@ -935,7 +935,7 @@

Parameters:
Source:
@@ -1078,7 +1078,7 @@
Parameters:
Source:
@@ -1240,7 +1240,7 @@
Parameters:
Source:
@@ -1335,7 +1335,7 @@

Source:
@@ -1479,7 +1479,7 @@

Parameters:
Source:
@@ -1623,7 +1623,7 @@
Parameters:
Source:
@@ -1767,7 +1767,7 @@
Parameters:
Source:
@@ -1910,7 +1910,7 @@
Parameters:
Source:
@@ -2057,7 +2057,7 @@
Parameters:
Source:
@@ -2151,7 +2151,7 @@

Source:
@@ -2245,7 +2245,7 @@

Source:
@@ -2339,7 +2339,7 @@

Source:
@@ -2433,7 +2433,7 @@

Source:
@@ -2527,7 +2527,7 @@

Source:
@@ -2621,7 +2621,7 @@

Source:
@@ -2764,7 +2764,7 @@

Parameters:
Source:
@@ -2861,7 +2861,7 @@

Source:
@@ -3015,7 +3015,7 @@

Parameters:
Source:
@@ -3109,7 +3109,7 @@

Source:
@@ -3203,7 +3203,7 @@

Source:
@@ -3297,7 +3297,7 @@

Source:
@@ -3463,7 +3463,7 @@

Parameters:
Source:
@@ -3601,7 +3601,7 @@
Parameters:
Source:
diff --git a/docs/docs/DataONEObject.html b/docs/docs/DataONEObject.html index 7f0311a6c..ac9cab440 100644 --- a/docs/docs/DataONEObject.html +++ b/docs/docs/DataONEObject.html @@ -125,7 +125,7 @@

Source:
@@ -250,7 +250,7 @@

Source:
@@ -300,7 +300,7 @@

- Converts the number of bytes into a human readable format and + Converts the number of bytes into a human readable format and updates the `sizeStr` attribute
@@ -345,7 +345,7 @@

Source:
@@ -500,7 +500,7 @@

Parameters:
Source:
@@ -799,7 +799,7 @@
Properties:
Source:
@@ -974,7 +974,7 @@
Parameters:
Source:
@@ -1092,7 +1092,7 @@

Source:
@@ -1186,7 +1186,7 @@

Source:
@@ -1254,7 +1254,7 @@

- This method will download this object while + This method will download this object while sending the user's auth token in the request.
@@ -1299,7 +1299,7 @@

Source:
@@ -1403,7 +1403,7 @@

Source:
@@ -1592,7 +1592,7 @@

Parameters:
Source:
@@ -1738,7 +1738,7 @@
Parameters:
Source:
@@ -1854,7 +1854,7 @@

Source:
@@ -1974,7 +1974,7 @@

Source:
@@ -2097,7 +2097,7 @@

Source:
@@ -2201,7 +2201,7 @@

Source:
@@ -2298,7 +2298,7 @@

Source:
@@ -2402,7 +2402,7 @@

Source:
@@ -2557,7 +2557,7 @@

Parameters:
Source:
@@ -2818,7 +2818,7 @@
Properties:
Source:
@@ -2912,7 +2912,7 @@

Source:
@@ -3073,7 +3073,7 @@

Parameters:
Source:
@@ -3192,7 +3192,7 @@

Source:
@@ -3357,7 +3357,7 @@

Parameters:
Source:
@@ -3469,7 +3469,7 @@

Source:
@@ -3586,7 +3586,7 @@

Source:
@@ -3698,7 +3698,7 @@

Source:
@@ -3817,7 +3817,7 @@

Source:
@@ -3982,7 +3982,7 @@

Parameters:
Source:
@@ -4096,7 +4096,7 @@

Source:
@@ -4319,7 +4319,7 @@

Parameters:
Source:
@@ -4414,7 +4414,7 @@

Source:
@@ -4560,7 +4560,7 @@

Parameters:
Source:
@@ -4654,7 +4654,7 @@

Source:
@@ -4748,7 +4748,7 @@

Source:
@@ -4842,7 +4842,7 @@

Source:
@@ -4954,7 +4954,7 @@

Source:
@@ -5049,7 +5049,7 @@

Source:
@@ -5197,7 +5197,7 @@

Parameters:
Source:
@@ -5291,7 +5291,7 @@

Source:
@@ -5457,7 +5457,7 @@

Parameters:
Source:
@@ -5632,7 +5632,7 @@
Parameters:
Source:
@@ -5775,7 +5775,7 @@
Parameters:
Source:
@@ -5869,7 +5869,7 @@

Source:
@@ -5963,7 +5963,7 @@

Source:
@@ -6057,7 +6057,7 @@

Source:
diff --git a/docs/docs/DataPackage.html b/docs/docs/DataPackage.html index 9b083c891..4f426be0a 100644 --- a/docs/docs/DataPackage.html +++ b/docs/docs/DataPackage.html @@ -122,7 +122,7 @@

Source:
@@ -248,7 +248,7 @@

Type:
Source:
@@ -326,7 +326,7 @@
Type:
Source:
@@ -405,7 +405,7 @@
Type:
Source:
@@ -484,7 +484,7 @@
Type:
Source:
@@ -562,7 +562,7 @@
Type:
Source:
@@ -640,7 +640,7 @@
Type:
Source:
@@ -718,7 +718,7 @@
Type:
Source:
@@ -787,7 +787,7 @@

Source:
@@ -868,7 +868,7 @@

Type:
Source:
@@ -948,7 +948,7 @@
Type:
Source:
@@ -1026,7 +1026,7 @@
Type:
Source:
@@ -1095,7 +1095,7 @@

Source:
@@ -1175,7 +1175,7 @@

Type:
Source:
@@ -1262,7 +1262,7 @@
Type:
Source:
@@ -1340,7 +1340,7 @@
Type:
Source:
@@ -1482,7 +1482,7 @@
Parameters:
Source:
@@ -1715,7 +1715,7 @@
Properties:
Source:
@@ -1810,7 +1810,7 @@

Source:
@@ -1956,7 +1956,7 @@

Parameters:
Source:
@@ -2077,7 +2077,7 @@

Source:
@@ -2182,7 +2182,7 @@

Source:
@@ -2349,7 +2349,7 @@

Parameters:
Source:
@@ -2559,7 +2559,7 @@
Parameters:
Source:
@@ -2653,7 +2653,7 @@

Source:
@@ -2757,7 +2757,7 @@

Source:
@@ -3006,7 +3006,7 @@

Properties
Source:
@@ -3166,7 +3166,7 @@
Parameters:
Source:
@@ -3260,7 +3260,7 @@

Source:
diff --git a/docs/docs/DataPackageView.html b/docs/docs/DataPackageView.html index d53c61642..aa3682693 100644 --- a/docs/docs/DataPackageView.html +++ b/docs/docs/DataPackageView.html @@ -131,7 +131,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -341,7 +341,7 @@

Source:
@@ -435,7 +435,7 @@

Source:
@@ -581,7 +581,7 @@

Parameters:
Source:
@@ -675,7 +675,7 @@

Source:
@@ -772,7 +772,7 @@

Source:
@@ -918,7 +918,7 @@

Parameters:
Source:
@@ -1064,7 +1064,7 @@
Parameters:
Source:
@@ -1210,7 +1210,7 @@
Parameters:
Source:
@@ -1356,7 +1356,7 @@
Parameters:
Source:
@@ -1453,7 +1453,7 @@

Source:
@@ -1547,7 +1547,7 @@

Source:
@@ -1685,7 +1685,7 @@

Parameters:
Source:
@@ -1779,7 +1779,7 @@

Source:
@@ -1873,7 +1873,7 @@

Source:
@@ -1970,7 +1970,7 @@

Source:
@@ -2064,7 +2064,7 @@

Source:
diff --git a/docs/docs/DateFilter.html b/docs/docs/DateFilter.html index a61edc19e..b518b9d02 100644 --- a/docs/docs/DateFilter.html +++ b/docs/docs/DateFilter.html @@ -120,7 +120,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -504,7 +504,7 @@
Properties:
Source:
@@ -653,7 +653,7 @@
Parameters:
Source:
@@ -774,7 +774,7 @@

Source:
@@ -890,7 +890,7 @@

Source:
@@ -1006,7 +1006,7 @@

Source:
@@ -1185,7 +1185,7 @@

Parameters:
Source:
@@ -1312,7 +1312,7 @@

Source:
@@ -1433,7 +1433,7 @@

Source:
@@ -1598,7 +1598,7 @@

Parameters:
Source:
@@ -1769,7 +1769,7 @@
Parameters:
Source:
@@ -1886,7 +1886,7 @@

Source:
@@ -2011,7 +2011,7 @@

Source:
@@ -2175,7 +2175,7 @@

Parameters:
Source:
@@ -2361,7 +2361,7 @@
Parameters:
Source:
@@ -2531,7 +2531,7 @@
Parameters:
Source:
@@ -2701,7 +2701,7 @@
Parameters:
Source:
@@ -2917,7 +2917,7 @@
Parameters:
Source:
@@ -3116,7 +3116,7 @@
Parameters:
Source:
@@ -3247,7 +3247,7 @@

Source:
@@ -3407,7 +3407,7 @@

Parameters:
Source:
@@ -3589,7 +3589,7 @@
Parameters:
Source:
@@ -3710,7 +3710,7 @@

Source:
diff --git a/docs/docs/DateFilterView.html b/docs/docs/DateFilterView.html index 377bd4adb..c8add212b 100644 --- a/docs/docs/DateFilterView.html +++ b/docs/docs/DateFilterView.html @@ -120,7 +120,7 @@

Source:
@@ -254,7 +254,7 @@

Type:
Source:
@@ -347,7 +347,7 @@
Type:
Source:
@@ -438,7 +438,7 @@
Type:
Source:
@@ -525,7 +525,7 @@
Type:
Source:
@@ -611,7 +611,7 @@
Type:
Source:
@@ -703,7 +703,7 @@
Type:
Source:
@@ -786,7 +786,7 @@
Type:
Source:
@@ -873,7 +873,7 @@
Type:
Source:
@@ -957,7 +957,7 @@
Type:
Source:
@@ -1043,7 +1043,7 @@
Type:
Source:
@@ -1132,7 +1132,7 @@
Type:
Source:
@@ -1223,7 +1223,7 @@

Source:
@@ -1490,7 +1490,7 @@

Parameters:
Source:
@@ -1663,7 +1663,7 @@
Parameters:
Source:
@@ -1823,7 +1823,7 @@
Parameters:
Source:
@@ -1973,7 +1973,7 @@
Parameters:
Source:
@@ -2067,7 +2067,7 @@

Source:
@@ -2263,7 +2263,7 @@

Parameters:
Source:
@@ -2412,7 +2412,7 @@
Parameters:
Source:
@@ -2578,7 +2578,7 @@
Parameters:
Source:
@@ -2904,7 +2904,7 @@
Parameters:
Source:
@@ -3048,7 +3048,7 @@
Parameters:
Source:
diff --git a/docs/docs/DraftsView.html b/docs/docs/DraftsView.html index 286cccb37..8e9b5b03d 100644 --- a/docs/docs/DraftsView.html +++ b/docs/docs/DraftsView.html @@ -120,7 +120,7 @@

Source:
@@ -247,7 +247,7 @@

Source:
@@ -391,7 +391,7 @@

Parameters:
Source:
@@ -486,7 +486,7 @@

Source:
diff --git a/docs/docs/DrawTool.html b/docs/docs/DrawTool.html index ead91e8bb..41e2b9997 100644 --- a/docs/docs/DrawTool.html +++ b/docs/docs/DrawTool.html @@ -1278,7 +1278,7 @@

Source:
@@ -1703,7 +1703,7 @@

Parameters:
Source:
@@ -2047,7 +2047,7 @@

Source:
@@ -2142,7 +2142,7 @@

Source:
@@ -2446,7 +2446,7 @@

Source:
@@ -2635,7 +2635,7 @@

Source:
@@ -2780,7 +2780,7 @@

Parameters:
Source:
@@ -2875,7 +2875,7 @@

Source:
@@ -3023,7 +3023,7 @@

Parameters:
Source:
@@ -3653,7 +3653,7 @@
Parameters:
Source:
diff --git a/docs/docs/EML211.html b/docs/docs/EML211.html index e545ab32c..b9edf160b 100644 --- a/docs/docs/EML211.html +++ b/docs/docs/EML211.html @@ -121,7 +121,7 @@

Source:
@@ -392,7 +392,7 @@

Properties:
Source:
@@ -491,7 +491,7 @@

Source:
@@ -541,7 +541,7 @@

- Converts the number of bytes into a human readable format and + Converts the number of bytes into a human readable format and updates the `sizeStr` attribute
@@ -591,7 +591,7 @@

Source:
@@ -751,7 +751,7 @@

Parameters:
Source:
@@ -1055,7 +1055,7 @@
Properties:
Source:
@@ -1235,7 +1235,7 @@
Parameters:
Source:
@@ -1358,7 +1358,7 @@

Source:
@@ -1457,7 +1457,7 @@

Source:
@@ -1525,7 +1525,7 @@

- This method will download this object while + This method will download this object while sending the user's auth token in the request.
@@ -1575,7 +1575,7 @@

Source:
@@ -1816,7 +1816,7 @@

Properties:
Source:
@@ -2010,7 +2010,7 @@
Parameters:
Source:
@@ -2161,7 +2161,7 @@
Parameters:
Source:
@@ -2282,7 +2282,7 @@

Source:
@@ -2407,7 +2407,7 @@

Source:
@@ -2529,7 +2529,7 @@

Source:
@@ -2652,7 +2652,7 @@

Source:
@@ -2761,7 +2761,7 @@

Source:
@@ -2863,7 +2863,7 @@

Source:
@@ -3019,7 +3019,7 @@

Parameters:
Source:
@@ -3118,7 +3118,7 @@

Source:
@@ -3278,7 +3278,7 @@

Parameters:
Source:
@@ -3544,7 +3544,7 @@
Properties:
Source:
@@ -3643,7 +3643,7 @@

Source:
@@ -3809,7 +3809,7 @@

Parameters:
Source:
@@ -3933,7 +3933,7 @@

Source:
@@ -4103,7 +4103,7 @@

Parameters:
Source:
@@ -4220,7 +4220,7 @@

Source:
@@ -4342,7 +4342,7 @@

Source:
@@ -4459,7 +4459,7 @@

Source:
@@ -4583,7 +4583,7 @@

Source:
@@ -4753,7 +4753,7 @@

Parameters:
Source:
@@ -4914,7 +4914,7 @@
Parameters:
Source:
@@ -5057,7 +5057,7 @@
Parameters:
Source:
@@ -5158,7 +5158,7 @@

Source:
@@ -5386,7 +5386,7 @@

Parameters:
Source:
@@ -5486,7 +5486,7 @@

Source:
@@ -5637,7 +5637,7 @@

Parameters:
Source:
@@ -5736,7 +5736,7 @@

Source:
@@ -5835,7 +5835,7 @@

Source:
@@ -5934,7 +5934,7 @@

Source:
@@ -6051,7 +6051,7 @@

Source:
@@ -6151,7 +6151,7 @@

Source:
@@ -6304,7 +6304,7 @@

Parameters:
Source:
@@ -6448,7 +6448,7 @@
Parameters:
Source:
@@ -6569,7 +6569,7 @@

Source:
@@ -6740,7 +6740,7 @@

Parameters:
Source:
@@ -6920,7 +6920,7 @@
Parameters:
Source:
@@ -7068,7 +7068,7 @@
Parameters:
Source:
@@ -7167,7 +7167,7 @@

Source:
@@ -7266,7 +7266,7 @@

Source:
@@ -7365,7 +7365,7 @@

Source:
diff --git a/docs/docs/EML211EditorView.html b/docs/docs/EML211EditorView.html index 7bd7eb7ae..7e22bb41a 100644 --- a/docs/docs/EML211EditorView.html +++ b/docs/docs/EML211EditorView.html @@ -120,7 +120,7 @@

Source:
@@ -248,7 +248,7 @@

Type:
Source:
@@ -331,7 +331,7 @@
Type:
Source:
@@ -417,7 +417,7 @@
Type:
Source:
@@ -495,7 +495,7 @@
Type:
Source:
@@ -568,7 +568,7 @@

Source:
@@ -654,7 +654,7 @@

Type:
Source:
@@ -738,7 +738,7 @@
Type:
Source:
@@ -816,7 +816,7 @@
Type:
Source:
@@ -899,7 +899,7 @@
Type:
Source:
@@ -977,7 +977,7 @@
Type:
Source:
@@ -1069,7 +1069,7 @@

Source:
@@ -1190,7 +1190,7 @@

Source:
@@ -1289,7 +1289,7 @@

Source:
@@ -1384,7 +1384,7 @@

Source:
@@ -1479,7 +1479,7 @@

Source:
@@ -1573,7 +1573,7 @@

Source:
@@ -1769,7 +1769,7 @@

Parameters:
Source:
@@ -1932,7 +1932,7 @@
Parameters:
Source:
@@ -2034,7 +2034,7 @@

Source:
@@ -2128,7 +2128,7 @@

Source:
@@ -2227,7 +2227,7 @@

Source:
@@ -2394,7 +2394,7 @@

Parameters:
Source:
@@ -2552,7 +2552,7 @@
Parameters:
Source:
@@ -2669,7 +2669,7 @@

Source:
@@ -2769,7 +2769,7 @@

Source:
@@ -2913,7 +2913,7 @@

Parameters:
Source:
@@ -3057,7 +3057,7 @@
Parameters:
Source:
@@ -3156,7 +3156,7 @@

Source:
@@ -3256,7 +3256,7 @@

Source:
@@ -3373,7 +3373,7 @@

Source:
@@ -3524,7 +3524,7 @@

Parameters:
Source:
@@ -3623,7 +3623,7 @@

Source:
@@ -3717,7 +3717,7 @@

Source:
@@ -3820,7 +3820,7 @@

Source:
@@ -3932,7 +3932,7 @@

Source:
@@ -4031,7 +4031,7 @@

Source:
@@ -4130,7 +4130,7 @@

Source:
@@ -4229,7 +4229,7 @@

Source:
@@ -4378,7 +4378,7 @@

Parameters:
Source:
@@ -4477,7 +4477,7 @@

Source:
@@ -4571,7 +4571,7 @@

Source:
@@ -4665,7 +4665,7 @@

Source:
@@ -4760,7 +4760,7 @@

Source:
@@ -4859,7 +4859,7 @@

Source:
@@ -4953,7 +4953,7 @@

Source:
@@ -5047,7 +5047,7 @@

Source:
@@ -5196,7 +5196,7 @@

Parameters:
Source:
@@ -5344,7 +5344,7 @@
Parameters:
Source:
@@ -5438,7 +5438,7 @@

Source:
@@ -5586,7 +5586,7 @@

Parameters:
Source:
@@ -5737,7 +5737,7 @@
Parameters:
Source:
@@ -5837,7 +5837,7 @@

Source:
@@ -5992,7 +5992,7 @@

Parameters:
Source:
@@ -6091,7 +6091,7 @@

Source:
@@ -6234,7 +6234,7 @@

Parameters:
Source:
@@ -6328,7 +6328,7 @@

Source:
@@ -6505,7 +6505,7 @@

Parameters:
Source:
@@ -6604,7 +6604,7 @@

Source:
@@ -6703,7 +6703,7 @@

Source:
@@ -6901,7 +6901,7 @@

Source:
@@ -6995,7 +6995,7 @@

Source:
@@ -7092,7 +7092,7 @@

Source:
@@ -7235,7 +7235,7 @@

Parameters:
Source:
diff --git a/docs/docs/EMLAnnotation.html b/docs/docs/EMLAnnotation.html index 62d6293ad..a2d3b214e 100644 --- a/docs/docs/EMLAnnotation.html +++ b/docs/docs/EMLAnnotation.html @@ -120,7 +120,7 @@

Source:
@@ -256,7 +256,7 @@

Source:
diff --git a/docs/docs/EMLAnnotations.html b/docs/docs/EMLAnnotations.html index 698fd5d67..207069533 100644 --- a/docs/docs/EMLAnnotations.html +++ b/docs/docs/EMLAnnotations.html @@ -123,7 +123,7 @@

Source:
@@ -247,7 +247,7 @@

Type:
Source:
@@ -385,7 +385,7 @@
Parameters:
Source:
@@ -555,7 +555,7 @@
Parameters:
Source:
diff --git a/docs/docs/EMLAttribute.html b/docs/docs/EMLAttribute.html index 2b7098535..ed68b4cd7 100644 --- a/docs/docs/EMLAttribute.html +++ b/docs/docs/EMLAttribute.html @@ -121,7 +121,7 @@

Source:
diff --git a/docs/docs/EMLAttributeView.html b/docs/docs/EMLAttributeView.html index d1cc21f46..1a3bdbe0a 100644 --- a/docs/docs/EMLAttributeView.html +++ b/docs/docs/EMLAttributeView.html @@ -131,7 +131,7 @@

Source:
@@ -252,7 +252,7 @@

Type:
Source:
@@ -331,7 +331,7 @@
Type:
Source:
@@ -409,7 +409,7 @@
Type:
Source:
@@ -544,7 +544,7 @@
Parameters:
Source:
@@ -796,7 +796,7 @@
Properties
Source:
@@ -891,7 +891,7 @@

Source:
@@ -985,7 +985,7 @@

Source:
@@ -1108,7 +1108,7 @@

Source:
@@ -1202,7 +1202,7 @@

Source:
@@ -1346,7 +1346,7 @@

Parameters:
Source:
diff --git a/docs/docs/EMLDataTable.html b/docs/docs/EMLDataTable.html index 17b57d04b..c4dbb8368 100644 --- a/docs/docs/EMLDataTable.html +++ b/docs/docs/EMLDataTable.html @@ -121,7 +121,7 @@

Source:
@@ -258,7 +258,7 @@

Source:
diff --git a/docs/docs/EMLDistribution.html b/docs/docs/EMLDistribution.html index 8ad67bc99..aa889e2cd 100644 --- a/docs/docs/EMLDistribution.html +++ b/docs/docs/EMLDistribution.html @@ -121,7 +121,7 @@

Source:
@@ -543,7 +543,7 @@

Properties:
Source:
@@ -626,7 +626,7 @@
Type:
Source:
@@ -708,7 +708,7 @@
Type:
Source:
@@ -790,7 +790,7 @@
Type:
Source:
@@ -871,7 +871,7 @@
Type:
Source:
@@ -1010,7 +1010,7 @@
Parameters:
Source:
@@ -1177,7 +1177,7 @@
Parameters:
Source:
@@ -1321,7 +1321,7 @@
Parameters:
Source:
@@ -1415,7 +1415,7 @@

Source:
diff --git a/docs/docs/EMLEntity.html b/docs/docs/EMLEntity.html index 429ad3479..13708e8c1 100644 --- a/docs/docs/EMLEntity.html +++ b/docs/docs/EMLEntity.html @@ -123,7 +123,7 @@

Source:
@@ -255,7 +255,7 @@

Source:
diff --git a/docs/docs/EMLEntityView.html b/docs/docs/EMLEntityView.html index c65e4f264..5994c76bf 100644 --- a/docs/docs/EMLEntityView.html +++ b/docs/docs/EMLEntityView.html @@ -130,7 +130,7 @@

Source:
@@ -254,7 +254,7 @@

Type:
Source:
@@ -341,7 +341,7 @@

Source:
@@ -438,7 +438,7 @@

Source:
@@ -535,7 +535,7 @@

Source:
@@ -681,7 +681,7 @@

Parameters:
Source:
@@ -778,7 +778,7 @@

Source:
@@ -924,7 +924,7 @@

Parameters:
Source:
@@ -1018,7 +1018,7 @@

Source:
@@ -1112,7 +1112,7 @@

Source:
@@ -1206,7 +1206,7 @@

Source:
@@ -1300,7 +1300,7 @@

Source:
@@ -1394,7 +1394,7 @@

Source:
@@ -1537,7 +1537,7 @@

Parameters:
Source:
@@ -1680,7 +1680,7 @@
Parameters:
Source:
@@ -1824,7 +1824,7 @@
Parameters:
Source:
@@ -1919,7 +1919,7 @@

Source:
@@ -2065,7 +2065,7 @@

Parameters:
Source:
@@ -2214,7 +2214,7 @@
Parameters:
Source:
@@ -2386,7 +2386,7 @@
Parameters:
Source:
diff --git a/docs/docs/EMLGeoCoverage.html b/docs/docs/EMLGeoCoverage.html index b843f3f84..78f33e6b6 100644 --- a/docs/docs/EMLGeoCoverage.html +++ b/docs/docs/EMLGeoCoverage.html @@ -121,7 +121,7 @@

Source:
@@ -511,7 +511,7 @@

Properties:
Source:
@@ -671,7 +671,7 @@
Parameters:
Source:
@@ -833,7 +833,7 @@
Parameters:
Source:
@@ -949,7 +949,7 @@

Source:
@@ -1090,7 +1090,7 @@

Parameters:
Source:
@@ -1208,7 +1208,7 @@

Source:
@@ -1378,7 +1378,7 @@

Parameters:
Source:
@@ -1494,7 +1494,7 @@

Source:
@@ -1658,7 +1658,7 @@

Parameters:
Source:
@@ -1823,7 +1823,7 @@
Parameters:
Source:
@@ -1953,7 +1953,7 @@

Source:
@@ -2069,7 +2069,7 @@

Source:
@@ -2167,7 +2167,7 @@

Source:
@@ -2369,7 +2369,7 @@

Parameters:
Source:
diff --git a/docs/docs/EMLMeasurementScale.html b/docs/docs/EMLMeasurementScale.html index b242fe005..3cc624206 100644 --- a/docs/docs/EMLMeasurementScale.html +++ b/docs/docs/EMLMeasurementScale.html @@ -123,7 +123,7 @@

Source:
diff --git a/docs/docs/EMLMeasurementScaleView.html b/docs/docs/EMLMeasurementScaleView.html index e680b9327..b1c98e000 100644 --- a/docs/docs/EMLMeasurementScaleView.html +++ b/docs/docs/EMLMeasurementScaleView.html @@ -120,7 +120,7 @@

Source:
diff --git a/docs/docs/EMLMeasurementTypeView.html b/docs/docs/EMLMeasurementTypeView.html index 9e05c7470..6a1a18046 100644 --- a/docs/docs/EMLMeasurementTypeView.html +++ b/docs/docs/EMLMeasurementTypeView.html @@ -116,7 +116,7 @@

Source:
@@ -228,7 +228,7 @@

Source:
@@ -297,7 +297,7 @@

Source:
@@ -366,7 +366,7 @@

Source:
@@ -434,7 +434,7 @@

Source:
@@ -502,7 +502,7 @@

Source:
@@ -572,7 +572,7 @@

Source:
@@ -712,7 +712,7 @@

Parameters:
Source:
@@ -860,7 +860,7 @@
Parameters:
Source:
@@ -1028,7 +1028,7 @@
Parameters:
Source:
@@ -1126,7 +1126,7 @@

Source:
@@ -1338,7 +1338,7 @@

Parameters:
Source:
diff --git a/docs/docs/EMLMethodStep.html b/docs/docs/EMLMethodStep.html index bb7230ea2..671879129 100644 --- a/docs/docs/EMLMethodStep.html +++ b/docs/docs/EMLMethodStep.html @@ -127,7 +127,7 @@

Source:
@@ -557,7 +557,7 @@

Source:
@@ -670,7 +670,7 @@

Source:
@@ -787,7 +787,7 @@

Source:
@@ -899,7 +899,7 @@

Source:
@@ -993,7 +993,7 @@

Source:
diff --git a/docs/docs/EMLMethods.html b/docs/docs/EMLMethods.html index 01c9b0e3f..bced12b53 100644 --- a/docs/docs/EMLMethods.html +++ b/docs/docs/EMLMethods.html @@ -123,7 +123,7 @@

Source:
@@ -312,7 +312,7 @@

Parameters:
Source:
@@ -409,7 +409,7 @@

Source:
@@ -669,7 +669,7 @@

Properties:
Source:
@@ -784,7 +784,7 @@

Source:
@@ -899,7 +899,7 @@

Source:
@@ -1011,7 +1011,7 @@

Source:
@@ -1131,7 +1131,7 @@

Source:
@@ -1248,7 +1248,7 @@

Source:
@@ -1412,7 +1412,7 @@

Parameters:
Source:
@@ -1506,7 +1506,7 @@

Source:
@@ -1601,7 +1601,7 @@

Source:
diff --git a/docs/docs/EMLMethodsView.html b/docs/docs/EMLMethodsView.html index 2494b3d80..e346f3f90 100644 --- a/docs/docs/EMLMethodsView.html +++ b/docs/docs/EMLMethodsView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -319,7 +319,7 @@
Type:
Source:
@@ -401,7 +401,7 @@
Type:
Source:
@@ -536,7 +536,7 @@
Parameters:
Source:
@@ -634,7 +634,7 @@

Source:
@@ -792,7 +792,7 @@

Parameters:
Source:
@@ -886,7 +886,7 @@

Source:
@@ -980,7 +980,7 @@

Source:
@@ -1077,7 +1077,7 @@

Source:
diff --git a/docs/docs/EMLMissingValueCodeView.html b/docs/docs/EMLMissingValueCodeView.html index b95324dd6..9889e911e 100644 --- a/docs/docs/EMLMissingValueCodeView.html +++ b/docs/docs/EMLMissingValueCodeView.html @@ -137,7 +137,7 @@

Source:
@@ -258,7 +258,7 @@

Type:
Source:
@@ -336,7 +336,7 @@
Type:
Source:
@@ -466,7 +466,7 @@
Properties:
Source:
@@ -551,7 +551,7 @@
Type:
Source:
@@ -729,7 +729,7 @@
Properties:
Source:
@@ -807,7 +807,7 @@
Type:
Source:
@@ -943,7 +943,7 @@
Parameters:
Source:
@@ -1149,7 +1149,7 @@
Properties
Source:
@@ -1244,7 +1244,7 @@

Source:
@@ -1338,7 +1338,7 @@

Source:
@@ -1433,7 +1433,7 @@

Source:
@@ -1527,7 +1527,7 @@

Source:
@@ -1694,7 +1694,7 @@

Parameters:
Source:
@@ -1810,7 +1810,7 @@

Source:
@@ -1926,7 +1926,7 @@

Source:
@@ -2021,7 +2021,7 @@

Source:
@@ -2165,7 +2165,7 @@

Parameters:
Source:
diff --git a/docs/docs/EMLMissingValueCodesView.html b/docs/docs/EMLMissingValueCodesView.html index 5d078fe16..ec259de21 100644 --- a/docs/docs/EMLMissingValueCodesView.html +++ b/docs/docs/EMLMissingValueCodesView.html @@ -138,7 +138,7 @@

Source:
@@ -259,7 +259,7 @@

Type:
Source:
@@ -461,7 +461,7 @@
Properties:
Source:
@@ -615,7 +615,7 @@
Properties:
Source:
@@ -693,7 +693,7 @@
Type:
Source:
@@ -780,7 +780,7 @@

Source:
@@ -924,7 +924,7 @@

Parameters:
Source:
@@ -1152,7 +1152,7 @@
Properties
Source:
@@ -1298,7 +1298,7 @@
Parameters:
Source:
@@ -1414,7 +1414,7 @@

Source:
@@ -1557,7 +1557,7 @@

Parameters:
Source:
@@ -1673,7 +1673,7 @@

Source:
@@ -1790,7 +1790,7 @@

Source:
@@ -1884,7 +1884,7 @@

Source:
@@ -1978,7 +1978,7 @@

Source:
diff --git a/docs/docs/EMLNonNumericDomain.html b/docs/docs/EMLNonNumericDomain.html index bcab2bf6d..123c7a3df 100644 --- a/docs/docs/EMLNonNumericDomain.html +++ b/docs/docs/EMLNonNumericDomain.html @@ -122,7 +122,7 @@

Source:
@@ -252,7 +252,7 @@

Type:
Source:
diff --git a/docs/docs/EMLNumericDomain.html b/docs/docs/EMLNumericDomain.html index 6c1945af6..0d3f7d7c2 100644 --- a/docs/docs/EMLNumericDomain.html +++ b/docs/docs/EMLNumericDomain.html @@ -122,7 +122,7 @@

Source:
@@ -242,7 +242,7 @@

Source:
@@ -328,7 +328,7 @@

Source:
diff --git a/docs/docs/EMLOtherEntity.html b/docs/docs/EMLOtherEntity.html index 980301cce..6f64eba80 100644 --- a/docs/docs/EMLOtherEntity.html +++ b/docs/docs/EMLOtherEntity.html @@ -121,7 +121,7 @@

Source:
@@ -258,7 +258,7 @@

Source:
diff --git a/docs/docs/EMLOtherEntityView.html b/docs/docs/EMLOtherEntityView.html index de9c3320a..670ef7da1 100644 --- a/docs/docs/EMLOtherEntityView.html +++ b/docs/docs/EMLOtherEntityView.html @@ -120,7 +120,7 @@

Source:
@@ -249,7 +249,7 @@

Type:
Source:
@@ -341,7 +341,7 @@

Source:
@@ -443,7 +443,7 @@

Source:
@@ -545,7 +545,7 @@

Source:
@@ -696,7 +696,7 @@

Parameters:
Source:
@@ -798,7 +798,7 @@

Source:
@@ -949,7 +949,7 @@

Parameters:
Source:
@@ -1048,7 +1048,7 @@

Source:
@@ -1147,7 +1147,7 @@

Source:
@@ -1246,7 +1246,7 @@

Source:
@@ -1345,7 +1345,7 @@

Source:
@@ -1444,7 +1444,7 @@

Source:
@@ -1592,7 +1592,7 @@

Parameters:
Source:
@@ -1740,7 +1740,7 @@
Parameters:
Source:
@@ -1889,7 +1889,7 @@
Parameters:
Source:
@@ -1989,7 +1989,7 @@

Source:
@@ -2140,7 +2140,7 @@

Parameters:
Source:
@@ -2294,7 +2294,7 @@
Parameters:
Source:
@@ -2471,7 +2471,7 @@
Parameters:
Source:
diff --git a/docs/docs/EMLParty.html b/docs/docs/EMLParty.html index 42a48cc26..67f5c928f 100644 --- a/docs/docs/EMLParty.html +++ b/docs/docs/EMLParty.html @@ -121,7 +121,7 @@

Source:
@@ -396,7 +396,7 @@

Properties:
Source:
@@ -486,7 +486,7 @@

Source:
@@ -606,7 +606,7 @@

Source:
diff --git a/docs/docs/EMLPartyView.html b/docs/docs/EMLPartyView.html index 6d79da17b..6f23204dc 100644 --- a/docs/docs/EMLPartyView.html +++ b/docs/docs/EMLPartyView.html @@ -120,7 +120,7 @@

Source:
@@ -245,7 +245,7 @@

Source:
@@ -364,7 +364,7 @@

Source:
@@ -459,7 +459,7 @@

Source:
diff --git a/docs/docs/EMLSpecializedText.html b/docs/docs/EMLSpecializedText.html index aef74caf3..0e53668af 100644 --- a/docs/docs/EMLSpecializedText.html +++ b/docs/docs/EMLSpecializedText.html @@ -127,7 +127,7 @@

Source:
@@ -257,7 +257,7 @@

Source:
@@ -533,7 +533,7 @@

Parameters:
Source:
@@ -650,7 +650,7 @@

Source:
@@ -745,7 +745,7 @@

Source:
@@ -911,7 +911,7 @@

Parameters:
Source:
diff --git a/docs/docs/EMLTaxonCoverage.html b/docs/docs/EMLTaxonCoverage.html index 13f9b4973..6ce2566b8 100644 --- a/docs/docs/EMLTaxonCoverage.html +++ b/docs/docs/EMLTaxonCoverage.html @@ -122,7 +122,7 @@

Source:
@@ -390,7 +390,7 @@

Properties:
Source:
@@ -875,7 +875,7 @@

Source:
diff --git a/docs/docs/EMLTempCoverageView.html b/docs/docs/EMLTempCoverageView.html index 0a5ef98dd..bbb865c32 100644 --- a/docs/docs/EMLTempCoverageView.html +++ b/docs/docs/EMLTempCoverageView.html @@ -120,7 +120,7 @@

Source:
@@ -245,7 +245,7 @@

Source:
@@ -339,7 +339,7 @@

Source:
@@ -433,7 +433,7 @@

Source:
@@ -527,7 +527,7 @@

Source:
@@ -621,7 +621,7 @@

Source:
diff --git a/docs/docs/EMLTemporalCoverage.html b/docs/docs/EMLTemporalCoverage.html index d14d77b42..ab23dd233 100644 --- a/docs/docs/EMLTemporalCoverage.html +++ b/docs/docs/EMLTemporalCoverage.html @@ -116,7 +116,7 @@

Source:
@@ -241,7 +241,7 @@

Source:
@@ -425,7 +425,7 @@

Parameters:
Source:
@@ -655,7 +655,7 @@
Parameters:
Source:
diff --git a/docs/docs/EMLText.html b/docs/docs/EMLText.html index 29d626c7b..09088edcd 100644 --- a/docs/docs/EMLText.html +++ b/docs/docs/EMLText.html @@ -120,7 +120,7 @@

Source:
@@ -250,7 +250,7 @@

Source:
@@ -520,7 +520,7 @@

Parameters:
Source:
@@ -637,7 +637,7 @@

Source:
@@ -732,7 +732,7 @@

Source:
@@ -898,7 +898,7 @@

Parameters:
Source:
diff --git a/docs/docs/EMLText211.html b/docs/docs/EMLText211.html index 987ee3cb8..b619f057e 100644 --- a/docs/docs/EMLText211.html +++ b/docs/docs/EMLText211.html @@ -120,7 +120,7 @@

Source:
@@ -245,7 +245,7 @@

Source:
@@ -509,7 +509,7 @@

Parameters:
Source:
@@ -603,7 +603,7 @@

Source:
@@ -715,7 +715,7 @@

Source:
diff --git a/docs/docs/EMLUnit.html b/docs/docs/EMLUnit.html index 1cba4e30e..8d959929e 100644 --- a/docs/docs/EMLUnit.html +++ b/docs/docs/EMLUnit.html @@ -120,7 +120,7 @@

Source:
diff --git a/docs/docs/EMLView.html b/docs/docs/EMLView.html index 9f48e452d..7620580cd 100644 --- a/docs/docs/EMLView.html +++ b/docs/docs/EMLView.html @@ -120,7 +120,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -324,7 +324,7 @@
Type:
Source:
@@ -354,7 +354,7 @@

- An array of literal objects to describe each type of EML Party. This property has been moved to + An array of literal objects to describe each type of EML Party. This property has been moved to EMLParty#partyTypes as of 2.21.0 and will soon be deprecated.
@@ -408,7 +408,7 @@

Type:
Source:
@@ -499,7 +499,7 @@

Source:
@@ -598,7 +598,7 @@

Source:
@@ -743,7 +743,7 @@

Parameters:
Source:
@@ -901,7 +901,7 @@
Parameters:
Source:
@@ -1011,7 +1011,7 @@

Source:
@@ -1130,7 +1130,7 @@

Source:
@@ -1302,7 +1302,7 @@

Parameters:
Source:
@@ -1468,7 +1468,7 @@
Parameters:
Source:
@@ -1611,7 +1611,7 @@
Parameters:
Source:
@@ -1705,7 +1705,7 @@

Source:
@@ -1809,7 +1809,7 @@

Source:
@@ -1975,7 +1975,7 @@

Parameters:
Source:
@@ -2071,7 +2071,7 @@

Source:
@@ -2217,7 +2217,7 @@

Parameters:
Source:
@@ -2339,7 +2339,7 @@

Source:
diff --git a/docs/docs/EMlGeoCoverageView_.html b/docs/docs/EMlGeoCoverageView_.html index b9f725617..d6cec8b54 100644 --- a/docs/docs/EMlGeoCoverageView_.html +++ b/docs/docs/EMlGeoCoverageView_.html @@ -121,7 +121,7 @@

Source:
diff --git a/docs/docs/EditCollectionView.html b/docs/docs/EditCollectionView.html index c310b1f42..54da9d1ef 100644 --- a/docs/docs/EditCollectionView.html +++ b/docs/docs/EditCollectionView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -319,7 +319,7 @@
Type:
Source:
@@ -397,7 +397,7 @@
Type:
Source:
@@ -475,7 +475,7 @@
Type:
Source:
@@ -553,7 +553,7 @@
Type:
Source:
@@ -631,7 +631,7 @@
Type:
Source:
@@ -709,7 +709,7 @@
Type:
Source:
@@ -789,7 +789,7 @@
Type:
Source:
@@ -870,7 +870,7 @@
Type:
Source:
@@ -952,7 +952,7 @@
Type:
Source:
@@ -1030,7 +1030,7 @@
Type:
Source:
@@ -1108,7 +1108,7 @@
Type:
Source:
@@ -1186,7 +1186,7 @@
Type:
Source:
@@ -1254,7 +1254,7 @@

Source:
@@ -1332,7 +1332,7 @@

Type:
Source:
@@ -1519,7 +1519,7 @@
Properties:
Source:
@@ -1613,7 +1613,7 @@

Source:
@@ -1707,7 +1707,7 @@

Source:
@@ -1801,7 +1801,7 @@

Source:
@@ -1895,7 +1895,7 @@

Source:
@@ -1990,7 +1990,7 @@

Source:
diff --git a/docs/docs/EditorView.html b/docs/docs/EditorView.html index eb6bddad4..5f3c014a3 100644 --- a/docs/docs/EditorView.html +++ b/docs/docs/EditorView.html @@ -243,7 +243,7 @@

Type:
Source:
@@ -321,7 +321,7 @@
Type:
Source:
@@ -402,7 +402,7 @@
Type:
Source:
@@ -470,7 +470,7 @@

Source:
@@ -551,7 +551,7 @@

Type:
Source:
@@ -629,7 +629,7 @@
Type:
Source:
@@ -707,7 +707,7 @@
Type:
Source:
@@ -794,7 +794,7 @@

Source:
@@ -910,7 +910,7 @@

Source:
@@ -1004,7 +1004,7 @@

Source:
@@ -1162,7 +1162,7 @@

Parameters:
Source:
@@ -1259,7 +1259,7 @@

Source:
@@ -1353,7 +1353,7 @@

Source:
@@ -1528,7 +1528,7 @@

Parameters:
Source:
@@ -1647,7 +1647,7 @@

Source:
@@ -1759,7 +1759,7 @@

Source:
@@ -1854,7 +1854,7 @@

Source:
@@ -1966,7 +1966,7 @@

Source:
@@ -2112,7 +2112,7 @@

Parameters:
Source:
@@ -2206,7 +2206,7 @@

Source:
@@ -2304,7 +2304,7 @@

Source:
@@ -2416,7 +2416,7 @@

Source:
@@ -2510,7 +2510,7 @@

Source:
@@ -2604,7 +2604,7 @@

Source:
@@ -2748,7 +2748,7 @@

Parameters:
Source:
@@ -2842,7 +2842,7 @@

Source:
@@ -2936,7 +2936,7 @@

Source:
@@ -3080,7 +3080,7 @@

Parameters:
Source:
@@ -3174,7 +3174,7 @@

Source:
@@ -3317,7 +3317,7 @@

Parameters:
Source:
@@ -3460,7 +3460,7 @@
Parameters:
Source:
@@ -3556,7 +3556,7 @@

Source:
@@ -3726,7 +3726,7 @@

Parameters:
Source:
@@ -3820,7 +3820,7 @@

Source:
@@ -3992,7 +3992,7 @@

Parameters:
Source:
@@ -4086,7 +4086,7 @@

Source:
@@ -4180,7 +4180,7 @@

Source:
@@ -4368,7 +4368,7 @@

Source:
diff --git a/docs/docs/ExpansionPanelView.html b/docs/docs/ExpansionPanelView.html index 24132ec7f..0e4e15e88 100644 --- a/docs/docs/ExpansionPanelView.html +++ b/docs/docs/ExpansionPanelView.html @@ -133,7 +133,7 @@

Source:
@@ -240,7 +240,7 @@

Source:
@@ -308,7 +308,7 @@

Source:
@@ -386,7 +386,7 @@

Type:
Source:
@@ -472,7 +472,7 @@

Source:
@@ -566,7 +566,7 @@

Source:
@@ -660,7 +660,7 @@

Source:
@@ -776,7 +776,7 @@

Source:
@@ -872,7 +872,7 @@

Source:
@@ -966,7 +966,7 @@

Source:
diff --git a/docs/docs/ExpansionPanelsModel.html b/docs/docs/ExpansionPanelsModel.html index 6e7abfa1f..76fe76b6a 100644 --- a/docs/docs/ExpansionPanelsModel.html +++ b/docs/docs/ExpansionPanelsModel.html @@ -404,7 +404,7 @@

Properties:
- The expansion panel view that + The expansion panel view that should remain open. @@ -551,7 +551,7 @@
Properties:
- The expansion panel view to be + The expansion panel view to be tracked. diff --git a/docs/docs/FeatureInfoView.html b/docs/docs/FeatureInfoView.html index 4f248540c..7352593ab 100644 --- a/docs/docs/FeatureInfoView.html +++ b/docs/docs/FeatureInfoView.html @@ -140,7 +140,7 @@

Source:
@@ -261,7 +261,7 @@

Type:
Source:
@@ -489,7 +489,7 @@
Properties:
Source:
@@ -572,7 +572,7 @@
Type:
Source:
@@ -650,7 +650,7 @@
Type:
Source:
@@ -728,7 +728,7 @@
Type:
Source:
@@ -806,7 +806,7 @@
Type:
Source:
@@ -884,7 +884,7 @@
Type:
Source:
@@ -1021,7 +1021,7 @@
Parameters:
Source:
@@ -1115,7 +1115,7 @@

Source:
@@ -1211,7 +1211,7 @@

Source:
@@ -1328,7 +1328,7 @@

Source:
@@ -1451,7 +1451,7 @@

Source:
@@ -1628,7 +1628,7 @@

Parameters:
Source:
@@ -1722,7 +1722,7 @@

Source:
@@ -1816,7 +1816,7 @@

Source:
@@ -1935,7 +1935,7 @@

Source:
@@ -2031,7 +2031,7 @@

Source:
@@ -2125,7 +2125,7 @@

Source:
@@ -2223,7 +2223,7 @@

Source:
@@ -2319,7 +2319,7 @@

Source:
diff --git a/docs/docs/Filter.html b/docs/docs/Filter.html index e259202b6..30b211613 100644 --- a/docs/docs/Filter.html +++ b/docs/docs/Filter.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -761,7 +761,7 @@
Properties:
Source:
@@ -923,7 +923,7 @@
Parameters:
Source:
@@ -1281,7 +1281,7 @@
Parameters:
Source:
@@ -1403,7 +1403,7 @@

Source:
@@ -1519,7 +1519,7 @@

Source:
@@ -1679,7 +1679,7 @@

Parameters:
Source:
@@ -1845,7 +1845,7 @@
Parameters:
Source:
@@ -1957,7 +1957,7 @@

Source:
@@ -2077,7 +2077,7 @@

Source:
@@ -2236,7 +2236,7 @@

Parameters:
Source:
@@ -2417,7 +2417,7 @@
Parameters:
Source:
@@ -2582,7 +2582,7 @@
Parameters:
Source:
@@ -2747,7 +2747,7 @@
Parameters:
Source:
@@ -2958,7 +2958,7 @@
Parameters:
Source:
@@ -3152,7 +3152,7 @@
Parameters:
Source:
@@ -3278,7 +3278,7 @@

Source:
@@ -3433,7 +3433,7 @@

Parameters:
Source:
@@ -3610,7 +3610,7 @@
Parameters:
Source:
@@ -3728,7 +3728,7 @@

Source:
diff --git a/docs/docs/FilterEditorView.html b/docs/docs/FilterEditorView.html index a6799fe55..afc00989e 100644 --- a/docs/docs/FilterEditorView.html +++ b/docs/docs/FilterEditorView.html @@ -133,7 +133,7 @@

Source:
@@ -254,7 +254,7 @@

Type:
Source:
@@ -652,7 +652,7 @@
Properties:
Source:
@@ -732,7 +732,7 @@
Type:
Source:
@@ -810,7 +810,7 @@
Type:
Source:
@@ -889,7 +889,7 @@
Type:
Source:
@@ -968,7 +968,7 @@
Type:
Source:
@@ -1038,7 +1038,7 @@

Source:
@@ -1127,7 +1127,7 @@

Type:
Source:
@@ -1209,7 +1209,7 @@
Type:
Source:
@@ -1288,7 +1288,7 @@
Type:
Source:
@@ -1625,7 +1625,7 @@
Properties:
Source:
@@ -1704,7 +1704,7 @@
Type:
Source:
@@ -1791,7 +1791,7 @@

Source:
@@ -1937,7 +1937,7 @@

Parameters:
Source:
@@ -2084,7 +2084,7 @@
Parameters:
Source:
@@ -2230,7 +2230,7 @@
Parameters:
Source:
@@ -2350,7 +2350,7 @@

Source:
@@ -2542,7 +2542,7 @@

Parameters:
Source:
@@ -2710,7 +2710,7 @@
Parameters:
Source:
@@ -2858,7 +2858,7 @@
Parameters:
Source:
@@ -3003,7 +3003,7 @@
Parameters:
Source:
@@ -3097,7 +3097,7 @@

Source:
@@ -3193,7 +3193,7 @@

Source:
@@ -3398,7 +3398,7 @@

Properties:
Source:
@@ -3569,7 +3569,7 @@
Parameters:
Source:
@@ -3687,7 +3687,7 @@

Source:
@@ -3783,7 +3783,7 @@

Source:
@@ -3879,7 +3879,7 @@

Source:
@@ -3974,7 +3974,7 @@

Source:
@@ -4175,7 +4175,7 @@

Parameters:
Source:
@@ -4320,7 +4320,7 @@
Parameters:
Source:
diff --git a/docs/docs/FilterGroup.html b/docs/docs/FilterGroup.html index 6acc3ddaa..c48cd56a5 100644 --- a/docs/docs/FilterGroup.html +++ b/docs/docs/FilterGroup.html @@ -122,7 +122,7 @@

Source:
@@ -243,7 +243,7 @@

Type:
Source:
@@ -625,7 +625,7 @@
Properties:
Source:
@@ -720,7 +720,7 @@

Source:
@@ -838,7 +838,7 @@

Source:
@@ -933,7 +933,7 @@

Source:
@@ -1125,7 +1125,7 @@

Parameters:
Source:
@@ -1336,7 +1336,7 @@
Parameters:
Source:
@@ -1516,7 +1516,7 @@
Parameters:
Source:
@@ -1728,7 +1728,7 @@
Parameters:
Source:
diff --git a/docs/docs/FilterGroupView.html b/docs/docs/FilterGroupView.html index ec47a0d0a..3e2e8f912 100644 --- a/docs/docs/FilterGroupView.html +++ b/docs/docs/FilterGroupView.html @@ -120,7 +120,7 @@

Source:
@@ -250,7 +250,7 @@

Type:
Source:
@@ -332,7 +332,7 @@
Type:
Source:
@@ -410,7 +410,7 @@
Type:
Source:
@@ -488,7 +488,7 @@
Type:
Source:
@@ -626,7 +626,7 @@
Parameters:
Source:
@@ -746,7 +746,7 @@

Source:
diff --git a/docs/docs/FilterGroupsView.html b/docs/docs/FilterGroupsView.html index d30bab78a..95fe8fdda 100644 --- a/docs/docs/FilterGroupsView.html +++ b/docs/docs/FilterGroupsView.html @@ -120,7 +120,7 @@

Source:
@@ -227,7 +227,7 @@

Source:
@@ -314,7 +314,7 @@

Type:
Source:
@@ -396,7 +396,7 @@
Type:
Source:
@@ -474,7 +474,7 @@
Type:
Source:
@@ -538,7 +538,7 @@

Source:
@@ -616,7 +616,7 @@

Type:
Source:
@@ -696,7 +696,7 @@
Type:
Source:
@@ -778,7 +778,7 @@
Type:
Source:
@@ -842,7 +842,7 @@

Source:
@@ -913,7 +913,7 @@

Source:
@@ -991,7 +991,7 @@

Type:
Source:
@@ -1127,7 +1127,7 @@
Parameters:
Source:
@@ -1300,7 +1300,7 @@
Parameters:
Source:
@@ -1462,7 +1462,7 @@
Parameters:
Source:
@@ -1552,7 +1552,7 @@

Source:
@@ -1647,7 +1647,7 @@

Source:
@@ -1788,7 +1788,7 @@

Parameters:
Source:
@@ -1931,7 +1931,7 @@
Parameters:
Source:
@@ -2171,7 +2171,7 @@
Properties
Source:
@@ -2261,7 +2261,7 @@

Source:
@@ -2405,7 +2405,7 @@

Parameters:
Source:
@@ -2499,7 +2499,7 @@

Source:
@@ -2594,7 +2594,7 @@

Source:
@@ -2813,7 +2813,7 @@

Properties:
Source:
@@ -3064,7 +3064,7 @@
Properties:
Source:
diff --git a/docs/docs/FilterView.html b/docs/docs/FilterView.html index 117ac770b..54007a9b9 100644 --- a/docs/docs/FilterView.html +++ b/docs/docs/FilterView.html @@ -120,7 +120,7 @@

Source:
@@ -249,7 +249,7 @@

Type:
Source:
@@ -337,7 +337,7 @@
Type:
Source:
@@ -423,7 +423,7 @@
Type:
Source:
@@ -505,7 +505,7 @@
Type:
Source:
@@ -586,7 +586,7 @@
Type:
Source:
@@ -673,7 +673,7 @@
Type:
Source:
@@ -751,7 +751,7 @@
Type:
Source:
@@ -833,7 +833,7 @@
Type:
Source:
@@ -912,7 +912,7 @@
Type:
Source:
@@ -993,7 +993,7 @@
Type:
Source:
@@ -1077,7 +1077,7 @@
Type:
Source:
@@ -1163,7 +1163,7 @@

Source:
@@ -1324,7 +1324,7 @@

Parameters:
Source:
@@ -1463,7 +1463,7 @@
Parameters:
Source:
@@ -1631,7 +1631,7 @@
Parameters:
Source:
@@ -1786,7 +1786,7 @@
Parameters:
Source:
@@ -1931,7 +1931,7 @@
Parameters:
Source:
@@ -2122,7 +2122,7 @@
Parameters:
Source:
@@ -2266,7 +2266,7 @@
Parameters:
Source:
@@ -2427,7 +2427,7 @@
Parameters:
Source:
@@ -2522,7 +2522,7 @@

Source:
@@ -2669,7 +2669,7 @@

Parameters:
Source:
diff --git a/docs/docs/Filters.html b/docs/docs/Filters.html index 2e43bf00f..9a25eef12 100644 --- a/docs/docs/Filters.html +++ b/docs/docs/Filters.html @@ -243,7 +243,7 @@
Type:
Source:
@@ -329,7 +329,7 @@
Type:
Source:
@@ -1227,7 +1227,7 @@
Parameters:
Source:
@@ -1345,7 +1345,7 @@

Source:
@@ -1614,7 +1614,7 @@

Source:
@@ -1819,7 +1819,7 @@

Parameters:
Source:
@@ -2150,7 +2150,7 @@
Properties:
Source:
@@ -2395,7 +2395,7 @@
Properties:
Source:
@@ -2602,7 +2602,7 @@
Parameters:
Source:
diff --git a/docs/docs/FiltersMapConnector.html b/docs/docs/FiltersMapConnector.html index 232f34e9f..e9cc6c4d3 100644 --- a/docs/docs/FiltersMapConnector.html +++ b/docs/docs/FiltersMapConnector.html @@ -127,7 +127,7 @@

Source:
@@ -256,7 +256,7 @@

Type:
Source:
@@ -394,7 +394,7 @@
Parameters:
Source:
@@ -491,7 +491,7 @@

Source:
@@ -757,7 +757,7 @@

Properties:
Source:
@@ -923,7 +923,7 @@
Parameters:
Source:
@@ -1089,7 +1089,7 @@
Parameters:
Source:
@@ -1329,7 +1329,7 @@
Properties
Source:
@@ -1424,7 +1424,7 @@

Source:
@@ -1519,7 +1519,7 @@

Source:
@@ -1614,7 +1614,7 @@

Source:
@@ -1709,7 +1709,7 @@

Source:
@@ -1803,7 +1803,7 @@

Source:
diff --git a/docs/docs/FiltersSearchConnector.html b/docs/docs/FiltersSearchConnector.html index 075d28275..eb1f19140 100644 --- a/docs/docs/FiltersSearchConnector.html +++ b/docs/docs/FiltersSearchConnector.html @@ -123,7 +123,7 @@

Source:
@@ -252,7 +252,7 @@

Type:
Source:
@@ -342,7 +342,7 @@

Source:
@@ -533,7 +533,7 @@

Properties:
Source:
@@ -630,7 +630,7 @@

Source:
@@ -779,7 +779,7 @@

Parameters:
Source:
diff --git a/docs/docs/FooterView.html b/docs/docs/FooterView.html index 10e5b99c9..2966555c2 100644 --- a/docs/docs/FooterView.html +++ b/docs/docs/FooterView.html @@ -120,7 +120,7 @@

Source:
@@ -248,7 +248,7 @@

Source:
@@ -345,7 +345,7 @@

Source:
diff --git a/docs/docs/GeoPoint.html b/docs/docs/GeoPoint.html index 18750c9ab..0b64ced2d 100644 --- a/docs/docs/GeoPoint.html +++ b/docs/docs/GeoPoint.html @@ -621,7 +621,7 @@

Parameters:
Source:
@@ -829,7 +829,7 @@
Returns:
- Latitude and longitude information for creating a + Latitude and longitude information for creating a GeoPoint.
@@ -917,7 +917,7 @@

Source:
@@ -1033,7 +1033,7 @@

Source:
@@ -1150,7 +1150,7 @@

Source:
@@ -1266,7 +1266,7 @@

Source:
@@ -1383,7 +1383,7 @@

Source:
@@ -1549,7 +1549,7 @@

Parameters:
Source:
diff --git a/docs/docs/GeoPointsCesiumConnector.html b/docs/docs/GeoPointsCesiumConnector.html index 9c2e0cd40..86dd869c0 100644 --- a/docs/docs/GeoPointsCesiumConnector.html +++ b/docs/docs/GeoPointsCesiumConnector.html @@ -124,7 +124,7 @@

Source:
@@ -250,7 +250,7 @@

Type:
Source:
@@ -337,7 +337,7 @@

Source:
@@ -532,7 +532,7 @@

Properties:
Source:
@@ -648,7 +648,7 @@

Source:
@@ -840,7 +840,7 @@

Parameters:
Source:
@@ -1071,7 +1071,7 @@
Parameters:
Source:
@@ -1230,7 +1230,7 @@
Parameters:
Source:
@@ -1413,7 +1413,7 @@
Parameters:
Source:
diff --git a/docs/docs/GeoPointsCesiumPointsConnector.html b/docs/docs/GeoPointsCesiumPointsConnector.html index 2d2da4e94..0d90bfa84 100644 --- a/docs/docs/GeoPointsCesiumPointsConnector.html +++ b/docs/docs/GeoPointsCesiumPointsConnector.html @@ -126,7 +126,7 @@

Source:
@@ -257,7 +257,7 @@

Type:
Source:
@@ -343,7 +343,7 @@

Source:
@@ -509,7 +509,7 @@

Parameters:
Source:
@@ -631,7 +631,7 @@

Source:
@@ -784,7 +784,7 @@

Properties:
Source:
@@ -905,7 +905,7 @@

Source:
@@ -1102,7 +1102,7 @@

Parameters:
Source:
@@ -1338,7 +1338,7 @@
Parameters:
Source:
@@ -1432,7 +1432,7 @@

Source:
@@ -1598,7 +1598,7 @@

Parameters:
Source:
@@ -1716,7 +1716,7 @@

Source:
@@ -1880,7 +1880,7 @@

Parameters:
Source:
@@ -2068,7 +2068,7 @@
Parameters:
Source:
diff --git a/docs/docs/GeoPointsCesiumPolygonConnector.html b/docs/docs/GeoPointsCesiumPolygonConnector.html index f46b60f53..945fa29fa 100644 --- a/docs/docs/GeoPointsCesiumPolygonConnector.html +++ b/docs/docs/GeoPointsCesiumPolygonConnector.html @@ -126,7 +126,7 @@

Source:
@@ -257,7 +257,7 @@

Type:
Source:
@@ -343,7 +343,7 @@

Source:
@@ -466,7 +466,7 @@

Source:
@@ -619,7 +619,7 @@

Properties:
Source:
@@ -740,7 +740,7 @@

Source:
@@ -840,7 +840,7 @@

Source:
@@ -1076,7 +1076,7 @@

Parameters:
Source:
@@ -1240,7 +1240,7 @@
Parameters:
Source:
@@ -1428,7 +1428,7 @@
Parameters:
Source:
diff --git a/docs/docs/GeocodedLocation.html b/docs/docs/GeocodedLocation.html index b7e51215b..6f6b66af9 100644 --- a/docs/docs/GeocodedLocation.html +++ b/docs/docs/GeocodedLocation.html @@ -119,7 +119,7 @@

Source:
@@ -254,7 +254,7 @@

Properties:
- Bounding box representing this location + Bounding box representing this location on a map. @@ -318,7 +318,7 @@
Properties:
Source:
diff --git a/docs/docs/GeocoderSearch.html b/docs/docs/GeocoderSearch.html index faf32c0c6..a90caa49e 100644 --- a/docs/docs/GeocoderSearch.html +++ b/docs/docs/GeocoderSearch.html @@ -124,7 +124,7 @@

Source:
@@ -225,7 +225,7 @@

Source:
@@ -293,7 +293,7 @@

Source:
@@ -429,7 +429,7 @@

Parameters:
Source:
@@ -597,7 +597,7 @@
Parameters:
Source:
diff --git a/docs/docs/Geohash.html b/docs/docs/Geohash.html index 2a0f51554..ee3ff54e5 100644 --- a/docs/docs/Geohash.html +++ b/docs/docs/Geohash.html @@ -4543,7 +4543,7 @@
Parameters:
Source:
diff --git a/docs/docs/Geohashes.html b/docs/docs/Geohashes.html index e4840a2f4..a81342b73 100644 --- a/docs/docs/Geohashes.html +++ b/docs/docs/Geohashes.html @@ -678,7 +678,7 @@
Parameters:
Source:
@@ -878,7 +878,7 @@
Parameters:
Source:
@@ -1141,7 +1141,7 @@

Source:
@@ -1663,7 +1663,7 @@

Source:
@@ -1832,7 +1832,7 @@

Parameters:
Source:
@@ -2138,7 +2138,7 @@
Parameters:
Source:
@@ -2305,7 +2305,7 @@
Parameters:
Source:
@@ -2498,7 +2498,7 @@
Parameters:
Source:
@@ -2688,7 +2688,7 @@
Parameters:
Source:
@@ -3158,7 +3158,7 @@
Parameters:
Source:
@@ -3373,7 +3373,7 @@
Parameters:
Source:
@@ -3613,7 +3613,7 @@
Parameters:
Source:
@@ -3730,7 +3730,7 @@

Source:
@@ -3875,7 +3875,7 @@

Parameters:
Source:
@@ -4042,7 +4042,7 @@
Parameters:
Source:
@@ -4732,7 +4732,7 @@
Parameters:
Source:
@@ -4849,7 +4849,7 @@

Source:
@@ -4966,7 +4966,7 @@

Source:
diff --git a/docs/docs/GoogleAnalytics.html b/docs/docs/GoogleAnalytics.html index e42a1cd7d..457b2c8fc 100644 --- a/docs/docs/GoogleAnalytics.html +++ b/docs/docs/GoogleAnalytics.html @@ -124,7 +124,7 @@

Source:
@@ -252,7 +252,7 @@

Type:
Source:
@@ -335,7 +335,7 @@
Type:
Source:
@@ -499,7 +499,7 @@
Parameters:
Source:
@@ -620,7 +620,7 @@

Source:
@@ -738,7 +738,7 @@

Source:
@@ -859,7 +859,7 @@

Source:
@@ -980,7 +980,7 @@

Source:
@@ -1079,7 +1079,7 @@

Source:
@@ -1201,7 +1201,7 @@

Source:
@@ -1419,7 +1419,7 @@

Parameters:
Source:
@@ -1616,7 +1616,7 @@
Parameters:
Source:
@@ -1788,7 +1788,7 @@
Parameters:
Source:
diff --git a/docs/docs/GoogleMapsAutocompleter.html b/docs/docs/GoogleMapsAutocompleter.html index 274f21d6b..f1ac7f98d 100644 --- a/docs/docs/GoogleMapsAutocompleter.html +++ b/docs/docs/GoogleMapsAutocompleter.html @@ -124,7 +124,7 @@

Source:
@@ -224,7 +224,7 @@

Source:
@@ -360,7 +360,7 @@

Parameters:
Source:
@@ -527,7 +527,7 @@
Parameters:
Source:
diff --git a/docs/docs/GoogleMapsGeocoder.html b/docs/docs/GoogleMapsGeocoder.html index 1489a9308..d3636c440 100644 --- a/docs/docs/GoogleMapsGeocoder.html +++ b/docs/docs/GoogleMapsGeocoder.html @@ -124,7 +124,7 @@

Source:
@@ -224,7 +224,7 @@

Source:
@@ -267,7 +267,7 @@

Use the Google Maps Geocoder API to convert a Google Maps Place ID into -a geocoded object that includes latitude and longitude information +a geocoded object that includes latitude and longitude information along with a bound box for viewing the location.
@@ -362,7 +362,7 @@

Parameters:
Source:
@@ -530,7 +530,7 @@
Parameters:
Source:
diff --git a/docs/docs/GroupListView.html b/docs/docs/GroupListView.html index eb90e3161..064b41166 100644 --- a/docs/docs/GroupListView.html +++ b/docs/docs/GroupListView.html @@ -130,7 +130,7 @@

Source:
diff --git a/docs/docs/IconUtilities.html b/docs/docs/IconUtilities.html index f8513c3fb..556231480 100644 --- a/docs/docs/IconUtilities.html +++ b/docs/docs/IconUtilities.html @@ -98,7 +98,7 @@

IconUtilities

Source:
@@ -270,7 +270,7 @@
Parameters:
Source:
@@ -414,7 +414,7 @@
Parameters:
Source:
@@ -680,7 +680,7 @@
Parameters:
Source:
@@ -848,7 +848,7 @@
Parameters:
Source:
@@ -1017,7 +1017,7 @@
Parameters:
Source:
@@ -1185,7 +1185,7 @@
Parameters:
Source:
@@ -1328,7 +1328,7 @@
Parameters:
Source:
@@ -1542,7 +1542,7 @@
Parameters:
Source:
@@ -1689,7 +1689,7 @@
Parameters:
Source:
diff --git a/docs/docs/ImageUploaderView.html b/docs/docs/ImageUploaderView.html index a0829815f..89a11d471 100644 --- a/docs/docs/ImageUploaderView.html +++ b/docs/docs/ImageUploaderView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -319,7 +319,7 @@
Type:
Source:
@@ -399,7 +399,7 @@
Type:
Source:
@@ -479,7 +479,7 @@
Type:
Source:
@@ -560,7 +560,7 @@
Type:
Source:
@@ -641,7 +641,7 @@
Type:
Source:
@@ -721,7 +721,7 @@
Type:
Source:
@@ -801,7 +801,7 @@
Type:
Source:
@@ -882,7 +882,7 @@
Type:
Source:
@@ -960,7 +960,7 @@
Type:
Source:
@@ -1028,7 +1028,7 @@

Source:
@@ -1106,7 +1106,7 @@

Type:
Source:
@@ -1184,7 +1184,7 @@
Type:
Source:
@@ -1263,7 +1263,7 @@
Type:
Source:
@@ -1343,7 +1343,7 @@
Type:
Source:
@@ -1504,7 +1504,7 @@
Parameters:
Source:
@@ -1931,7 +1931,7 @@
Properties:
Source:
@@ -2028,7 +2028,7 @@

Source:
@@ -2247,7 +2247,7 @@

Parameters:
Source:
@@ -2344,7 +2344,7 @@

Source:
@@ -2438,7 +2438,7 @@

Source:
@@ -2582,7 +2582,7 @@

Parameters:
Source:
@@ -2752,7 +2752,7 @@
Parameters:
Source:
@@ -2847,7 +2847,7 @@

Source:
diff --git a/docs/docs/LayerCategoryItemView.html b/docs/docs/LayerCategoryItemView.html index 50497c1e6..aa270be9c 100644 --- a/docs/docs/LayerCategoryItemView.html +++ b/docs/docs/LayerCategoryItemView.html @@ -135,7 +135,7 @@

Source:
@@ -256,7 +256,7 @@

Type:
Source:
@@ -334,7 +334,7 @@
Type:
Source:
@@ -412,7 +412,7 @@
Type:
Source:
@@ -490,7 +490,7 @@
Type:
Source:
@@ -572,7 +572,7 @@

Source:
@@ -715,7 +715,7 @@

Parameters:
Source:
@@ -809,7 +809,7 @@

Source:
@@ -903,7 +903,7 @@

Source:
@@ -1080,7 +1080,7 @@

Parameters:
Source:
@@ -1198,7 +1198,7 @@

Source:
@@ -1292,7 +1292,7 @@

Source:
diff --git a/docs/docs/LayerCategoryListView.html b/docs/docs/LayerCategoryListView.html index e546d936c..49b99538a 100644 --- a/docs/docs/LayerCategoryListView.html +++ b/docs/docs/LayerCategoryListView.html @@ -134,7 +134,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -333,7 +333,7 @@
Type:
Source:
@@ -411,7 +411,7 @@
Type:
Source:
@@ -546,7 +546,7 @@
Parameters:
Source:
@@ -640,7 +640,7 @@

Source:
diff --git a/docs/docs/LayerDetailView.html b/docs/docs/LayerDetailView.html index 80797fab8..436f0e0e4 100644 --- a/docs/docs/LayerDetailView.html +++ b/docs/docs/LayerDetailView.html @@ -137,7 +137,7 @@

Source:
@@ -258,7 +258,7 @@

Type:
Source:
@@ -474,7 +474,7 @@
Properties:
Source:
@@ -553,7 +553,7 @@
Type:
Source:
@@ -631,7 +631,7 @@
Type:
Source:
@@ -709,7 +709,7 @@
Type:
Source:
@@ -787,7 +787,7 @@
Type:
Source:
@@ -865,7 +865,7 @@
Type:
Source:
@@ -943,7 +943,7 @@
Type:
Source:
@@ -1031,7 +1031,7 @@

Source:
@@ -1204,7 +1204,7 @@

Parameters:
Source:
@@ -1299,7 +1299,7 @@

Source:
@@ -1393,7 +1393,7 @@

Source:
@@ -1509,7 +1509,7 @@

Source:
diff --git a/docs/docs/LayerDetailsView.html b/docs/docs/LayerDetailsView.html index 933f88600..c1a1f5432 100644 --- a/docs/docs/LayerDetailsView.html +++ b/docs/docs/LayerDetailsView.html @@ -135,7 +135,7 @@

Source:
@@ -256,7 +256,7 @@

Type:
Source:
@@ -506,7 +506,7 @@
Properties:
Source:
@@ -584,7 +584,7 @@
Type:
Source:
@@ -662,7 +662,7 @@
Type:
Source:
@@ -742,7 +742,7 @@
Type:
Source:
@@ -820,7 +820,7 @@
Type:
Source:
@@ -898,7 +898,7 @@
Type:
Source:
@@ -985,7 +985,7 @@

Source:
@@ -1081,7 +1081,7 @@

Source:
@@ -1254,7 +1254,7 @@

Parameters:
Source:
@@ -1349,7 +1349,7 @@

Source:
@@ -1443,7 +1443,7 @@

Source:
@@ -1614,7 +1614,7 @@

Parameters:
Source:
diff --git a/docs/docs/LayerInfoView.html b/docs/docs/LayerInfoView.html index adf42b2dd..4e44ce22a 100644 --- a/docs/docs/LayerInfoView.html +++ b/docs/docs/LayerInfoView.html @@ -134,7 +134,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -333,7 +333,7 @@
Type:
Source:
@@ -411,7 +411,7 @@
Type:
Source:
@@ -489,7 +489,7 @@
Type:
Source:
@@ -567,7 +567,7 @@
Type:
Source:
@@ -714,7 +714,7 @@
Parameters:
Source:
@@ -808,7 +808,7 @@

Source:
diff --git a/docs/docs/LayerItemView.html b/docs/docs/LayerItemView.html index 5337d1fb6..0d6095532 100644 --- a/docs/docs/LayerItemView.html +++ b/docs/docs/LayerItemView.html @@ -138,7 +138,7 @@

Source:
@@ -259,7 +259,7 @@

Type:
Source:
@@ -556,7 +556,7 @@
Properties:
Source:
@@ -636,7 +636,7 @@
Type:
Source:
@@ -715,7 +715,7 @@
Type:
Source:
@@ -793,7 +793,7 @@
Type:
Source:
@@ -871,7 +871,7 @@
Type:
Source:
@@ -949,7 +949,7 @@
Type:
Source:
@@ -1036,7 +1036,7 @@

Source:
@@ -1214,7 +1214,7 @@

Parameters:
Source:
@@ -1309,7 +1309,7 @@

Source:
@@ -1404,7 +1404,7 @@

Source:
@@ -1498,7 +1498,7 @@

Source:
@@ -1676,7 +1676,7 @@

Parameters:
Source:
@@ -1885,7 +1885,7 @@
Parameters:
Source:
@@ -2030,7 +2030,7 @@
Parameters:
Source:
@@ -2125,7 +2125,7 @@

Source:
@@ -2224,7 +2224,7 @@

Source:
@@ -2319,7 +2319,7 @@

Source:
@@ -2415,7 +2415,7 @@

Source:
@@ -2511,7 +2511,7 @@

Source:
@@ -2606,7 +2606,7 @@

Source:
diff --git a/docs/docs/LayerListView.html b/docs/docs/LayerListView.html index b877a0204..51b608e98 100644 --- a/docs/docs/LayerListView.html +++ b/docs/docs/LayerListView.html @@ -135,7 +135,7 @@

Source:
@@ -256,7 +256,7 @@

Type:
Source:
@@ -334,7 +334,7 @@
Type:
Source:
@@ -412,7 +412,7 @@
Type:
Source:
@@ -491,7 +491,7 @@
Type:
Source:
@@ -569,7 +569,7 @@
Type:
Source:
@@ -647,7 +647,7 @@
Type:
Source:
@@ -794,7 +794,7 @@
Parameters:
Source:
@@ -891,7 +891,7 @@

Source:
@@ -985,7 +985,7 @@

Source:
@@ -1162,7 +1162,7 @@

Parameters:
Source:
@@ -1281,7 +1281,7 @@

Source:
diff --git a/docs/docs/LayerNavigationView.html b/docs/docs/LayerNavigationView.html index f2a623141..998590c88 100644 --- a/docs/docs/LayerNavigationView.html +++ b/docs/docs/LayerNavigationView.html @@ -135,7 +135,7 @@

Source:
@@ -256,7 +256,7 @@

Type:
Source:
@@ -387,7 +387,7 @@
Properties:
Source:
@@ -465,7 +465,7 @@
Type:
Source:
@@ -543,7 +543,7 @@
Type:
Source:
@@ -621,7 +621,7 @@
Type:
Source:
@@ -709,7 +709,7 @@

Source:
@@ -824,7 +824,7 @@

Source:
@@ -979,7 +979,7 @@

Parameters:
Source:
@@ -1073,7 +1073,7 @@

Source:
diff --git a/docs/docs/LayerOpacityView.html b/docs/docs/LayerOpacityView.html index 09779660b..7dc4d6349 100644 --- a/docs/docs/LayerOpacityView.html +++ b/docs/docs/LayerOpacityView.html @@ -135,7 +135,7 @@

Source:
@@ -256,7 +256,7 @@

Type:
Source:
@@ -462,7 +462,7 @@
Properties:
Source:
@@ -540,7 +540,7 @@
Type:
Source:
@@ -618,7 +618,7 @@
Type:
Source:
@@ -696,7 +696,7 @@
Type:
Source:
@@ -774,7 +774,7 @@
Type:
Source:
@@ -921,7 +921,7 @@
Parameters:
Source:
@@ -1016,7 +1016,7 @@

Source:
@@ -1110,7 +1110,7 @@

Source:
@@ -1276,7 +1276,7 @@

Parameters:
Source:
@@ -1420,7 +1420,7 @@
Parameters:
Source:
@@ -1515,7 +1515,7 @@

Source:
diff --git a/docs/docs/LayersPanelView.html b/docs/docs/LayersPanelView.html index c40f7814d..23e717bed 100644 --- a/docs/docs/LayersPanelView.html +++ b/docs/docs/LayersPanelView.html @@ -654,7 +654,7 @@

Parameters:
Source:
diff --git a/docs/docs/LegendView.html b/docs/docs/LegendView.html index 933eb3356..a618c776f 100644 --- a/docs/docs/LegendView.html +++ b/docs/docs/LegendView.html @@ -137,7 +137,7 @@

Source:
@@ -258,7 +258,7 @@

Type:
Source:
@@ -461,7 +461,7 @@
Properties:
Source:
@@ -539,7 +539,7 @@
Type:
Source:
@@ -619,7 +619,7 @@
Type:
Source:
@@ -698,7 +698,7 @@
Type:
Source:
@@ -878,7 +878,7 @@
Properties:
Source:
@@ -956,7 +956,7 @@
Type:
Source:
@@ -1034,7 +1034,7 @@
Type:
Source:
@@ -1272,7 +1272,7 @@
Properties:
Source:
@@ -1449,7 +1449,7 @@
Parameters:
Source:
@@ -1543,7 +1543,7 @@

Source:
@@ -1710,7 +1710,7 @@

Parameters:
Source:
@@ -1855,7 +1855,7 @@
Parameters:
Source:
@@ -1999,7 +1999,7 @@
Parameters:
Source:
diff --git a/docs/docs/LogsSearch.html b/docs/docs/LogsSearch.html index 224d395f8..388434370 100644 --- a/docs/docs/LogsSearch.html +++ b/docs/docs/LogsSearch.html @@ -123,7 +123,7 @@

Source:
diff --git a/docs/docs/LookupModel.html b/docs/docs/LookupModel.html index ad99efb3a..bd44dab1e 100644 --- a/docs/docs/LookupModel.html +++ b/docs/docs/LookupModel.html @@ -121,7 +121,7 @@

Source:
@@ -354,7 +354,7 @@

Parameters:
Source:
@@ -525,7 +525,7 @@
Parameters:
Source:
@@ -718,7 +718,7 @@
Parameters:
Source:
@@ -914,7 +914,7 @@
Parameters:
Source:
diff --git a/docs/docs/Map.html b/docs/docs/Map.html index 7d6c1292d..f0130d63b 100644 --- a/docs/docs/Map.html +++ b/docs/docs/Map.html @@ -120,7 +120,7 @@

Source:
@@ -245,7 +245,7 @@

Source:
diff --git a/docs/docs/MapAsset.html b/docs/docs/MapAsset.html index 020656d88..d41e2572a 100644 --- a/docs/docs/MapAsset.html +++ b/docs/docs/MapAsset.html @@ -128,7 +128,7 @@

Source:
@@ -1062,7 +1062,7 @@

Properties:
Source:
@@ -1140,7 +1140,7 @@
Type:
Source:
@@ -1280,7 +1280,7 @@
Parameters:
Source:
@@ -1451,7 +1451,7 @@
Parameters:
Source:
@@ -1620,7 +1620,7 @@
Parameters:
Source:
@@ -1788,7 +1788,7 @@
Parameters:
Source:
@@ -1984,7 +1984,7 @@
Parameters:
Source:
@@ -2182,7 +2182,7 @@
Parameters:
Source:
@@ -2303,7 +2303,7 @@

Source:
@@ -2472,7 +2472,7 @@

Parameters:
Source:
@@ -2640,7 +2640,7 @@
Parameters:
Source:
@@ -2761,7 +2761,7 @@

Source:
@@ -2917,7 +2917,7 @@

Parameters:
Source:
@@ -3012,7 +3012,7 @@

Source:
@@ -3132,7 +3132,7 @@

Source:
@@ -3230,7 +3230,7 @@

Source:
@@ -3380,7 +3380,7 @@

Parameters:
Source:
@@ -3477,7 +3477,7 @@

Source:
@@ -3574,7 +3574,7 @@

Source:
@@ -3669,7 +3669,7 @@

Source:
@@ -3813,7 +3813,7 @@

Parameters:
Source:
@@ -3966,7 +3966,7 @@
Parameters:
Source:
@@ -4083,7 +4083,7 @@

Source:
@@ -4252,7 +4252,7 @@

Parameters:
Source:
diff --git a/docs/docs/MapAssets.html b/docs/docs/MapAssets.html index 01b675274..e24ebce76 100644 --- a/docs/docs/MapAssets.html +++ b/docs/docs/MapAssets.html @@ -353,7 +353,7 @@
Parameters:
Source:
@@ -527,7 +527,7 @@
Parameters:
Source:
@@ -706,7 +706,7 @@
Parameters:
Source:
@@ -880,7 +880,7 @@
Parameters:
Source:
@@ -997,7 +997,7 @@

Source:
@@ -1321,7 +1321,7 @@

Parameters:
Source:
diff --git a/docs/docs/MapConfig.html b/docs/docs/MapConfig.html index 2f94d05b5..9df9ee7b9 100644 --- a/docs/docs/MapConfig.html +++ b/docs/docs/MapConfig.html @@ -401,7 +401,7 @@
Properties:
Whether or not to show the viewfinder UI and viewfinder button in the toolbar. The ViewfinderView -requires a Google Maps API key present in the AppModel. In order to +requires a Google Maps API key present in the AppModel. In order to work properly the Geocoding API and Places API must be enabled. @@ -792,7 +792,7 @@
Properties:
Source:
@@ -1321,7 +1321,7 @@
Properties:
Source:
@@ -1551,7 +1551,7 @@
Properties:
Source:
@@ -1574,7 +1574,7 @@
Examples
color: { red: 0, green: 0.1, - blue: 1 + blue: 1 } } @@ -2043,7 +2043,7 @@
Properties:
Source:
@@ -2125,7 +2125,7 @@
Type:
Source:
@@ -2300,7 +2300,7 @@
Properties:
Source:
@@ -2535,7 +2535,7 @@
Properties:
Source:
@@ -3384,7 +3384,7 @@
Properties:
Source:
@@ -3599,7 +3599,7 @@
Properties:
Source:
@@ -3830,7 +3830,7 @@
Properties:
Source:
@@ -4073,7 +4073,7 @@
Properties:
Source:
diff --git a/docs/docs/MapHelpPanel.html b/docs/docs/MapHelpPanel.html index 2d5dc3716..44450f3d7 100644 --- a/docs/docs/MapHelpPanel.html +++ b/docs/docs/MapHelpPanel.html @@ -333,7 +333,7 @@
Type:
Source:
@@ -411,7 +411,7 @@
Type:
Source:
@@ -590,7 +590,7 @@
Properties:
Source:
@@ -673,7 +673,7 @@
Type:
Source:
@@ -756,7 +756,7 @@
Type:
Source:
@@ -1014,7 +1014,7 @@
Parameters:
Source:
@@ -1400,7 +1400,7 @@

Source:
@@ -1566,7 +1566,7 @@

Parameters:
Source:
@@ -1733,7 +1733,7 @@
Parameters:
Source:
@@ -1900,7 +1900,7 @@
Parameters:
Source:
@@ -2088,7 +2088,7 @@
Parameters:
Source:
diff --git a/docs/docs/MapInteraction.html b/docs/docs/MapInteraction.html index 9875de52f..53f823270 100644 --- a/docs/docs/MapInteraction.html +++ b/docs/docs/MapInteraction.html @@ -126,7 +126,7 @@

Source:
@@ -247,7 +247,7 @@

Type:
Source:
@@ -333,7 +333,7 @@

Source:
@@ -716,7 +716,7 @@

Properties:
Source:
@@ -908,7 +908,7 @@
Parameters:
Source:
@@ -1053,7 +1053,7 @@
Parameters:
Source:
@@ -1148,7 +1148,7 @@

Source:
@@ -1250,7 +1250,7 @@

Source:
@@ -1403,7 +1403,7 @@

Parameters:
Source:
@@ -1556,7 +1556,7 @@
Parameters:
Source:
@@ -1700,7 +1700,7 @@
Parameters:
Source:
@@ -1816,7 +1816,7 @@

Source:
@@ -2068,7 +2068,7 @@

Parameters:
Source:
@@ -2217,7 +2217,7 @@
Parameters:
Source:
@@ -2361,7 +2361,7 @@
Parameters:
Source:
@@ -2556,7 +2556,7 @@
Parameters:
Source:
@@ -2723,7 +2723,7 @@
Parameters:
Source:
@@ -2890,7 +2890,7 @@
Parameters:
Source:
diff --git a/docs/docs/MapModel.html b/docs/docs/MapModel.html index a03dde5d4..8f7ae1732 100644 --- a/docs/docs/MapModel.html +++ b/docs/docs/MapModel.html @@ -124,7 +124,7 @@

Source:
@@ -932,7 +932,7 @@

Properties:
Source:
@@ -1018,7 +1018,7 @@
Type:
Source:
@@ -1162,7 +1162,7 @@
Parameters:
Source:
@@ -1285,7 +1285,7 @@

Source:
@@ -1383,7 +1383,7 @@

Source:
@@ -1496,7 +1496,7 @@

Source:
@@ -1618,7 +1618,7 @@

Source:
@@ -1785,7 +1785,7 @@

Parameters:
Source:
@@ -1931,7 +1931,7 @@
Parameters:
Source:
@@ -2026,7 +2026,7 @@

Source:
@@ -2124,7 +2124,7 @@

Source:
@@ -2291,7 +2291,7 @@

Parameters:
Source:
@@ -2388,7 +2388,7 @@

Source:
@@ -2567,7 +2567,7 @@

Parameters:
Source:
diff --git a/docs/docs/MapSearchConnector.html b/docs/docs/MapSearchConnector.html index 6bb474a19..60f38ada7 100644 --- a/docs/docs/MapSearchConnector.html +++ b/docs/docs/MapSearchConnector.html @@ -121,7 +121,7 @@

Source:
@@ -250,7 +250,7 @@

Type:
Source:
@@ -343,7 +343,7 @@

Source:
@@ -437,7 +437,7 @@

Source:
@@ -559,7 +559,7 @@

Source:
@@ -771,7 +771,7 @@

Properties:
Source:
@@ -866,7 +866,7 @@

Source:
@@ -1012,7 +1012,7 @@

Parameters:
Source:
@@ -1201,7 +1201,7 @@
Parameters:
Source:
@@ -1324,7 +1324,7 @@

Source:
@@ -1442,7 +1442,7 @@

Source:
@@ -1559,7 +1559,7 @@

Source:
@@ -1678,7 +1678,7 @@

Source:
@@ -2006,7 +2006,7 @@

Parameters:
Source:
@@ -2102,7 +2102,7 @@

Source:
@@ -2246,7 +2246,7 @@

Parameters:
Source:
@@ -2341,7 +2341,7 @@

Source:
@@ -2441,7 +2441,7 @@

Source:
@@ -2541,7 +2541,7 @@

Source:
diff --git a/docs/docs/MapSearchFiltersConnector.html b/docs/docs/MapSearchFiltersConnector.html index f38067530..42f3d6439 100644 --- a/docs/docs/MapSearchFiltersConnector.html +++ b/docs/docs/MapSearchFiltersConnector.html @@ -124,7 +124,7 @@

Source:
@@ -250,7 +250,7 @@

Source:
@@ -346,7 +346,7 @@

Source:
@@ -444,7 +444,7 @@

Source:
@@ -636,7 +636,7 @@

Properties:
Source:
@@ -801,7 +801,7 @@
Parameters:
Source:
@@ -968,7 +968,7 @@
Parameters:
Source:
@@ -1062,7 +1062,7 @@

Source:
@@ -1178,7 +1178,7 @@

Source:
@@ -1654,7 +1654,7 @@

Properties
Source:
@@ -1748,7 +1748,7 @@

Source:
@@ -1865,7 +1865,7 @@

Source:
@@ -1961,7 +1961,7 @@

Source:
@@ -2177,7 +2177,7 @@

Parameters:
Source:
@@ -2378,7 +2378,7 @@
Parameters:
Source:
@@ -2534,7 +2534,7 @@
Parameters:
Source:
@@ -2682,7 +2682,7 @@
Parameters:
Source:
diff --git a/docs/docs/MapView.html b/docs/docs/MapView.html index 7595c014d..1a965d579 100644 --- a/docs/docs/MapView.html +++ b/docs/docs/MapView.html @@ -134,7 +134,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -333,7 +333,7 @@
Type:
Source:
@@ -411,7 +411,7 @@
Type:
Source:
@@ -489,7 +489,7 @@
Type:
Source:
@@ -567,7 +567,7 @@
Type:
Source:
@@ -656,7 +656,7 @@

Source:
@@ -822,7 +822,7 @@

Parameters:
Source:
@@ -919,7 +919,7 @@

Source:
@@ -1013,7 +1013,7 @@

Source:
@@ -1131,7 +1131,7 @@

Source:
@@ -1248,7 +1248,7 @@

Source:
@@ -1366,7 +1366,7 @@

Source:
@@ -1483,7 +1483,7 @@

Source:
@@ -1600,7 +1600,7 @@

Source:
diff --git a/docs/docs/MarkdownEditorView.html b/docs/docs/MarkdownEditorView.html index 13b855b88..5baa73045 100644 --- a/docs/docs/MarkdownEditorView.html +++ b/docs/docs/MarkdownEditorView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -319,7 +319,7 @@
Type:
Source:
@@ -397,7 +397,7 @@
Type:
Source:
@@ -486,7 +486,7 @@
Type:
Source:
@@ -575,7 +575,7 @@
Type:
Source:
@@ -657,7 +657,7 @@
Type:
Source:
@@ -736,7 +736,7 @@
Type:
Source:
@@ -816,7 +816,7 @@
Type:
Source:
@@ -895,7 +895,7 @@
Type:
Source:
@@ -974,7 +974,7 @@
Type:
Source:
@@ -1052,7 +1052,7 @@
Type:
Source:
@@ -1233,7 +1233,7 @@
Parameters:
Source:
@@ -1445,7 +1445,7 @@
Parameters:
Source:
@@ -1544,7 +1544,7 @@

Source:
@@ -1738,7 +1738,7 @@

Parameters:
Source:
@@ -1881,7 +1881,7 @@
Parameters:
Source:
@@ -1975,7 +1975,7 @@

Source:
@@ -2070,7 +2070,7 @@

Source:
@@ -2263,7 +2263,7 @@

Parameters:
Source:
@@ -2358,7 +2358,7 @@

Source:
diff --git a/docs/docs/MarkdownView.html b/docs/docs/MarkdownView.html index b3cfccf4a..31f998cfe 100644 --- a/docs/docs/MarkdownView.html +++ b/docs/docs/MarkdownView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -319,7 +319,7 @@
Type:
Source:
@@ -398,7 +398,7 @@
Type:
Source:
@@ -476,7 +476,7 @@
Type:
Source:
@@ -556,7 +556,7 @@
Type:
Source:
@@ -634,7 +634,7 @@
Type:
Source:
@@ -712,7 +712,7 @@
Type:
Source:
@@ -847,7 +847,7 @@
Parameters:
Source:
@@ -1086,7 +1086,7 @@

Source:
@@ -1181,7 +1181,7 @@

Source:
@@ -1275,7 +1275,7 @@

Source:
diff --git a/docs/docs/MdqRunView.html b/docs/docs/MdqRunView.html index 3e1c8e818..d19c14360 100644 --- a/docs/docs/MdqRunView.html +++ b/docs/docs/MdqRunView.html @@ -120,7 +120,7 @@

Source:
@@ -245,7 +245,7 @@

Type:
Source:
@@ -323,7 +323,7 @@
Type:
Source:
@@ -401,7 +401,7 @@
Type:
Source:
@@ -487,7 +487,7 @@

Source:
@@ -581,7 +581,7 @@

Source:
@@ -823,7 +823,7 @@

Parameters:
Source:
diff --git a/docs/docs/MetacatUI.html b/docs/docs/MetacatUI.html index 2a196095b..9f6dbe05c 100644 --- a/docs/docs/MetacatUI.html +++ b/docs/docs/MetacatUI.html @@ -187,7 +187,7 @@
Type:
Source:
@@ -266,7 +266,7 @@
Type:
Source:
@@ -345,7 +345,7 @@
Type:
Source:
@@ -508,7 +508,7 @@
Type:
Source:
@@ -671,7 +671,7 @@
Type:
Source:
@@ -755,7 +755,7 @@
Type:
Source:
@@ -844,7 +844,7 @@

Source:
diff --git a/docs/docs/MetadataView.html b/docs/docs/MetadataView.html index c97963a34..1b98debb6 100644 --- a/docs/docs/MetadataView.html +++ b/docs/docs/MetadataView.html @@ -130,7 +130,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -344,7 +344,7 @@

Source:
@@ -440,7 +440,7 @@

Source:
@@ -534,7 +534,7 @@

Source:
@@ -726,7 +726,7 @@

Parameters:
Source:
@@ -922,7 +922,7 @@
Parameters:
Source:
@@ -1147,7 +1147,7 @@
Parameters:
Source:
@@ -1372,7 +1372,7 @@
Parameters:
Source:
@@ -1470,7 +1470,7 @@

Source:
@@ -1567,7 +1567,7 @@

Source:
@@ -1713,7 +1713,7 @@

Parameters:
Source:
@@ -1809,7 +1809,7 @@

Source:
@@ -2051,7 +2051,7 @@

Properties
Source:
@@ -2217,7 +2217,7 @@
Parameters:
Source:
@@ -2393,7 +2393,7 @@
Parameters:
Source:
@@ -2488,7 +2488,7 @@

Source:
@@ -2659,7 +2659,7 @@

Parameters:
Source:
@@ -2777,7 +2777,7 @@

Source:
@@ -2874,7 +2874,7 @@

Source:
@@ -3024,7 +3024,7 @@

Parameters:
Source:
@@ -3196,7 +3196,7 @@
Parameters:
Source:
@@ -3292,7 +3292,7 @@

Source:
@@ -3391,7 +3391,7 @@

Source:
@@ -3512,7 +3512,7 @@

Source:
diff --git a/docs/docs/MetricModalView.html b/docs/docs/MetricModalView.html index 693221296..f5e918532 100644 --- a/docs/docs/MetricModalView.html +++ b/docs/docs/MetricModalView.html @@ -121,7 +121,7 @@

Source:
@@ -242,7 +242,7 @@

Type:
Source:
@@ -321,7 +321,7 @@
Type:
Source:
@@ -399,7 +399,7 @@
Type:
Source:
@@ -477,7 +477,7 @@
Type:
Source:
@@ -556,7 +556,7 @@
Type:
Source:
@@ -634,7 +634,7 @@
Type:
Source:
@@ -712,7 +712,7 @@
Type:
Source:
@@ -790,7 +790,7 @@
Type:
Source:
@@ -876,7 +876,7 @@

Source:
@@ -1022,7 +1022,7 @@

Parameters:
Source:
@@ -1138,7 +1138,7 @@

Source:
@@ -1254,7 +1254,7 @@

Source:
@@ -1518,7 +1518,7 @@

Properties
Source:
@@ -1612,7 +1612,7 @@

Source:
@@ -1706,7 +1706,7 @@

Source:
@@ -1800,7 +1800,7 @@

Source:
@@ -1916,7 +1916,7 @@

Source:
@@ -2010,7 +2010,7 @@

Source:
@@ -2104,7 +2104,7 @@

Source:
@@ -2198,7 +2198,7 @@

Source:
@@ -2292,7 +2292,7 @@

Source:
@@ -2386,7 +2386,7 @@

Source:
@@ -2480,7 +2480,7 @@

Source:
@@ -2574,7 +2574,7 @@

Source:
diff --git a/docs/docs/MetricView.html b/docs/docs/MetricView.html index 04edcf0eb..efa7c476d 100644 --- a/docs/docs/MetricView.html +++ b/docs/docs/MetricView.html @@ -130,7 +130,7 @@

Source:
@@ -251,7 +251,7 @@

Type:
Source:
@@ -329,7 +329,7 @@
Type:
Source:
@@ -407,7 +407,7 @@
Type:
Source:
@@ -636,7 +636,7 @@
Properties:
Source:
@@ -730,7 +730,7 @@

Source:
@@ -824,7 +824,7 @@

Source:
@@ -918,7 +918,7 @@

Source:
@@ -1012,7 +1012,7 @@

Source:
@@ -1106,7 +1106,7 @@

Source:
diff --git a/docs/docs/Metrics.html b/docs/docs/Metrics.html index 102e04a88..2104bf86d 100644 --- a/docs/docs/Metrics.html +++ b/docs/docs/Metrics.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -327,7 +327,7 @@

Source:
@@ -421,7 +421,7 @@

Source:
@@ -582,7 +582,7 @@

Parameters:
Source:
@@ -676,7 +676,7 @@

Source:
diff --git a/docs/docs/MetricsChartView.html b/docs/docs/MetricsChartView.html index 8f0b48f62..b5c4dd246 100644 --- a/docs/docs/MetricsChartView.html +++ b/docs/docs/MetricsChartView.html @@ -130,7 +130,7 @@

Source:
@@ -255,7 +255,7 @@

Source:
@@ -349,7 +349,7 @@

Source:
@@ -443,7 +443,7 @@

Source:
@@ -537,7 +537,7 @@

Source:
diff --git a/docs/docs/NavbarView.html b/docs/docs/NavbarView.html index 1bfdffcc8..069d9bfb1 100644 --- a/docs/docs/NavbarView.html +++ b/docs/docs/NavbarView.html @@ -130,7 +130,7 @@

Type:
Source:
@@ -321,7 +321,7 @@
Type:
Source:
@@ -395,7 +395,7 @@
Type:
Source:
diff --git a/docs/docs/NodeSelect.html b/docs/docs/NodeSelect.html index 5bde86bd4..433633391 100644 --- a/docs/docs/NodeSelect.html +++ b/docs/docs/NodeSelect.html @@ -134,7 +134,7 @@

Source:
diff --git a/docs/docs/NumericFilter.html b/docs/docs/NumericFilter.html index 7b2fc15bc..94a6752f7 100644 --- a/docs/docs/NumericFilter.html +++ b/docs/docs/NumericFilter.html @@ -120,7 +120,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -386,7 +386,7 @@
Parameters:
Source:
@@ -698,7 +698,7 @@
Properties:
Source:
@@ -847,7 +847,7 @@
Parameters:
Source:
@@ -968,7 +968,7 @@

Source:
@@ -1084,7 +1084,7 @@

Source:
@@ -1263,7 +1263,7 @@

Parameters:
Source:
@@ -1390,7 +1390,7 @@

Source:
@@ -1511,7 +1511,7 @@

Source:
@@ -1676,7 +1676,7 @@

Parameters:
Source:
@@ -1847,7 +1847,7 @@
Parameters:
Source:
@@ -1964,7 +1964,7 @@

Source:
@@ -2089,7 +2089,7 @@

Source:
@@ -2253,7 +2253,7 @@

Parameters:
Source:
@@ -2439,7 +2439,7 @@
Parameters:
Source:
@@ -2555,7 +2555,7 @@

Source:
@@ -2703,7 +2703,7 @@

Parameters:
Source:
@@ -2873,7 +2873,7 @@
Parameters:
Source:
@@ -3089,7 +3089,7 @@
Parameters:
Source:
@@ -3288,7 +3288,7 @@
Parameters:
Source:
@@ -3419,7 +3419,7 @@

Source:
@@ -3514,7 +3514,7 @@

Source:
@@ -3686,7 +3686,7 @@

Parameters:
Source:
@@ -3846,7 +3846,7 @@
Parameters:
Source:
@@ -4028,7 +4028,7 @@
Parameters:
Source:
@@ -4149,7 +4149,7 @@

Source:
diff --git a/docs/docs/NumericFilterView.html b/docs/docs/NumericFilterView.html index 5c697dcc9..0b6efa749 100644 --- a/docs/docs/NumericFilterView.html +++ b/docs/docs/NumericFilterView.html @@ -120,7 +120,7 @@

Source:
@@ -254,7 +254,7 @@

Type:
Source:
@@ -347,7 +347,7 @@
Type:
Source:
@@ -438,7 +438,7 @@
Type:
Source:
@@ -525,7 +525,7 @@
Type:
Source:
@@ -611,7 +611,7 @@
Type:
Source:
@@ -703,7 +703,7 @@
Type:
Source:
@@ -786,7 +786,7 @@
Type:
Source:
@@ -873,7 +873,7 @@
Type:
Source:
@@ -952,7 +952,7 @@
Type:
Source:
@@ -1036,7 +1036,7 @@
Type:
Source:
@@ -1122,7 +1122,7 @@
Type:
Source:
@@ -1211,7 +1211,7 @@
Type:
Source:
@@ -1302,7 +1302,7 @@

Source:
@@ -1468,7 +1468,7 @@

Parameters:
Source:
@@ -1612,7 +1612,7 @@
Parameters:
Source:
@@ -1785,7 +1785,7 @@
Parameters:
Source:
@@ -1945,7 +1945,7 @@
Parameters:
Source:
@@ -2095,7 +2095,7 @@
Parameters:
Source:
@@ -2189,7 +2189,7 @@

Source:
@@ -2385,7 +2385,7 @@

Parameters:
Source:
@@ -2534,7 +2534,7 @@
Parameters:
Source:
@@ -2700,7 +2700,7 @@
Parameters:
Source:
@@ -2802,7 +2802,7 @@

Source:
@@ -2946,7 +2946,7 @@

Parameters:
Source:
@@ -3098,7 +3098,7 @@
Parameters:
Source:
diff --git a/docs/docs/ObjectFormat.html b/docs/docs/ObjectFormat.html index 4d8ce5e3a..f0c685f7c 100644 --- a/docs/docs/ObjectFormat.html +++ b/docs/docs/ObjectFormat.html @@ -121,7 +121,7 @@

Source:
diff --git a/docs/docs/ObjectFormatSelect.html b/docs/docs/ObjectFormatSelect.html index b04d8c4b0..acd4c8b20 100644 --- a/docs/docs/ObjectFormatSelect.html +++ b/docs/docs/ObjectFormatSelect.html @@ -134,7 +134,7 @@

Source:
diff --git a/docs/docs/ObjectFormats.html b/docs/docs/ObjectFormats.html index 167e94845..455463d08 100644 --- a/docs/docs/ObjectFormats.html +++ b/docs/docs/ObjectFormats.html @@ -125,7 +125,7 @@

Source:
@@ -344,7 +344,7 @@

Source:
@@ -439,7 +439,7 @@

Source:
diff --git a/docs/docs/PortEditorDataView.html b/docs/docs/PortEditorDataView.html index 819c8686f..9a9bf7a51 100644 --- a/docs/docs/PortEditorDataView.html +++ b/docs/docs/PortEditorDataView.html @@ -116,7 +116,7 @@

Source:
@@ -242,7 +242,7 @@

Type:
Source:
@@ -320,7 +320,7 @@
Type:
Source:
@@ -402,7 +402,7 @@
Type:
Source:
@@ -485,7 +485,7 @@
Type:
Source:
@@ -568,7 +568,7 @@
Type:
Source:
@@ -636,7 +636,7 @@

Source:
@@ -719,7 +719,7 @@

Type:
Source:
@@ -797,7 +797,7 @@
Type:
Source:
@@ -880,7 +880,7 @@
Type:
Source:
@@ -963,7 +963,7 @@
Type:
Source:
@@ -1046,7 +1046,7 @@
Type:
Source:
@@ -1119,7 +1119,7 @@

Source:
@@ -1202,7 +1202,7 @@

Type:
Source:
@@ -1285,7 +1285,7 @@
Type:
Source:
@@ -1425,7 +1425,7 @@
Parameters:
Source:
@@ -1573,7 +1573,7 @@
Parameters:
Source:
@@ -1721,7 +1721,7 @@
Parameters:
Source:
@@ -1869,7 +1869,7 @@
Parameters:
Source:
@@ -2013,7 +2013,7 @@
Parameters:
Source:
@@ -2112,7 +2112,7 @@

Source:
@@ -2260,7 +2260,7 @@

Parameters:
Source:
diff --git a/docs/docs/PortEditorImageView.html b/docs/docs/PortEditorImageView.html index 8ef263ec0..168764f7b 100644 --- a/docs/docs/PortEditorImageView.html +++ b/docs/docs/PortEditorImageView.html @@ -241,7 +241,7 @@
Type:
Source:
@@ -319,7 +319,7 @@
Type:
Source:
@@ -397,7 +397,7 @@
Type:
Source:
@@ -476,7 +476,7 @@
Type:
Source:
@@ -556,7 +556,7 @@
Type:
Source:
@@ -634,7 +634,7 @@
Type:
Source:
@@ -712,7 +712,7 @@
Type:
Source:
@@ -794,7 +794,7 @@
Type:
Source:
@@ -875,7 +875,7 @@
Type:
Source:
@@ -956,7 +956,7 @@
Type:
Source:
@@ -1036,7 +1036,7 @@
Type:
Source:
@@ -1116,7 +1116,7 @@
Type:
Source:
@@ -1194,7 +1194,7 @@
Type:
Source:
@@ -1276,7 +1276,7 @@
Type:
Source:
@@ -1354,7 +1354,7 @@
Type:
Source:
@@ -1432,7 +1432,7 @@
Type:
Source:
@@ -1510,7 +1510,7 @@
Type:
Source:
@@ -1578,7 +1578,7 @@

Source:
@@ -1656,7 +1656,7 @@

Type:
Source:
@@ -1734,7 +1734,7 @@
Type:
Source:
@@ -1816,7 +1816,7 @@
Type:
Source:
@@ -1902,7 +1902,7 @@

Source:
@@ -2396,7 +2396,7 @@

Properties:
Source:
@@ -2490,7 +2490,7 @@

Source:
@@ -2587,7 +2587,7 @@

Source:
@@ -2682,7 +2682,7 @@

Source:
@@ -2870,7 +2870,7 @@

Source:
@@ -2964,7 +2964,7 @@

Source:
@@ -3058,7 +3058,7 @@

Source:
diff --git a/docs/docs/PortEditorLogosView.html b/docs/docs/PortEditorLogosView.html index 3948d31ab..ef8faf9b5 100644 --- a/docs/docs/PortEditorLogosView.html +++ b/docs/docs/PortEditorLogosView.html @@ -237,7 +237,7 @@

Type:
Source:
@@ -315,7 +315,7 @@
Type:
Source:
@@ -393,7 +393,7 @@
Type:
Source:
@@ -471,7 +471,7 @@
Type:
Source:
@@ -549,7 +549,7 @@
Type:
Source:
@@ -627,7 +627,7 @@
Type:
Source:
@@ -766,7 +766,7 @@
Parameters:
Source:
@@ -862,7 +862,7 @@

Source:
@@ -1080,7 +1080,7 @@

Properties:
Source:
@@ -1174,7 +1174,7 @@

Source:
@@ -1317,7 +1317,7 @@

Parameters:
Source:
@@ -1411,7 +1411,7 @@

Source:
diff --git a/docs/docs/PortEditorMdSectionView.html b/docs/docs/PortEditorMdSectionView.html index fc34615f7..7a63904eb 100644 --- a/docs/docs/PortEditorMdSectionView.html +++ b/docs/docs/PortEditorMdSectionView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -324,7 +324,7 @@
Type:
Source:
@@ -407,7 +407,7 @@
Type:
Source:
@@ -490,7 +490,7 @@
Type:
Source:
@@ -568,7 +568,7 @@
Type:
Source:
@@ -647,7 +647,7 @@
Type:
Source:
@@ -725,7 +725,7 @@
Type:
Source:
@@ -808,7 +808,7 @@
Type:
Source:
@@ -886,7 +886,7 @@
Type:
Source:
@@ -969,7 +969,7 @@
Type:
Source:
@@ -1052,7 +1052,7 @@
Type:
Source:
@@ -1135,7 +1135,7 @@
Type:
Source:
@@ -1218,7 +1218,7 @@
Type:
Source:
@@ -1297,7 +1297,7 @@
Type:
Source:
@@ -1380,7 +1380,7 @@
Type:
Source:
@@ -1464,7 +1464,7 @@
Type:
Source:
@@ -1604,7 +1604,7 @@
Parameters:
Source:
@@ -1752,7 +1752,7 @@
Parameters:
Source:
@@ -1900,7 +1900,7 @@
Parameters:
Source:
@@ -2048,7 +2048,7 @@
Parameters:
Source:
@@ -2147,7 +2147,7 @@

Source:
@@ -2291,7 +2291,7 @@

Parameters:
Source:
@@ -2385,7 +2385,7 @@

Source:
@@ -2533,7 +2533,7 @@

Parameters:
Source:
diff --git a/docs/docs/PortEditorSectionView.html b/docs/docs/PortEditorSectionView.html index b806fced0..555f14f01 100644 --- a/docs/docs/PortEditorSectionView.html +++ b/docs/docs/PortEditorSectionView.html @@ -121,7 +121,7 @@

Source:
@@ -242,7 +242,7 @@

Type:
Source:
@@ -320,7 +320,7 @@
Type:
Source:
@@ -398,7 +398,7 @@
Type:
Source:
@@ -476,7 +476,7 @@
Type:
Source:
@@ -554,7 +554,7 @@
Type:
Source:
@@ -632,7 +632,7 @@
Type:
Source:
@@ -710,7 +710,7 @@
Type:
Source:
@@ -778,7 +778,7 @@

Source:
@@ -856,7 +856,7 @@

Type:
Source:
@@ -935,7 +935,7 @@
Type:
Source:
@@ -1070,7 +1070,7 @@
Parameters:
Source:
@@ -1213,7 +1213,7 @@
Parameters:
Source:
@@ -1356,7 +1356,7 @@
Parameters:
Source:
@@ -1551,7 +1551,7 @@
Properties:
Source:
@@ -1645,7 +1645,7 @@

Source:
@@ -1788,7 +1788,7 @@

Parameters:
Source:
diff --git a/docs/docs/PortEditorSectionsView.html b/docs/docs/PortEditorSectionsView.html index 13b7afb1d..e422ca5b6 100644 --- a/docs/docs/PortEditorSectionsView.html +++ b/docs/docs/PortEditorSectionsView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -319,7 +319,7 @@
Type:
Source:
@@ -397,7 +397,7 @@
Type:
Source:
@@ -475,7 +475,7 @@
Type:
Source:
@@ -553,7 +553,7 @@
Type:
Source:
@@ -631,7 +631,7 @@
Type:
Source:
@@ -709,7 +709,7 @@
Type:
Source:
@@ -787,7 +787,7 @@
Type:
Source:
@@ -861,7 +861,7 @@
Type:
Source:
@@ -939,7 +939,7 @@
Type:
Source:
@@ -1003,7 +1003,7 @@

Source:
@@ -1081,7 +1081,7 @@

Type:
Source:
@@ -1159,7 +1159,7 @@
Type:
Source:
@@ -1237,7 +1237,7 @@
Type:
Source:
@@ -1311,7 +1311,7 @@
Type:
Source:
@@ -1389,7 +1389,7 @@
Type:
Source:
@@ -1467,7 +1467,7 @@
Type:
Source:
@@ -1545,7 +1545,7 @@
Type:
Source:
@@ -1624,7 +1624,7 @@
Type:
Source:
@@ -1702,7 +1702,7 @@
Type:
Source:
@@ -1780,7 +1780,7 @@
Type:
Source:
@@ -1854,7 +1854,7 @@
Type:
Source:
@@ -1932,7 +1932,7 @@
Type:
Source:
@@ -2067,7 +2067,7 @@
Parameters:
Source:
@@ -2256,7 +2256,7 @@
Parameters:
Source:
@@ -2350,7 +2350,7 @@

Source:
@@ -2516,7 +2516,7 @@

Parameters:
Source:
@@ -2677,7 +2677,7 @@
Parameters:
Source:
@@ -2845,7 +2845,7 @@
Parameters:
Source:
@@ -3014,7 +3014,7 @@
Parameters:
Source:
@@ -3179,7 +3179,7 @@
Parameters:
Source:
@@ -3325,7 +3325,7 @@
Parameters:
Source:
@@ -3419,7 +3419,7 @@

Source:
@@ -3706,7 +3706,7 @@

Parameters:
Source:
@@ -3849,7 +3849,7 @@
Parameters:
Source:
@@ -4004,7 +4004,7 @@
Parameters:
Source:
@@ -4098,7 +4098,7 @@

Source:
@@ -4192,7 +4192,7 @@

Source:
@@ -4358,7 +4358,7 @@

Parameters:
Source:
@@ -4452,7 +4452,7 @@

Source:
@@ -4546,7 +4546,7 @@

Source:
@@ -4640,7 +4640,7 @@

Source:
@@ -4734,7 +4734,7 @@

Source:
@@ -4889,7 +4889,7 @@

Parameters:
Source:
@@ -5032,7 +5032,7 @@
Parameters:
Source:
@@ -5189,7 +5189,7 @@
Parameters:
Source:
@@ -5284,7 +5284,7 @@

Source:
@@ -5519,7 +5519,7 @@

Parameters:
Source:
@@ -5615,7 +5615,7 @@

Source:
@@ -5866,7 +5866,7 @@

Source:
@@ -6032,7 +6032,7 @@

Parameters:
Source:
@@ -6205,7 +6205,7 @@
Parameters:
Source:
@@ -6315,7 +6315,7 @@
Type:
Source:
@@ -6393,7 +6393,7 @@
Type:
Source:
@@ -6471,7 +6471,7 @@
Type:
Source:
@@ -6549,7 +6549,7 @@
Type:
Source:
@@ -6627,7 +6627,7 @@
Type:
Source:
@@ -6705,7 +6705,7 @@
Type:
Source:
@@ -6783,7 +6783,7 @@
Type:
Source:
@@ -6861,7 +6861,7 @@
Type:
Source:
@@ -6935,7 +6935,7 @@
Type:
Source:
@@ -7013,7 +7013,7 @@
Type:
Source:
@@ -7077,7 +7077,7 @@

Source:
@@ -7155,7 +7155,7 @@

Type:
Source:
@@ -7233,7 +7233,7 @@
Type:
Source:
@@ -7311,7 +7311,7 @@
Type:
Source:
@@ -7385,7 +7385,7 @@
Type:
Source:
@@ -7463,7 +7463,7 @@
Type:
Source:
@@ -7541,7 +7541,7 @@
Type:
Source:
@@ -7619,7 +7619,7 @@
Type:
Source:
@@ -7698,7 +7698,7 @@
Type:
Source:
@@ -7776,7 +7776,7 @@
Type:
Source:
@@ -7854,7 +7854,7 @@
Type:
Source:
@@ -7928,7 +7928,7 @@
Type:
Source:
@@ -8006,7 +8006,7 @@
Type:
Source:
@@ -8141,7 +8141,7 @@
Parameters:
Source:
@@ -8330,7 +8330,7 @@
Parameters:
Source:
@@ -8424,7 +8424,7 @@

Source:
@@ -8590,7 +8590,7 @@

Parameters:
Source:
@@ -8751,7 +8751,7 @@
Parameters:
Source:
@@ -8919,7 +8919,7 @@
Parameters:
Source:
@@ -9088,7 +9088,7 @@
Parameters:
Source:
@@ -9253,7 +9253,7 @@
Parameters:
Source:
@@ -9399,7 +9399,7 @@
Parameters:
Source:
@@ -9493,7 +9493,7 @@

Source:
@@ -9780,7 +9780,7 @@

Parameters:
Source:
@@ -9923,7 +9923,7 @@
Parameters:
Source:
@@ -10078,7 +10078,7 @@
Parameters:
Source:
@@ -10172,7 +10172,7 @@

Source:
@@ -10266,7 +10266,7 @@

Source:
@@ -10432,7 +10432,7 @@

Parameters:
Source:
@@ -10526,7 +10526,7 @@

Source:
@@ -10620,7 +10620,7 @@

Source:
@@ -10714,7 +10714,7 @@

Source:
@@ -10808,7 +10808,7 @@

Source:
@@ -10963,7 +10963,7 @@

Parameters:
Source:
@@ -11106,7 +11106,7 @@
Parameters:
Source:
@@ -11263,7 +11263,7 @@
Parameters:
Source:
@@ -11358,7 +11358,7 @@

Source:
@@ -11593,7 +11593,7 @@

Parameters:
Source:
@@ -11689,7 +11689,7 @@

Source:
@@ -11940,7 +11940,7 @@

Source:
@@ -12106,7 +12106,7 @@

Parameters:
Source:
diff --git a/docs/docs/PortEditorSettingsView.html b/docs/docs/PortEditorSettingsView.html index 876fecab1..5bb8e8602 100644 --- a/docs/docs/PortEditorSettingsView.html +++ b/docs/docs/PortEditorSettingsView.html @@ -116,7 +116,7 @@

Source:
@@ -242,7 +242,7 @@

Type:
Source:
@@ -325,7 +325,7 @@
Type:
Source:
@@ -408,7 +408,7 @@
Type:
Source:
@@ -476,7 +476,7 @@

Source:
@@ -559,7 +559,7 @@

Type:
Source:
@@ -637,7 +637,7 @@
Type:
Source:
@@ -720,7 +720,7 @@
Type:
Source:
@@ -803,7 +803,7 @@
Type:
Source:
@@ -886,7 +886,7 @@
Type:
Source:
@@ -959,7 +959,7 @@

Source:
@@ -1042,7 +1042,7 @@

Type:
Source:
@@ -1125,7 +1125,7 @@
Type:
Source:
@@ -1265,7 +1265,7 @@
Parameters:
Source:
@@ -1359,7 +1359,7 @@

Source:
@@ -1453,7 +1453,7 @@

Source:
@@ -1601,7 +1601,7 @@

Parameters:
Source:
@@ -1749,7 +1749,7 @@
Parameters:
Source:
@@ -1897,7 +1897,7 @@
Parameters:
Source:
@@ -1991,7 +1991,7 @@

Source:
@@ -2134,7 +2134,7 @@

Parameters:
Source:
@@ -2233,7 +2233,7 @@

Source:
@@ -2378,7 +2378,7 @@

Parameters:
Source:
@@ -2521,7 +2521,7 @@
Parameters:
Source:
@@ -2669,7 +2669,7 @@
Parameters:
Source:
diff --git a/docs/docs/PortalDataView.html b/docs/docs/PortalDataView.html index 59e0c5748..7abc568a5 100644 --- a/docs/docs/PortalDataView.html +++ b/docs/docs/PortalDataView.html @@ -121,7 +121,7 @@

Source:
@@ -247,7 +247,7 @@

Type:
Source:
@@ -330,7 +330,7 @@
Type:
Source:
@@ -413,7 +413,7 @@
Type:
Source:
@@ -496,7 +496,7 @@
Type:
Source:
@@ -574,7 +574,7 @@
Type:
Source:
@@ -657,7 +657,7 @@
Type:
Source:
@@ -736,7 +736,7 @@
Type:
Source:
@@ -819,7 +819,7 @@
Type:
Source:
@@ -902,7 +902,7 @@
Type:
Source:
@@ -1106,7 +1106,7 @@
Properties:
Source:
@@ -1347,7 +1347,7 @@
Properties:
Source:
@@ -1447,7 +1447,7 @@

Source:
@@ -1547,7 +1547,7 @@

Source:
@@ -1646,7 +1646,7 @@

Source:
diff --git a/docs/docs/PortalEditorView.html b/docs/docs/PortalEditorView.html index ba415b8f6..cec8a0a48 100644 --- a/docs/docs/PortalEditorView.html +++ b/docs/docs/PortalEditorView.html @@ -120,7 +120,7 @@

Source:
@@ -248,7 +248,7 @@

Type:
Source:
@@ -331,7 +331,7 @@
Type:
Source:
@@ -417,7 +417,7 @@
Type:
Source:
@@ -495,7 +495,7 @@
Type:
Source:
@@ -568,7 +568,7 @@

Source:
@@ -654,7 +654,7 @@

Type:
Source:
@@ -738,7 +738,7 @@
Type:
Source:
@@ -816,7 +816,7 @@
Type:
Source:
@@ -894,7 +894,7 @@
Type:
Source:
@@ -974,7 +974,7 @@
Type:
Source:
@@ -1053,7 +1053,7 @@
Type:
Source:
@@ -1131,7 +1131,7 @@
Type:
Source:
@@ -1209,7 +1209,7 @@
Type:
Source:
@@ -1287,7 +1287,7 @@
Type:
Source:
@@ -1370,7 +1370,7 @@
Type:
Source:
@@ -1448,7 +1448,7 @@
Type:
Source:
@@ -1516,7 +1516,7 @@

Source:
@@ -1594,7 +1594,7 @@

Type:
Source:
@@ -1672,7 +1672,7 @@
Type:
Source:
@@ -1763,7 +1763,7 @@

Source:
@@ -1863,7 +1863,7 @@

Source:
@@ -1984,7 +1984,7 @@

Source:
@@ -2083,7 +2083,7 @@

Source:
@@ -2177,7 +2177,7 @@

Source:
@@ -2340,7 +2340,7 @@

Parameters:
Source:
@@ -2442,7 +2442,7 @@

Source:
@@ -2541,7 +2541,7 @@

Source:
@@ -2721,7 +2721,7 @@

Parameters:
Source:
@@ -2845,7 +2845,7 @@

Source:
@@ -2962,7 +2962,7 @@

Source:
@@ -3056,7 +3056,7 @@

Source:
@@ -3156,7 +3156,7 @@

Source:
@@ -3273,7 +3273,7 @@

Source:
@@ -3372,7 +3372,7 @@

Source:
@@ -3471,7 +3471,7 @@

Source:
@@ -3614,7 +3614,7 @@

Parameters:
Source:
@@ -3717,7 +3717,7 @@

Source:
@@ -3834,7 +3834,7 @@

Source:
@@ -3933,7 +3933,7 @@

Source:
@@ -4032,7 +4032,7 @@

Source:
@@ -4181,7 +4181,7 @@

Parameters:
Source:
@@ -4280,7 +4280,7 @@

Source:
@@ -4379,7 +4379,7 @@

Source:
@@ -4475,7 +4475,7 @@

Source:
@@ -4569,7 +4569,7 @@

Source:
@@ -4718,7 +4718,7 @@

Parameters:
Source:
@@ -4815,7 +4815,7 @@

Source:
@@ -4914,7 +4914,7 @@

Source:
@@ -5062,7 +5062,7 @@

Parameters:
Source:
@@ -5210,7 +5210,7 @@
Parameters:
Source:
@@ -5311,7 +5311,7 @@

Source:
@@ -5486,7 +5486,7 @@

Parameters:
Source:
@@ -5585,7 +5585,7 @@

Source:
@@ -5762,7 +5762,7 @@

Parameters:
Source:
@@ -5861,7 +5861,7 @@

Source:
@@ -5960,7 +5960,7 @@

Source:
@@ -6059,7 +6059,7 @@

Source:
@@ -6158,7 +6158,7 @@

Source:
@@ -6324,7 +6324,7 @@

Parameters:
Source:
@@ -6419,7 +6419,7 @@
Parameters:
Source:
diff --git a/docs/docs/PortalHeaderView.html b/docs/docs/PortalHeaderView.html index 8af2cfa3b..31b6d2aea 100644 --- a/docs/docs/PortalHeaderView.html +++ b/docs/docs/PortalHeaderView.html @@ -121,7 +121,7 @@

Source:
diff --git a/docs/docs/PortalImage.html b/docs/docs/PortalImage.html index 4086b4168..bf34f7b8a 100644 --- a/docs/docs/PortalImage.html +++ b/docs/docs/PortalImage.html @@ -120,7 +120,7 @@

Source:
@@ -227,7 +227,7 @@

Source:
@@ -313,7 +313,7 @@

Source:
@@ -479,7 +479,7 @@

Parameters:
Source:
@@ -595,7 +595,7 @@

Source:
@@ -712,7 +712,7 @@

Source:
@@ -873,7 +873,7 @@

Parameters:
Source:
@@ -989,7 +989,7 @@

Source:
@@ -1107,7 +1107,7 @@

Source:
diff --git a/docs/docs/PortalListView.html b/docs/docs/PortalListView.html index 34221609f..f7cf97f87 100644 --- a/docs/docs/PortalListView.html +++ b/docs/docs/PortalListView.html @@ -331,7 +331,7 @@

Type:
Source:
@@ -410,7 +410,7 @@
Type:
Source:
@@ -488,7 +488,7 @@
Type:
Source:
@@ -815,7 +815,7 @@
Type:
Source:
@@ -893,7 +893,7 @@
Type:
Source:
@@ -961,7 +961,7 @@

Source:
@@ -1092,7 +1092,7 @@

Parameters:
Source:
@@ -1204,7 +1204,7 @@

Source:
@@ -1298,7 +1298,7 @@

Source:
@@ -1392,7 +1392,7 @@

Source:
@@ -1486,7 +1486,7 @@

Source:
@@ -1580,7 +1580,7 @@

Source:
@@ -1674,7 +1674,7 @@

Source:
diff --git a/docs/docs/PortalLogosView.html b/docs/docs/PortalLogosView.html index aea8e98c3..fc4a568b7 100644 --- a/docs/docs/PortalLogosView.html +++ b/docs/docs/PortalLogosView.html @@ -252,7 +252,7 @@

Type:
Source:
@@ -330,7 +330,7 @@
Type:
Source:
@@ -408,7 +408,7 @@
Type:
Source:
@@ -486,7 +486,7 @@
Type:
Source:
@@ -564,7 +564,7 @@
Type:
Source:
@@ -650,7 +650,7 @@

Source:
@@ -744,7 +744,7 @@

Source:
diff --git a/docs/docs/PortalMembersView.html b/docs/docs/PortalMembersView.html index 4c9d09022..ed32f2384 100644 --- a/docs/docs/PortalMembersView.html +++ b/docs/docs/PortalMembersView.html @@ -121,7 +121,7 @@

Source:
@@ -247,7 +247,7 @@

Type:
Source:
@@ -330,7 +330,7 @@
Type:
Source:
@@ -413,7 +413,7 @@
Type:
Source:
@@ -496,7 +496,7 @@
Type:
Source:
@@ -579,7 +579,7 @@
Type:
Source:
@@ -658,7 +658,7 @@
Type:
Source:
@@ -741,7 +741,7 @@
Type:
Source:
@@ -824,7 +824,7 @@
Type:
Source:
@@ -1028,7 +1028,7 @@
Properties:
Source:
@@ -1269,7 +1269,7 @@
Properties:
Source:
@@ -1369,7 +1369,7 @@

Source:
@@ -1469,7 +1469,7 @@

Source:
@@ -1568,7 +1568,7 @@

Source:
diff --git a/docs/docs/PortalModel.html b/docs/docs/PortalModel.html index a298109a5..8b1b68ff1 100644 --- a/docs/docs/PortalModel.html +++ b/docs/docs/PortalModel.html @@ -124,7 +124,7 @@

Source:
@@ -245,7 +245,7 @@

Type:
Source:
@@ -328,7 +328,7 @@
Type:
Source:
@@ -482,7 +482,7 @@
Parameters:
Source:
@@ -653,7 +653,7 @@
Parameters:
Source:
@@ -752,7 +752,7 @@

Source:
@@ -802,7 +802,7 @@

- Converts the number of bytes into a human readable format and + Converts the number of bytes into a human readable format and updates the `sizeStr` attribute
@@ -852,7 +852,7 @@

Source:
@@ -946,7 +946,7 @@

Source:
@@ -1095,7 +1095,7 @@

Parameters:
Source:
@@ -1255,7 +1255,7 @@
Parameters:
Source:
@@ -1559,7 +1559,7 @@
Properties:
Source:
@@ -1721,7 +1721,7 @@
Parameters:
Source:
@@ -1866,7 +1866,7 @@
Parameters:
Source:
@@ -2009,7 +2009,7 @@
Parameters:
Source:
@@ -2193,7 +2193,7 @@
Parameters:
Source:
@@ -2359,7 +2359,7 @@
Parameters:
Source:
@@ -2461,7 +2461,7 @@

Source:
@@ -2579,7 +2579,7 @@

Source:
@@ -2678,7 +2678,7 @@

Source:
@@ -2795,7 +2795,7 @@

Source:
@@ -2913,7 +2913,7 @@

Source:
@@ -2963,7 +2963,7 @@

- This method will download this object while + This method will download this object while sending the user's auth token in the request.
@@ -3013,7 +3013,7 @@

Source:
@@ -3277,7 +3277,7 @@

Properties:
Source:
@@ -3399,7 +3399,7 @@

Source:
@@ -3593,7 +3593,7 @@

Parameters:
Source:
@@ -3744,7 +3744,7 @@
Parameters:
Source:
@@ -3865,7 +3865,7 @@

Source:
@@ -3990,7 +3990,7 @@

Source:
@@ -4118,7 +4118,7 @@

Source:
@@ -4227,7 +4227,7 @@

Source:
@@ -4329,7 +4329,7 @@

Source:
@@ -4440,7 +4440,7 @@

Source:
@@ -4539,7 +4539,7 @@

Source:
@@ -4655,7 +4655,7 @@

Source:
@@ -4751,7 +4751,7 @@

Source:
@@ -4857,7 +4857,7 @@

Source:
@@ -5025,7 +5025,7 @@

Parameters:
Source:
@@ -5212,7 +5212,7 @@
Parameters:
Source:
@@ -5478,7 +5478,7 @@
Properties:
Source:
@@ -5577,7 +5577,7 @@

Source:
@@ -5738,7 +5738,7 @@

Parameters:
Source:
@@ -5904,7 +5904,7 @@
Parameters:
Source:
@@ -6052,7 +6052,7 @@
Parameters:
Source:
@@ -6176,7 +6176,7 @@

Source:
@@ -6346,7 +6346,7 @@

Parameters:
Source:
@@ -6463,7 +6463,7 @@

Source:
@@ -6585,7 +6585,7 @@

Source:
@@ -6702,7 +6702,7 @@

Source:
@@ -6826,7 +6826,7 @@

Source:
@@ -6996,7 +6996,7 @@

Parameters:
Source:
@@ -7115,7 +7115,7 @@

Source:
@@ -7343,7 +7343,7 @@

Parameters:
Source:
@@ -7492,7 +7492,7 @@
Parameters:
Source:
@@ -7662,7 +7662,7 @@
Parameters:
Source:
@@ -7873,7 +7873,7 @@
Parameters:
Source:
@@ -8092,7 +8092,7 @@
Parameters:
Source:
@@ -8284,7 +8284,7 @@
Parameters:
Source:
@@ -8428,7 +8428,7 @@
Parameters:
Source:
@@ -8577,7 +8577,7 @@
Parameters:
Source:
@@ -8728,7 +8728,7 @@
Parameters:
Source:
@@ -8827,7 +8827,7 @@

Source:
@@ -8926,7 +8926,7 @@

Source:
@@ -9025,7 +9025,7 @@

Source:
@@ -9168,7 +9168,7 @@

Parameters:
Source:
@@ -9317,7 +9317,7 @@
Parameters:
Source:
@@ -9434,7 +9434,7 @@

Source:
@@ -9555,7 +9555,7 @@

Source:
@@ -9672,7 +9672,7 @@

Source:
@@ -9772,7 +9772,7 @@

Source:
@@ -9925,7 +9925,7 @@

Parameters:
Source:
@@ -10022,7 +10022,7 @@

Source:
@@ -10126,7 +10126,7 @@

Source:
@@ -10297,7 +10297,7 @@

Parameters:
Source:
@@ -10467,7 +10467,7 @@
Parameters:
Source:
@@ -10647,7 +10647,7 @@
Parameters:
Source:
@@ -10741,7 +10741,7 @@

Source:
@@ -10889,7 +10889,7 @@

Parameters:
Source:
@@ -10988,7 +10988,7 @@

Source:
@@ -11168,7 +11168,7 @@

Parameters:
Source:
@@ -11267,7 +11267,7 @@

Source:
@@ -11417,7 +11417,7 @@

Parameters:
Source:
@@ -11538,7 +11538,7 @@

Source:
@@ -11755,7 +11755,7 @@

Parameters:
Source:
@@ -11928,7 +11928,7 @@
Parameters:
Source:
@@ -12146,7 +12146,7 @@
Properties:
Source:
diff --git a/docs/docs/PortalSectionModel.html b/docs/docs/PortalSectionModel.html index 672feae5f..2fd627ba3 100644 --- a/docs/docs/PortalSectionModel.html +++ b/docs/docs/PortalSectionModel.html @@ -120,7 +120,7 @@

Source:
@@ -318,7 +318,7 @@

Parameters:
Source:
@@ -656,7 +656,7 @@
Parameters:
Source:
@@ -772,7 +772,7 @@

Source:
@@ -890,7 +890,7 @@

Source:
diff --git a/docs/docs/PortalSectionView.html b/docs/docs/PortalSectionView.html index 1a64a105e..e57734d20 100644 --- a/docs/docs/PortalSectionView.html +++ b/docs/docs/PortalSectionView.html @@ -243,7 +243,7 @@

Type:
Source:
@@ -321,7 +321,7 @@
Type:
Source:
@@ -399,7 +399,7 @@
Type:
Source:
@@ -477,7 +477,7 @@
Type:
Source:
@@ -555,7 +555,7 @@
Type:
Source:
@@ -629,7 +629,7 @@
Type:
Source:
@@ -707,7 +707,7 @@
Type:
Source:
@@ -786,7 +786,7 @@
Type:
Source:
@@ -985,7 +985,7 @@
Properties:
Source:
@@ -1221,7 +1221,7 @@
Properties:
Source:
@@ -1316,7 +1316,7 @@

Source:
@@ -1411,7 +1411,7 @@

Source:
@@ -1505,7 +1505,7 @@

Source:
diff --git a/docs/docs/PortalUsagesView.html b/docs/docs/PortalUsagesView.html index 0cab9e487..b518db43b 100644 --- a/docs/docs/PortalUsagesView.html +++ b/docs/docs/PortalUsagesView.html @@ -123,7 +123,7 @@

Source:
@@ -334,7 +334,7 @@

Type:
Source:
@@ -418,7 +418,7 @@
Type:
Source:
@@ -501,7 +501,7 @@
Type:
Source:
@@ -848,7 +848,7 @@
Type:
Source:
@@ -931,7 +931,7 @@
Type:
Source:
@@ -1004,7 +1004,7 @@

Source:
@@ -1082,7 +1082,7 @@

Type:
Source:
@@ -1218,7 +1218,7 @@
Parameters:
Source:
@@ -1335,7 +1335,7 @@

Source:
@@ -1479,7 +1479,7 @@

Parameters:
Source:
@@ -1578,7 +1578,7 @@

Source:
@@ -1672,7 +1672,7 @@

Source:
@@ -1771,7 +1771,7 @@

Source:
@@ -1870,7 +1870,7 @@

Source:
@@ -1969,7 +1969,7 @@

Source:
@@ -2068,7 +2068,7 @@

Source:
@@ -2162,7 +2162,7 @@

Source:
diff --git a/docs/docs/PortalView.html b/docs/docs/PortalView.html index 62f373f5c..69bea49b5 100644 --- a/docs/docs/PortalView.html +++ b/docs/docs/PortalView.html @@ -121,7 +121,7 @@

Source:
@@ -242,7 +242,7 @@

Type:
Source:
@@ -320,7 +320,7 @@
Type:
Source:
@@ -398,7 +398,7 @@
Type:
Source:
@@ -479,7 +479,7 @@
Type:
Source:
@@ -557,7 +557,7 @@
Type:
Source:
@@ -635,7 +635,7 @@
Type:
Source:
@@ -713,7 +713,7 @@
Type:
Source:
@@ -791,7 +791,7 @@
Type:
Source:
@@ -869,7 +869,7 @@
Type:
Source:
@@ -947,7 +947,7 @@
Type:
Source:
@@ -1025,7 +1025,7 @@
Type:
Source:
@@ -1103,7 +1103,7 @@
Type:
Source:
@@ -1181,7 +1181,7 @@
Type:
Source:
@@ -1259,7 +1259,7 @@
Type:
Source:
@@ -1337,7 +1337,7 @@
Type:
Source:
@@ -1415,7 +1415,7 @@
Type:
Source:
@@ -1493,7 +1493,7 @@
Type:
Source:
@@ -1629,7 +1629,7 @@
Parameters:
Source:
@@ -1772,7 +1772,7 @@
Parameters:
Source:
@@ -1867,7 +1867,7 @@

Source:
@@ -2011,7 +2011,7 @@

Parameters:
Source:
@@ -2154,7 +2154,7 @@
Parameters:
Source:
@@ -2323,7 +2323,7 @@
Parameters:
Source:
@@ -2439,7 +2439,7 @@

Source:
@@ -2533,7 +2533,7 @@

Source:
@@ -2676,7 +2676,7 @@

Parameters:
Source:
@@ -2770,7 +2770,7 @@

Source:
@@ -2913,7 +2913,7 @@

Parameters:
Source:
@@ -3008,7 +3008,7 @@

Source:
@@ -3102,7 +3102,7 @@

Source:
@@ -3219,7 +3219,7 @@

Source:
@@ -3313,7 +3313,7 @@

Source:
@@ -3407,7 +3407,7 @@

Source:
@@ -3576,7 +3576,7 @@

Parameters:
Source:
@@ -3670,7 +3670,7 @@

Source:
@@ -3827,7 +3827,7 @@

Parameters:
Source:
@@ -3922,7 +3922,7 @@
Parameters:
Source:
@@ -4172,7 +4172,7 @@

Source:
diff --git a/docs/docs/PortalVisualizationsView.html b/docs/docs/PortalVisualizationsView.html index 46d2bb330..d9f2c8841 100644 --- a/docs/docs/PortalVisualizationsView.html +++ b/docs/docs/PortalVisualizationsView.html @@ -247,7 +247,7 @@

Type:
Source:
@@ -330,7 +330,7 @@
Type:
Source:
@@ -413,7 +413,7 @@
Type:
Source:
@@ -496,7 +496,7 @@
Type:
Source:
@@ -579,7 +579,7 @@
Type:
Source:
@@ -662,7 +662,7 @@
Type:
Source:
@@ -745,7 +745,7 @@
Type:
Source:
@@ -829,7 +829,7 @@
Type:
Source:
@@ -1033,7 +1033,7 @@
Properties:
Source:
@@ -1203,7 +1203,7 @@
Parameters:
Source:
@@ -1297,7 +1297,7 @@

Source:
@@ -1397,7 +1397,7 @@

Source:
@@ -1498,7 +1498,7 @@

Source:
@@ -1597,7 +1597,7 @@

Source:
@@ -1691,7 +1691,7 @@

Source:
@@ -1785,7 +1785,7 @@

Source:
diff --git a/docs/docs/PortalsSearchView.html b/docs/docs/PortalsSearchView.html index 88ff85db3..5e0eb8f61 100644 --- a/docs/docs/PortalsSearchView.html +++ b/docs/docs/PortalsSearchView.html @@ -244,7 +244,7 @@

Source:
@@ -330,7 +330,7 @@

Source:
@@ -424,7 +424,7 @@

Source:
diff --git a/docs/docs/Prediction.html b/docs/docs/Prediction.html index 190295f10..84f2bee88 100644 --- a/docs/docs/Prediction.html +++ b/docs/docs/Prediction.html @@ -278,7 +278,7 @@

Properties:
- Unique identifier that can be + Unique identifier that can be geocoded by the Google Maps Geocoder API. diff --git a/docs/docs/PredictionView.html b/docs/docs/PredictionView.html index 513c3ac0c..0c2db17e6 100644 --- a/docs/docs/PredictionView.html +++ b/docs/docs/PredictionView.html @@ -134,7 +134,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -333,7 +333,7 @@
Type:
Source:
@@ -411,7 +411,7 @@
Type:
Source:
@@ -489,7 +489,7 @@
Type:
Source:
@@ -557,7 +557,7 @@

Source:
@@ -635,7 +635,7 @@

Type:
Source:
@@ -723,7 +723,7 @@

Source:
@@ -773,7 +773,7 @@

- Event handler function that selects this element, deselecting any other + Event handler function that selects this element, deselecting any other sibling list elements.
@@ -818,7 +818,7 @@

Source:
@@ -912,7 +912,7 @@

Source:
diff --git a/docs/docs/PredictionsListView.html b/docs/docs/PredictionsListView.html index 346d29a6c..1627846f0 100644 --- a/docs/docs/PredictionsListView.html +++ b/docs/docs/PredictionsListView.html @@ -134,7 +134,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -333,7 +333,7 @@
Type:
Source:
@@ -411,7 +411,7 @@
Type:
Source:
@@ -497,7 +497,7 @@

Source:
@@ -593,7 +593,7 @@

Source:
@@ -687,7 +687,7 @@

Source:
diff --git a/docs/docs/Project.html b/docs/docs/Project.html index ca339fd43..7626a92b3 100644 --- a/docs/docs/Project.html +++ b/docs/docs/Project.html @@ -126,7 +126,7 @@

Source:
@@ -252,7 +252,7 @@

Source:
@@ -346,7 +346,7 @@

Source:
@@ -440,7 +440,7 @@

Source:
diff --git a/docs/docs/ProjectList.html b/docs/docs/ProjectList.html index 775ef4385..fbeb7db9c 100644 --- a/docs/docs/ProjectList.html +++ b/docs/docs/ProjectList.html @@ -126,7 +126,7 @@

Source:
@@ -251,7 +251,7 @@

Source:
@@ -345,7 +345,7 @@

Source:
@@ -439,7 +439,7 @@

Source:
@@ -533,7 +533,7 @@

Source:
diff --git a/docs/docs/ProjectView.html b/docs/docs/ProjectView.html index 45a5acefc..edb56d260 100644 --- a/docs/docs/ProjectView.html +++ b/docs/docs/ProjectView.html @@ -125,7 +125,7 @@

Source:
@@ -254,7 +254,7 @@

Source:
@@ -400,7 +400,7 @@

Parameters:
Source:
@@ -490,7 +490,7 @@

Source:
@@ -606,7 +606,7 @@

Source:
diff --git a/docs/docs/QualityCheck.html b/docs/docs/QualityCheck.html index 05ca432fe..5589bdc78 100644 --- a/docs/docs/QualityCheck.html +++ b/docs/docs/QualityCheck.html @@ -124,7 +124,7 @@

Source:
diff --git a/docs/docs/QualityReport.html b/docs/docs/QualityReport.html index 6f0ec96b9..65e2d494a 100644 --- a/docs/docs/QualityReport.html +++ b/docs/docs/QualityReport.html @@ -122,7 +122,7 @@

Source:
diff --git a/docs/docs/QueryBuilderView.html b/docs/docs/QueryBuilderView.html index 59fc4ef5e..730055f2a 100644 --- a/docs/docs/QueryBuilderView.html +++ b/docs/docs/QueryBuilderView.html @@ -134,7 +134,7 @@

Source:
@@ -260,7 +260,7 @@

Type:
Source:
@@ -343,7 +343,7 @@
Type:
Source:
@@ -421,7 +421,7 @@
Type:
Source:
@@ -502,7 +502,7 @@
Type:
Source:
@@ -582,7 +582,7 @@
Type:
Source:
@@ -666,7 +666,7 @@
Type:
Source:
@@ -751,7 +751,7 @@
Type:
Source:
@@ -832,7 +832,7 @@
Type:
Source:
@@ -917,7 +917,7 @@
Type:
Source:
@@ -1000,7 +1000,7 @@
Type:
Source:
@@ -1078,7 +1078,7 @@
Type:
Source:
@@ -1156,7 +1156,7 @@
Type:
Source:
@@ -1235,7 +1235,7 @@
Type:
Source:
@@ -1317,7 +1317,7 @@
Type:
Source:
@@ -1395,7 +1395,7 @@
Type:
Source:
@@ -1473,7 +1473,7 @@
Type:
Source:
@@ -1613,7 +1613,7 @@
Parameters:
Source:
@@ -1758,7 +1758,7 @@
Parameters:
Source:
@@ -1853,7 +1853,7 @@

Source:
@@ -2025,7 +2025,7 @@

Parameters:
Source:
@@ -2119,7 +2119,7 @@

Source:
@@ -2241,7 +2241,7 @@

Source:
diff --git a/docs/docs/QueryField.html b/docs/docs/QueryField.html index 8c5ea0609..fb407dc4a 100644 --- a/docs/docs/QueryField.html +++ b/docs/docs/QueryField.html @@ -127,7 +127,7 @@

Source:
@@ -253,7 +253,7 @@

Source:
@@ -372,7 +372,7 @@

Source:
@@ -491,7 +491,7 @@

Source:
@@ -608,7 +608,7 @@

Source:
@@ -721,7 +721,7 @@

Source:
@@ -840,7 +840,7 @@

Source:
@@ -961,7 +961,7 @@

Source:
@@ -1078,7 +1078,7 @@

Source:
@@ -1195,7 +1195,7 @@

Source:
@@ -1300,7 +1300,7 @@

Source:
@@ -1417,7 +1417,7 @@

Source:
@@ -1560,7 +1560,7 @@

Parameters:
Source:
@@ -1676,7 +1676,7 @@

Source:
diff --git a/docs/docs/QueryFieldSelectView.html b/docs/docs/QueryFieldSelectView.html index a9093c31b..75671c9ee 100644 --- a/docs/docs/QueryFieldSelectView.html +++ b/docs/docs/QueryFieldSelectView.html @@ -134,7 +134,7 @@

Source:
@@ -261,7 +261,7 @@

Type:
Source:
@@ -344,7 +344,7 @@
Type:
Source:
@@ -422,7 +422,7 @@
Type:
Source:
@@ -501,7 +501,7 @@
Type:
Source:
@@ -579,7 +579,7 @@
Type:
Source:
@@ -658,7 +658,7 @@
Type:
Source:
@@ -736,7 +736,7 @@
Type:
Source:
@@ -814,7 +814,7 @@
Type:
Source:
@@ -883,7 +883,7 @@
Type:
Source:
@@ -1129,7 +1129,7 @@
Parameters:
Source:
@@ -1296,7 +1296,7 @@
Parameters:
Source:
@@ -1462,7 +1462,7 @@
Parameters:
Source:
@@ -1605,7 +1605,7 @@
Parameters:
Source:
@@ -1725,7 +1725,7 @@

Source:
@@ -1823,7 +1823,7 @@

Source:
diff --git a/docs/docs/QueryFields.html b/docs/docs/QueryFields.html index aae3536d5..158fd1f1a 100644 --- a/docs/docs/QueryFields.html +++ b/docs/docs/QueryFields.html @@ -127,7 +127,7 @@

Source:
@@ -238,7 +238,7 @@

Source:
@@ -374,7 +374,7 @@

Parameters:
Source:
@@ -490,7 +490,7 @@

Source:
@@ -637,7 +637,7 @@

Parameters:
Source:
@@ -754,7 +754,7 @@

Source:
@@ -897,7 +897,7 @@

Parameters:
Source:
@@ -1013,7 +1013,7 @@

Source:
diff --git a/docs/docs/QueryRuleView.html b/docs/docs/QueryRuleView.html index 62c0aaac4..c600c7da8 100644 --- a/docs/docs/QueryRuleView.html +++ b/docs/docs/QueryRuleView.html @@ -134,7 +134,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -333,7 +333,7 @@
Type:
Source:
@@ -411,7 +411,7 @@
Type:
Source:
@@ -504,7 +504,7 @@
Type:
Source:
@@ -585,7 +585,7 @@
Type:
Source:
@@ -670,7 +670,7 @@
Type:
Source:
@@ -748,7 +748,7 @@
Type:
Source:
@@ -827,7 +827,7 @@
Type:
Source:
@@ -905,7 +905,7 @@
Type:
Source:
@@ -984,7 +984,7 @@
Type:
Source:
@@ -1067,7 +1067,7 @@
Type:
Source:
@@ -1145,7 +1145,7 @@
Type:
Source:
@@ -1230,7 +1230,7 @@
Type:
Source:
@@ -1308,7 +1308,7 @@
Type:
Source:
@@ -1390,7 +1390,7 @@
Type:
Source:
@@ -1468,7 +1468,7 @@
Type:
Source:
@@ -1555,7 +1555,7 @@

Source:
@@ -1700,7 +1700,7 @@

Parameters:
Source:
@@ -1794,7 +1794,7 @@

Source:
@@ -1888,7 +1888,7 @@

Source:
@@ -1982,7 +1982,7 @@

Source:
@@ -2158,7 +2158,7 @@

Parameters:
Source:
@@ -2377,7 +2377,7 @@
Parameters:
Source:
@@ -2516,7 +2516,7 @@

Source:
@@ -2683,7 +2683,7 @@

Parameters:
Source:
@@ -2865,7 +2865,7 @@
Parameters:
Source:
@@ -3073,7 +3073,7 @@
Parameters:
Source:
@@ -3190,7 +3190,7 @@

Source:
@@ -3378,7 +3378,7 @@

Parameters:
Source:
@@ -3550,7 +3550,7 @@
Parameters:
Source:
@@ -3695,7 +3695,7 @@
Parameters:
Source:
@@ -3839,7 +3839,7 @@
Parameters:
Source:
@@ -3982,7 +3982,7 @@
Parameters:
Source:
@@ -4077,7 +4077,7 @@

Source:
@@ -4221,7 +4221,7 @@

Parameters:
Source:
@@ -4316,7 +4316,7 @@

Source:
@@ -4410,7 +4410,7 @@

Source:
@@ -4529,7 +4529,7 @@

Source:
diff --git a/docs/docs/Quota.html b/docs/docs/Quota.html index 54e30eb1e..64121bb92 100644 --- a/docs/docs/Quota.html +++ b/docs/docs/Quota.html @@ -128,7 +128,7 @@

Source:
@@ -531,7 +531,7 @@

Properties:
Source:
@@ -609,7 +609,7 @@
Type:
Source:
diff --git a/docs/docs/Quotas.html b/docs/docs/Quotas.html index a00e6dd0a..68534ddaf 100644 --- a/docs/docs/Quotas.html +++ b/docs/docs/Quotas.html @@ -125,7 +125,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -326,7 +326,7 @@
Type:
Source:
@@ -549,7 +549,7 @@
Properties:
Source:
@@ -644,7 +644,7 @@

Source:
@@ -896,7 +896,7 @@

Properties:
Source:
diff --git a/docs/docs/RegisterCitationView.html b/docs/docs/RegisterCitationView.html index c6f3c2a02..6bf2d4f33 100644 --- a/docs/docs/RegisterCitationView.html +++ b/docs/docs/RegisterCitationView.html @@ -131,7 +131,7 @@

Source:
@@ -252,7 +252,7 @@

Type:
Source:
@@ -330,7 +330,7 @@
Type:
Source:
@@ -408,7 +408,7 @@
Type:
Source:
@@ -494,7 +494,7 @@

Source:
@@ -588,7 +588,7 @@

Source:
@@ -682,7 +682,7 @@

Source:
@@ -776,7 +776,7 @@

Source:
@@ -873,7 +873,7 @@

Source:
diff --git a/docs/docs/ScaleBarView.html b/docs/docs/ScaleBarView.html index 9aeaba180..959cb36cf 100644 --- a/docs/docs/ScaleBarView.html +++ b/docs/docs/ScaleBarView.html @@ -135,7 +135,7 @@

Source:
@@ -256,7 +256,7 @@

Type:
Source:
@@ -483,7 +483,7 @@
Properties:
Source:
@@ -563,7 +563,7 @@
Type:
Source:
@@ -641,7 +641,7 @@
Type:
Source:
@@ -719,7 +719,7 @@
Type:
Source:
@@ -800,7 +800,7 @@
Type:
Source:
@@ -881,7 +881,7 @@
Type:
Source:
@@ -959,7 +959,7 @@
Type:
Source:
@@ -1037,7 +1037,7 @@
Type:
Source:
@@ -1184,7 +1184,7 @@
Parameters:
Source:
@@ -1281,7 +1281,7 @@

Source:
@@ -1378,7 +1378,7 @@

Source:
@@ -1475,7 +1475,7 @@

Source:
@@ -1725,7 +1725,7 @@

Properties:
Source:
@@ -1842,7 +1842,7 @@

Source:
@@ -1958,7 +1958,7 @@

Source:
@@ -2055,7 +2055,7 @@

Source:
@@ -2248,7 +2248,7 @@

Parameters:
Source:
@@ -2418,7 +2418,7 @@
Parameters:
Source:
diff --git a/docs/docs/ScienceMetadata.html b/docs/docs/ScienceMetadata.html index 9e9004c2c..ceff95369 100644 --- a/docs/docs/ScienceMetadata.html +++ b/docs/docs/ScienceMetadata.html @@ -123,7 +123,7 @@

Source:
@@ -253,7 +253,7 @@

Source:
@@ -303,7 +303,7 @@

- Converts the number of bytes into a human readable format and + Converts the number of bytes into a human readable format and updates the `sizeStr` attribute
@@ -353,7 +353,7 @@

Source:
@@ -513,7 +513,7 @@

Parameters:
Source:
@@ -817,7 +817,7 @@
Properties:
Source:
@@ -997,7 +997,7 @@
Parameters:
Source:
@@ -1120,7 +1120,7 @@

Source:
@@ -1219,7 +1219,7 @@

Source:
@@ -1287,7 +1287,7 @@

- This method will download this object while + This method will download this object while sending the user's auth token in the request.
@@ -1337,7 +1337,7 @@

Source:
@@ -1446,7 +1446,7 @@

Source:
@@ -1640,7 +1640,7 @@

Parameters:
Source:
@@ -1791,7 +1791,7 @@
Parameters:
Source:
@@ -1912,7 +1912,7 @@

Source:
@@ -2037,7 +2037,7 @@

Source:
@@ -2165,7 +2165,7 @@

Source:
@@ -2274,7 +2274,7 @@

Source:
@@ -2376,7 +2376,7 @@

Source:
@@ -2485,7 +2485,7 @@

Source:
@@ -2645,7 +2645,7 @@

Parameters:
Source:
@@ -2911,7 +2911,7 @@
Properties:
Source:
@@ -3010,7 +3010,7 @@

Source:
@@ -3176,7 +3176,7 @@

Parameters:
Source:
@@ -3300,7 +3300,7 @@

Source:
@@ -3470,7 +3470,7 @@

Parameters:
Source:
@@ -3587,7 +3587,7 @@

Source:
@@ -3709,7 +3709,7 @@

Source:
@@ -3826,7 +3826,7 @@

Source:
@@ -3950,7 +3950,7 @@

Source:
@@ -4120,7 +4120,7 @@

Parameters:
Source:
@@ -4239,7 +4239,7 @@

Source:
@@ -4467,7 +4467,7 @@

Parameters:
Source:
@@ -4567,7 +4567,7 @@

Source:
@@ -4718,7 +4718,7 @@

Parameters:
Source:
@@ -4817,7 +4817,7 @@

Source:
@@ -4916,7 +4916,7 @@

Source:
@@ -5015,7 +5015,7 @@

Source:
@@ -5132,7 +5132,7 @@

Source:
@@ -5232,7 +5232,7 @@

Source:
@@ -5385,7 +5385,7 @@

Parameters:
Source:
@@ -5484,7 +5484,7 @@

Source:
@@ -5655,7 +5655,7 @@

Parameters:
Source:
@@ -5835,7 +5835,7 @@
Parameters:
Source:
@@ -5983,7 +5983,7 @@
Parameters:
Source:
@@ -6082,7 +6082,7 @@

Source:
@@ -6181,7 +6181,7 @@

Source:
@@ -6280,7 +6280,7 @@

Source:
diff --git a/docs/docs/ScienceMetadataView.html b/docs/docs/ScienceMetadataView.html index 60abea92e..821d481c3 100644 --- a/docs/docs/ScienceMetadataView.html +++ b/docs/docs/ScienceMetadataView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -327,7 +327,7 @@

Source:
@@ -421,7 +421,7 @@

Source:
diff --git a/docs/docs/Search.html b/docs/docs/Search.html index 4c576b43f..59e02d75c 100644 --- a/docs/docs/Search.html +++ b/docs/docs/Search.html @@ -125,7 +125,7 @@

Parameters:
Source:
@@ -440,7 +440,7 @@
Properties:
Source:
@@ -660,7 +660,7 @@
Properties:
Source:
diff --git a/docs/docs/SearchInputView.html b/docs/docs/SearchInputView.html index 9363b5550..896317726 100644 --- a/docs/docs/SearchInputView.html +++ b/docs/docs/SearchInputView.html @@ -124,7 +124,7 @@

Source:
@@ -245,7 +245,7 @@

Type:
Source:
@@ -313,7 +313,7 @@

Source:
@@ -391,7 +391,7 @@

Type:
Source:
@@ -477,7 +477,7 @@

Source:
@@ -571,7 +571,7 @@

Source:
@@ -665,7 +665,7 @@

Source:
@@ -759,7 +759,7 @@

Source:
@@ -853,7 +853,7 @@

Source:
@@ -958,7 +958,7 @@

Source:
@@ -1063,7 +1063,7 @@

Source:
@@ -1168,7 +1168,7 @@

Source:
@@ -1273,7 +1273,7 @@

Source:
@@ -1378,7 +1378,7 @@

Source:
@@ -1484,7 +1484,7 @@

Source:
@@ -1534,7 +1534,7 @@

- Event handler for Backbone.View configuration that is called whenever + Event handler for Backbone.View configuration that is called whenever the user blurs the input.
@@ -1579,7 +1579,7 @@

Source:
@@ -1673,7 +1673,7 @@

Source:
@@ -1723,7 +1723,7 @@

- Event handler for Backbone.View configuration that is called whenever + Event handler for Backbone.View configuration that is called whenever the user focuses the input.
@@ -1768,7 +1768,7 @@

Source:
@@ -1818,7 +1818,7 @@

- Event handler for Backbone.View configuration that is called whenever + Event handler for Backbone.View configuration that is called whenever the user types a key.
@@ -1863,7 +1863,7 @@

Source:
@@ -1913,7 +1913,7 @@

- Event handler for Backbone.View configuration that is called whenever + Event handler for Backbone.View configuration that is called whenever the user types a key.
@@ -1958,7 +1958,7 @@

Source:
@@ -2008,7 +2008,7 @@

- Event handler for Backbone.View configuration that is called whenever + Event handler for Backbone.View configuration that is called whenever the user clicks the search button or hits the Enter key.
@@ -2053,7 +2053,7 @@

Source:
@@ -2149,7 +2149,7 @@

Source:
@@ -2292,7 +2292,7 @@

Parameters:
Source:
@@ -2386,7 +2386,7 @@

Source:
@@ -2481,7 +2481,7 @@

Source:
diff --git a/docs/docs/SearchResultView.html b/docs/docs/SearchResultView.html index 1da5514ae..ee268efca 100644 --- a/docs/docs/SearchResultView.html +++ b/docs/docs/SearchResultView.html @@ -130,7 +130,7 @@

Source:
@@ -251,7 +251,7 @@

Type:
Source:
@@ -330,7 +330,7 @@
Type:
Source:
@@ -408,7 +408,7 @@
Type:
Source:
@@ -486,7 +486,7 @@
Type:
Source:
@@ -564,7 +564,7 @@
Type:
Source:
@@ -642,7 +642,7 @@
Type:
Source:
@@ -729,7 +729,7 @@

Source:
@@ -872,7 +872,7 @@

Parameters:
Source:
@@ -970,7 +970,7 @@

Source:
@@ -1138,7 +1138,7 @@

Parameters:
Source:
@@ -1232,7 +1232,7 @@

Source:
@@ -1326,7 +1326,7 @@

Source:
@@ -1420,7 +1420,7 @@

Source:
@@ -1586,7 +1586,7 @@

Parameters:
Source:
@@ -1680,7 +1680,7 @@

Source:
@@ -1824,7 +1824,7 @@

Parameters:
Source:
diff --git a/docs/docs/SearchResultsPagerView.html b/docs/docs/SearchResultsPagerView.html index d88181694..9085d153b 100644 --- a/docs/docs/SearchResultsPagerView.html +++ b/docs/docs/SearchResultsPagerView.html @@ -127,7 +127,7 @@

Source:
@@ -248,7 +248,7 @@

Type:
Source:
@@ -326,7 +326,7 @@
Type:
Source:
@@ -405,7 +405,7 @@
Type:
Source:
@@ -483,7 +483,7 @@
Type:
Source:
@@ -562,7 +562,7 @@
Type:
Source:
@@ -700,7 +700,7 @@
Parameters:
Source:
@@ -843,7 +843,7 @@
Parameters:
Source:
@@ -940,7 +940,7 @@

Source:
@@ -1084,7 +1084,7 @@

Parameters:
Source:
@@ -1196,7 +1196,7 @@

Source:
@@ -1290,7 +1290,7 @@

Source:
@@ -1384,7 +1384,7 @@

Source:
@@ -1478,7 +1478,7 @@

Source:
@@ -1575,7 +1575,7 @@

Source:
@@ -1723,7 +1723,7 @@

Parameters:
Source:
diff --git a/docs/docs/SearchResultsView.html b/docs/docs/SearchResultsView.html index 55e7ebe2d..5162bd595 100644 --- a/docs/docs/SearchResultsView.html +++ b/docs/docs/SearchResultsView.html @@ -123,7 +123,7 @@

Source:
@@ -244,7 +244,7 @@

Type:
Source:
@@ -323,7 +323,7 @@
Type:
Source:
@@ -404,7 +404,7 @@
Type:
Source:
@@ -485,7 +485,7 @@
Type:
Source:
@@ -563,7 +563,7 @@
Type:
Source:
@@ -641,7 +641,7 @@
Type:
Source:
@@ -719,7 +719,7 @@
Type:
Source:
@@ -806,7 +806,7 @@

Source:
@@ -949,7 +949,7 @@

Parameters:
Source:
@@ -1092,7 +1092,7 @@
Parameters:
Source:
@@ -1186,7 +1186,7 @@

Source:
@@ -1280,7 +1280,7 @@

Source:
@@ -1375,7 +1375,7 @@

Source:
@@ -1471,7 +1471,7 @@

Source:
@@ -1565,7 +1565,7 @@

Source:
@@ -1659,7 +1659,7 @@

Source:
@@ -1776,7 +1776,7 @@

Source:
@@ -1961,7 +1961,7 @@

Parameters:
Source:
@@ -2055,7 +2055,7 @@

Source:
@@ -2150,7 +2150,7 @@

Source:
@@ -2248,7 +2248,7 @@

Source:
diff --git a/docs/docs/SearchView.html b/docs/docs/SearchView.html index 387ed8367..631d0252a 100644 --- a/docs/docs/SearchView.html +++ b/docs/docs/SearchView.html @@ -137,7 +137,7 @@

Source:
@@ -244,7 +244,7 @@

Source:
@@ -312,7 +312,7 @@

Source:
@@ -390,7 +390,7 @@

Type:
Source:
@@ -478,7 +478,7 @@

Source:
@@ -572,7 +572,7 @@

Source:
@@ -689,7 +689,7 @@

Source:
@@ -813,7 +813,7 @@

Parameters:
- Mouse event corresponding to a change in + Mouse event corresponding to a change in focus. @@ -855,7 +855,7 @@
Parameters:
Source:
@@ -950,7 +950,7 @@

Source:
@@ -1000,7 +1000,7 @@

- Event handler for Backbone.View configuration that is called whenever + Event handler for Backbone.View configuration that is called whenever the user types a key.
@@ -1045,7 +1045,7 @@

Source:
@@ -1141,7 +1141,7 @@

Source:
@@ -1235,7 +1235,7 @@

Source:
@@ -1329,7 +1329,7 @@

Source:
@@ -1423,7 +1423,7 @@

Source:
@@ -1611,7 +1611,7 @@

Source:
@@ -1706,7 +1706,7 @@

Source:
diff --git a/docs/docs/SearchableSelectView.html b/docs/docs/SearchableSelectView.html index 099f22c87..306439bdd 100644 --- a/docs/docs/SearchableSelectView.html +++ b/docs/docs/SearchableSelectView.html @@ -135,7 +135,7 @@

Source:
@@ -258,7 +258,7 @@

Type:
Source:
@@ -336,7 +336,7 @@
Type:
Source:
@@ -428,7 +428,7 @@
Type:
Source:
@@ -519,7 +519,7 @@
Type:
Source:
@@ -602,7 +602,7 @@
Type:
Source:
@@ -680,7 +680,7 @@
Type:
Source:
@@ -759,7 +759,7 @@
Type:
Source:
@@ -838,7 +838,7 @@
Type:
Source:
@@ -916,7 +916,7 @@
Type:
Source:
@@ -994,7 +994,7 @@
Type:
Source:
@@ -1072,7 +1072,7 @@
Type:
Source:
@@ -1305,7 +1305,7 @@
Properties:
Source:
@@ -1430,7 +1430,7 @@
Type:
Source:
@@ -1510,7 +1510,7 @@
Type:
Source:
@@ -1592,7 +1592,7 @@
Type:
Source:
@@ -1676,7 +1676,7 @@
Type:
Source:
@@ -1765,7 +1765,7 @@
Type:
Source:
@@ -1849,7 +1849,7 @@
Type:
Source:
@@ -1946,7 +1946,7 @@
Type:
Source:
@@ -2024,7 +2024,7 @@
Type:
Source:
@@ -2184,7 +2184,7 @@
Parameters:
Source:
@@ -2349,7 +2349,7 @@
Parameters:
Source:
@@ -2445,7 +2445,7 @@

Source:
@@ -2540,7 +2540,7 @@

Source:
@@ -2635,7 +2635,7 @@

Source:
@@ -2730,7 +2730,7 @@

Source:
@@ -2828,7 +2828,7 @@

Source:
@@ -2944,7 +2944,7 @@

Source:
@@ -3038,7 +3038,7 @@

Source:
@@ -3133,7 +3133,7 @@

Source:
@@ -3227,7 +3227,7 @@

Source:
@@ -3370,7 +3370,7 @@

Parameters:
Source:
@@ -3513,7 +3513,7 @@
Parameters:
Source:
@@ -3630,7 +3630,7 @@

Source:
@@ -3725,7 +3725,7 @@

Source:
@@ -3819,7 +3819,7 @@

Source:
@@ -3939,7 +3939,7 @@

Source:
@@ -4057,7 +4057,7 @@

Source:
@@ -4152,7 +4152,7 @@

Source:
@@ -4344,7 +4344,7 @@

Parameters:
Source:
@@ -4439,7 +4439,7 @@

Source:
@@ -4585,7 +4585,7 @@

Parameters:
Source:
diff --git a/docs/docs/SemanticFilterView.html b/docs/docs/SemanticFilterView.html index 17c48507a..bd1af33bb 100644 --- a/docs/docs/SemanticFilterView.html +++ b/docs/docs/SemanticFilterView.html @@ -268,7 +268,7 @@
Type:
Source:
@@ -361,7 +361,7 @@
Type:
Source:
@@ -452,7 +452,7 @@
Type:
Source:
@@ -539,7 +539,7 @@
Type:
Source:
@@ -625,7 +625,7 @@
Type:
Source:
@@ -717,7 +717,7 @@
Type:
Source:
@@ -800,7 +800,7 @@
Type:
Source:
@@ -887,7 +887,7 @@
Type:
Source:
@@ -971,7 +971,7 @@
Type:
Source:
@@ -1057,7 +1057,7 @@
Type:
Source:
@@ -1146,7 +1146,7 @@
Type:
Source:
@@ -1342,7 +1342,7 @@

Source:
@@ -1508,7 +1508,7 @@

Parameters:
Source:
@@ -1652,7 +1652,7 @@
Parameters:
Source:
@@ -1825,7 +1825,7 @@
Parameters:
Source:
@@ -1985,7 +1985,7 @@
Parameters:
Source:
@@ -2091,7 +2091,7 @@

Source:
@@ -2269,7 +2269,7 @@

Parameters:
Source:
@@ -2465,7 +2465,7 @@
Parameters:
Source:
@@ -2614,7 +2614,7 @@
Parameters:
Source:
@@ -2780,7 +2780,7 @@
Parameters:
Source:
@@ -2880,7 +2880,7 @@

Source:
@@ -3032,7 +3032,7 @@

Parameters:
Source:
diff --git a/docs/docs/SignInView.html b/docs/docs/SignInView.html index cac29848f..b744fccae 100644 --- a/docs/docs/SignInView.html +++ b/docs/docs/SignInView.html @@ -126,7 +126,7 @@

Source:
@@ -258,7 +258,7 @@

Type:
Source:
@@ -352,7 +352,7 @@

Source:
diff --git a/docs/docs/SolrResult.html b/docs/docs/SolrResult.html index 46171b42b..1c11268f2 100644 --- a/docs/docs/SolrResult.html +++ b/docs/docs/SolrResult.html @@ -120,7 +120,7 @@

Source:
@@ -307,7 +307,7 @@

Parameters:
Source:
@@ -411,7 +411,7 @@

Source:
@@ -523,7 +523,7 @@

Source:
@@ -684,7 +684,7 @@

Parameters:
Source:
diff --git a/docs/docs/SolrResults.html b/docs/docs/SolrResults.html index e5b3b1feb..af934e023 100644 --- a/docs/docs/SolrResults.html +++ b/docs/docs/SolrResults.html @@ -120,7 +120,7 @@

Source:
@@ -249,7 +249,7 @@

Type:
Source:
@@ -338,7 +338,7 @@

Source:
@@ -453,7 +453,7 @@

Source:
@@ -565,7 +565,7 @@

Source:
@@ -680,7 +680,7 @@

Source:
@@ -795,7 +795,7 @@

Source:
@@ -913,7 +913,7 @@

Source:
@@ -1028,7 +1028,7 @@

Source:
@@ -1147,7 +1147,7 @@

Source:
@@ -1262,7 +1262,7 @@

Source:
@@ -1378,7 +1378,7 @@

Source:
@@ -1490,7 +1490,7 @@

Source:
@@ -1584,7 +1584,7 @@

Source:
@@ -1727,7 +1727,7 @@

Parameters:
Source:
diff --git a/docs/docs/SorterView.html b/docs/docs/SorterView.html index 09043b56d..af33abe68 100644 --- a/docs/docs/SorterView.html +++ b/docs/docs/SorterView.html @@ -134,7 +134,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -334,7 +334,7 @@
Type:
Source:
@@ -413,7 +413,7 @@
Type:
Source:
@@ -491,7 +491,7 @@
Type:
Source:
@@ -580,7 +580,7 @@

Source:
@@ -677,7 +677,7 @@

Source:
@@ -771,7 +771,7 @@

Source:
@@ -915,7 +915,7 @@

Parameters:
Source:
@@ -1012,7 +1012,7 @@

Source:
diff --git a/docs/docs/SpatialFilter.html b/docs/docs/SpatialFilter.html index 95f7ae42c..3cb0c6cbe 100644 --- a/docs/docs/SpatialFilter.html +++ b/docs/docs/SpatialFilter.html @@ -247,7 +247,7 @@

Type:
Source:
@@ -965,7 +965,7 @@
Parameters:
Source:
@@ -1539,7 +1539,7 @@
Parameters:
Source:
@@ -1666,7 +1666,7 @@

Source:
@@ -2071,7 +2071,7 @@

Parameters:
Source:
@@ -2242,7 +2242,7 @@
Parameters:
Source:
@@ -2359,7 +2359,7 @@

Source:
@@ -2484,7 +2484,7 @@

Source:
@@ -2648,7 +2648,7 @@

Parameters:
Source:
@@ -2834,7 +2834,7 @@
Parameters:
Source:
@@ -3004,7 +3004,7 @@
Parameters:
Source:
@@ -3174,7 +3174,7 @@
Parameters:
Source:
@@ -3390,7 +3390,7 @@
Parameters:
Source:
@@ -4024,7 +4024,7 @@
Parameters:
Source:
@@ -4155,7 +4155,7 @@

Source:
@@ -4413,7 +4413,7 @@

Parameters:
Source:
@@ -4696,7 +4696,7 @@
Parameters:
Source:
@@ -4819,7 +4819,7 @@

Source:
diff --git a/docs/docs/Stats.html b/docs/docs/Stats.html index 4227449d3..5cf5faed9 100644 --- a/docs/docs/Stats.html +++ b/docs/docs/Stats.html @@ -120,7 +120,7 @@

Source:
@@ -838,7 +838,7 @@

Properties:
Source:
@@ -932,7 +932,7 @@

Source:
@@ -1025,7 +1025,7 @@

Source:
@@ -1118,7 +1118,7 @@

Source:
@@ -1212,7 +1212,7 @@

Source:
@@ -1307,7 +1307,7 @@

Source:
@@ -1400,7 +1400,7 @@

Source:
@@ -1494,7 +1494,7 @@

Source:
@@ -1588,7 +1588,7 @@

Source:
@@ -1681,7 +1681,7 @@

Source:
@@ -1775,7 +1775,7 @@

Source:
@@ -1919,7 +1919,7 @@

Parameters:
Source:
@@ -2034,7 +2034,7 @@

Source:
@@ -2177,7 +2177,7 @@

Parameters:
Source:
diff --git a/docs/docs/Subscription.html b/docs/docs/Subscription.html index 6c117e9e0..efeeef961 100644 --- a/docs/docs/Subscription.html +++ b/docs/docs/Subscription.html @@ -128,7 +128,7 @@

Source:
@@ -600,7 +600,7 @@

Properties:
Source:
@@ -678,7 +678,7 @@
Type:
Source:
@@ -764,7 +764,7 @@

Source:
diff --git a/docs/docs/TOCView.html b/docs/docs/TOCView.html index 9964f31e3..95cf388a8 100644 --- a/docs/docs/TOCView.html +++ b/docs/docs/TOCView.html @@ -324,7 +324,7 @@

Parameters:
Source:
@@ -418,7 +418,7 @@

Source:
@@ -515,7 +515,7 @@

Source:
@@ -663,7 +663,7 @@

Parameters:
Source:
@@ -762,7 +762,7 @@

Source:
@@ -907,7 +907,7 @@

Parameters:
Source:
diff --git a/docs/docs/TableEditorView.html b/docs/docs/TableEditorView.html index 0eff12e34..e6fa6249d 100644 --- a/docs/docs/TableEditorView.html +++ b/docs/docs/TableEditorView.html @@ -120,7 +120,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -320,7 +320,7 @@
Type:
Source:
@@ -398,7 +398,7 @@
Type:
Source:
@@ -476,7 +476,7 @@
Type:
Source:
@@ -555,7 +555,7 @@
Type:
Source:
@@ -633,7 +633,7 @@
Type:
Source:
@@ -711,7 +711,7 @@
Type:
Source:
@@ -790,7 +790,7 @@
Type:
Source:
@@ -868,7 +868,7 @@
Type:
Source:
@@ -1026,7 +1026,7 @@
Parameters:
Source:
@@ -1192,7 +1192,7 @@
Parameters:
Source:
@@ -1381,7 +1381,7 @@
Parameters:
Source:
@@ -1546,7 +1546,7 @@
Parameters:
Source:
@@ -1640,7 +1640,7 @@

Source:
@@ -1735,7 +1735,7 @@

Source:
@@ -1878,7 +1878,7 @@

Parameters:
Source:
@@ -2021,7 +2021,7 @@
Parameters:
Source:
@@ -2164,7 +2164,7 @@
Parameters:
Source:
@@ -2307,7 +2307,7 @@
Parameters:
Source:
@@ -2496,7 +2496,7 @@
Parameters:
Source:
@@ -2613,7 +2613,7 @@

Source:
@@ -2756,7 +2756,7 @@

Parameters:
Source:
@@ -2872,7 +2872,7 @@

Source:
@@ -3039,7 +3039,7 @@

Parameters:
Source:
@@ -3206,7 +3206,7 @@
Parameters:
Source:
@@ -3349,7 +3349,7 @@
Parameters:
Source:
@@ -3465,7 +3465,7 @@

Source:
@@ -3559,7 +3559,7 @@

Source:
@@ -3653,7 +3653,7 @@

Source:
@@ -3797,7 +3797,7 @@

Parameters:
Source:
@@ -3940,7 +3940,7 @@
Parameters:
Source:
@@ -4105,7 +4105,7 @@
Parameters:
Source:
@@ -4249,7 +4249,7 @@
Parameters:
Source:
@@ -4422,7 +4422,7 @@
Parameters:
Source:
@@ -4532,7 +4532,7 @@
Type:
Source:
@@ -4611,7 +4611,7 @@
Type:
Source:
@@ -4689,7 +4689,7 @@
Type:
Source:
@@ -4767,7 +4767,7 @@
Type:
Source:
@@ -4846,7 +4846,7 @@
Type:
Source:
@@ -4924,7 +4924,7 @@
Type:
Source:
@@ -5002,7 +5002,7 @@
Type:
Source:
@@ -5081,7 +5081,7 @@
Type:
Source:
@@ -5159,7 +5159,7 @@
Type:
Source:
@@ -5317,7 +5317,7 @@
Parameters:
Source:
@@ -5483,7 +5483,7 @@
Parameters:
Source:
@@ -5672,7 +5672,7 @@
Parameters:
Source:
@@ -5837,7 +5837,7 @@
Parameters:
Source:
@@ -5931,7 +5931,7 @@

Source:
@@ -6026,7 +6026,7 @@

Source:
@@ -6169,7 +6169,7 @@

Parameters:
Source:
@@ -6312,7 +6312,7 @@
Parameters:
Source:
@@ -6455,7 +6455,7 @@
Parameters:
Source:
@@ -6598,7 +6598,7 @@
Parameters:
Source:
@@ -6787,7 +6787,7 @@
Parameters:
Source:
@@ -6904,7 +6904,7 @@

Source:
@@ -7047,7 +7047,7 @@

Parameters:
Source:
@@ -7163,7 +7163,7 @@

Source:
@@ -7330,7 +7330,7 @@

Parameters:
Source:
@@ -7497,7 +7497,7 @@
Parameters:
Source:
@@ -7640,7 +7640,7 @@
Parameters:
Source:
@@ -7756,7 +7756,7 @@

Source:
@@ -7850,7 +7850,7 @@

Source:
@@ -7944,7 +7944,7 @@

Source:
@@ -8088,7 +8088,7 @@

Parameters:
Source:
@@ -8231,7 +8231,7 @@
Parameters:
Source:
@@ -8396,7 +8396,7 @@
Parameters:
Source:
@@ -8540,7 +8540,7 @@
Parameters:
Source:
diff --git a/docs/docs/ToggleFilter.html b/docs/docs/ToggleFilter.html index 4be6db8a3..0d778c063 100644 --- a/docs/docs/ToggleFilter.html +++ b/docs/docs/ToggleFilter.html @@ -120,7 +120,7 @@

Source:
@@ -246,7 +246,7 @@

Type:
Source:
@@ -487,7 +487,7 @@
Properties:
Source:
@@ -636,7 +636,7 @@
Parameters:
Source:
@@ -1004,7 +1004,7 @@
Parameters:
Source:
@@ -1131,7 +1131,7 @@

Source:
@@ -1252,7 +1252,7 @@

Source:
@@ -1417,7 +1417,7 @@

Parameters:
Source:
@@ -1588,7 +1588,7 @@
Parameters:
Source:
@@ -1705,7 +1705,7 @@

Source:
@@ -1830,7 +1830,7 @@

Source:
@@ -1994,7 +1994,7 @@

Parameters:
Source:
@@ -2180,7 +2180,7 @@
Parameters:
Source:
@@ -2350,7 +2350,7 @@
Parameters:
Source:
@@ -2520,7 +2520,7 @@
Parameters:
Source:
@@ -2736,7 +2736,7 @@
Parameters:
Source:
@@ -2935,7 +2935,7 @@
Parameters:
Source:
@@ -3066,7 +3066,7 @@

Source:
@@ -3226,7 +3226,7 @@

Parameters:
Source:
@@ -3408,7 +3408,7 @@
Parameters:
Source:
@@ -3529,7 +3529,7 @@

Source:
diff --git a/docs/docs/ToggleFilterView.html b/docs/docs/ToggleFilterView.html index 339b35d71..bec477f7b 100644 --- a/docs/docs/ToggleFilterView.html +++ b/docs/docs/ToggleFilterView.html @@ -120,7 +120,7 @@

Source:
@@ -254,7 +254,7 @@

Type:
Source:
@@ -347,7 +347,7 @@
Type:
Source:
@@ -438,7 +438,7 @@
Type:
Source:
@@ -525,7 +525,7 @@
Type:
Source:
@@ -611,7 +611,7 @@
Type:
Source:
@@ -703,7 +703,7 @@
Type:
Source:
@@ -786,7 +786,7 @@
Type:
Source:
@@ -873,7 +873,7 @@
Type:
Source:
@@ -957,7 +957,7 @@
Type:
Source:
@@ -1043,7 +1043,7 @@
Type:
Source:
@@ -1132,7 +1132,7 @@
Type:
Source:
@@ -1223,7 +1223,7 @@

Source:
@@ -1389,7 +1389,7 @@

Parameters:
Source:
@@ -1533,7 +1533,7 @@
Parameters:
Source:
@@ -1706,7 +1706,7 @@
Parameters:
Source:
@@ -1866,7 +1866,7 @@
Parameters:
Source:
@@ -1961,7 +1961,7 @@

Source:
@@ -2111,7 +2111,7 @@

Parameters:
Source:
@@ -2206,7 +2206,7 @@

Source:
@@ -2402,7 +2402,7 @@

Parameters:
Source:
@@ -2551,7 +2551,7 @@
Parameters:
Source:
@@ -2717,7 +2717,7 @@
Parameters:
Source:
@@ -2817,7 +2817,7 @@

Source:
@@ -2969,7 +2969,7 @@

Parameters:
Source:
diff --git a/docs/docs/ToolbarView.html b/docs/docs/ToolbarView.html index b6304d5ae..8c9be48a7 100644 --- a/docs/docs/ToolbarView.html +++ b/docs/docs/ToolbarView.html @@ -134,7 +134,7 @@

Source:
@@ -255,7 +255,7 @@

Type:
Source:
@@ -576,7 +576,7 @@
Properties:
Source:
@@ -655,7 +655,7 @@
Type:
Source:
@@ -733,7 +733,7 @@
Type:
Source:
@@ -812,7 +812,7 @@
Type:
Source:
@@ -892,7 +892,7 @@
Type:
Source:
@@ -970,7 +970,7 @@
Type:
Source:
@@ -1105,7 +1105,7 @@
Parameters:
Source:
@@ -1199,7 +1199,7 @@

Source:
@@ -1343,7 +1343,7 @@

Parameters:
Source:
@@ -1509,7 +1509,7 @@
Parameters:
Source:
@@ -1654,7 +1654,7 @@
Parameters:
Source:
@@ -1748,7 +1748,7 @@

Source:
@@ -1891,7 +1891,7 @@

Parameters:
Source:
@@ -2046,7 +2046,7 @@
Parameters:
Source:
@@ -2140,7 +2140,7 @@

Source:
@@ -2234,7 +2234,7 @@

Source:
@@ -2401,7 +2401,7 @@

Parameters:
Source:
@@ -2568,7 +2568,7 @@
Parameters:
Source:
diff --git a/docs/docs/UIRouter.html b/docs/docs/UIRouter.html index 6c95733b2..20a980117 100644 --- a/docs/docs/UIRouter.html +++ b/docs/docs/UIRouter.html @@ -120,7 +120,7 @@

Source:
@@ -247,7 +247,7 @@

Source:
@@ -414,7 +414,7 @@

Parameters:
Source:
@@ -635,7 +635,7 @@
Parameters:
Source:
@@ -729,7 +729,7 @@

Source:
diff --git a/docs/docs/Units.html b/docs/docs/Units.html index 6d84880d3..edb835121 100644 --- a/docs/docs/Units.html +++ b/docs/docs/Units.html @@ -120,7 +120,7 @@

Source:
diff --git a/docs/docs/Usage.html b/docs/docs/Usage.html index 601ffea35..f962cded4 100644 --- a/docs/docs/Usage.html +++ b/docs/docs/Usage.html @@ -127,7 +127,7 @@

Source:
@@ -507,7 +507,7 @@

Properties:
Source:
@@ -585,7 +585,7 @@
Type:
Source:
diff --git a/docs/docs/Usages.html b/docs/docs/Usages.html index 484e50aa4..d4184ca61 100644 --- a/docs/docs/Usages.html +++ b/docs/docs/Usages.html @@ -126,7 +126,7 @@

Source:
@@ -247,7 +247,7 @@

Type:
Source:
@@ -327,7 +327,7 @@
Type:
Source:
@@ -405,7 +405,7 @@
Type:
Source:
@@ -628,7 +628,7 @@
Properties:
Source:
@@ -723,7 +723,7 @@

Source:
@@ -818,7 +818,7 @@

Source:
@@ -1070,7 +1070,7 @@

Properties:
Source:
diff --git a/docs/docs/UserGroup.html b/docs/docs/UserGroup.html index 4f1333d04..e3a82d5a5 100644 --- a/docs/docs/UserGroup.html +++ b/docs/docs/UserGroup.html @@ -120,7 +120,7 @@

Source:
diff --git a/docs/docs/UserGroupView.html b/docs/docs/UserGroupView.html index a8e7a8877..e4fc436e1 100644 --- a/docs/docs/UserGroupView.html +++ b/docs/docs/UserGroupView.html @@ -131,7 +131,7 @@

Source:
@@ -306,7 +306,7 @@

Parameters:
Source:
@@ -449,7 +449,7 @@
Parameters:
Source:
@@ -615,7 +615,7 @@
Parameters:
Source:
@@ -709,7 +709,7 @@

Source:
@@ -804,7 +804,7 @@

Source:
@@ -970,7 +970,7 @@

Parameters:
Source:
@@ -1086,7 +1086,7 @@

Source:
@@ -1181,7 +1181,7 @@

Source:
@@ -1370,7 +1370,7 @@

Parameters:
Source:
diff --git a/docs/docs/UserView.html b/docs/docs/UserView.html index f7e00f156..e85369a75 100644 --- a/docs/docs/UserView.html +++ b/docs/docs/UserView.html @@ -131,7 +131,7 @@

Source:
@@ -252,7 +252,7 @@

Type:
Source:
@@ -387,7 +387,7 @@
Parameters:
Source:
@@ -481,7 +481,7 @@

Source:
diff --git a/docs/docs/Utilities.html b/docs/docs/Utilities.html index e5af8db27..7cf79ed61 100644 --- a/docs/docs/Utilities.html +++ b/docs/docs/Utilities.html @@ -97,7 +97,7 @@

Utilities

Source:
@@ -244,7 +244,7 @@
Parameters:
Source:
@@ -408,7 +408,7 @@
Parameters:
Source:
@@ -643,7 +643,7 @@
Parameters:
Source:
@@ -793,7 +793,7 @@
Parameters:
Source:
diff --git a/docs/docs/VectorFilter.html b/docs/docs/VectorFilter.html index acf8098af..e0f7f30ee 100644 --- a/docs/docs/VectorFilter.html +++ b/docs/docs/VectorFilter.html @@ -126,7 +126,7 @@

Source:
@@ -466,7 +466,7 @@

Properties:
Source:
@@ -544,7 +544,7 @@
Type:
Source:
@@ -680,7 +680,7 @@
Parameters:
Source:
diff --git a/docs/docs/VectorFilters.html b/docs/docs/VectorFilters.html index 6b72a072f..8d36201d4 100644 --- a/docs/docs/VectorFilters.html +++ b/docs/docs/VectorFilters.html @@ -124,7 +124,7 @@

Source:
@@ -245,7 +245,7 @@

Type:
Source:
@@ -382,7 +382,7 @@
Parameters:
Source:
diff --git a/docs/docs/ViewfinderModel.html b/docs/docs/ViewfinderModel.html index ca035de18..14d3277c8 100644 --- a/docs/docs/ViewfinderModel.html +++ b/docs/docs/ViewfinderModel.html @@ -124,7 +124,7 @@

Source:
@@ -368,7 +368,7 @@

Properties:
Source:
@@ -503,7 +503,7 @@
Parameters:
Source:
@@ -554,7 +554,7 @@

Decrement the focused index with a minimum value of 0. This corresponds -to an ArrowUp key down event. +to an ArrowUp key down event. Note: An ArrowUp key press while the current index is -1 will result in highlighting the first element in the list.
@@ -600,7 +600,7 @@

Source:
@@ -744,7 +744,7 @@

Parameters:
Source:
@@ -839,7 +839,7 @@

Source:
@@ -979,7 +979,7 @@

Parameters:
Source:
@@ -1074,7 +1074,7 @@

Source:
@@ -1124,7 +1124,7 @@

Parameters:
Source:
@@ -1362,7 +1362,7 @@
Parameters:
Source:
@@ -1466,7 +1466,7 @@
Parameters:
- A user selected preset for which to + A user selected preset for which to enable layers and navigate. @@ -1508,7 +1508,7 @@
Parameters:
Source:
diff --git a/docs/docs/ViewfinderView.html b/docs/docs/ViewfinderView.html index b94b992e0..d4392704a 100644 --- a/docs/docs/ViewfinderView.html +++ b/docs/docs/ViewfinderView.html @@ -135,7 +135,7 @@

Source:
@@ -256,7 +256,7 @@

Type:
Source:
@@ -324,7 +324,7 @@

Source:
@@ -402,7 +402,7 @@

Type:
Source:
@@ -493,7 +493,7 @@

Source:
@@ -587,7 +587,7 @@

Source:
@@ -706,7 +706,7 @@

Source:
@@ -824,7 +824,7 @@

Source:
@@ -918,7 +918,7 @@

Source:
@@ -1015,7 +1015,7 @@

Source:
diff --git a/docs/docs/ZoomPresetModel.html b/docs/docs/ZoomPresetModel.html index 030ffa4de..828d22d29 100644 --- a/docs/docs/ZoomPresetModel.html +++ b/docs/docs/ZoomPresetModel.html @@ -124,7 +124,7 @@

Source:
@@ -241,7 +241,7 @@

Type:
Source:
@@ -373,7 +373,7 @@
Parameters:
Source:
diff --git a/docs/docs/ZoomPresetView.html b/docs/docs/ZoomPresetView.html index 6bfc63fbe..2ba63eb41 100644 --- a/docs/docs/ZoomPresetView.html +++ b/docs/docs/ZoomPresetView.html @@ -135,7 +135,7 @@

Source:
@@ -242,7 +242,7 @@

Source:
@@ -310,7 +310,7 @@

Source:
@@ -388,7 +388,7 @@

Type:
Source:
@@ -474,7 +474,7 @@

Source:
@@ -570,7 +570,7 @@

Source:
@@ -665,7 +665,7 @@

Source:
diff --git a/docs/docs/ZoomPresets.html b/docs/docs/ZoomPresets.html index 0de1dfd93..26d06607d 100644 --- a/docs/docs/ZoomPresets.html +++ b/docs/docs/ZoomPresets.html @@ -125,7 +125,7 @@

Source:
@@ -232,7 +232,7 @@

Source:
@@ -388,7 +388,7 @@

Parameters:
Source:
diff --git a/docs/docs/ZoomPresetsListView.html b/docs/docs/ZoomPresetsListView.html index 3fc21c149..91b39297c 100644 --- a/docs/docs/ZoomPresetsListView.html +++ b/docs/docs/ZoomPresetsListView.html @@ -134,7 +134,7 @@

Source:
@@ -241,7 +241,7 @@

Source:
@@ -319,7 +319,7 @@

Type:
Source:
@@ -405,7 +405,7 @@

Source:
diff --git a/docs/docs/global.html b/docs/docs/global.html index ff9a456cf..1c705a463 100644 --- a/docs/docs/global.html +++ b/docs/docs/global.html @@ -479,7 +479,7 @@

Properties
Source:
@@ -614,7 +614,7 @@
Parameters:
Source:
@@ -890,7 +890,7 @@
Properties:
Source:
@@ -1120,7 +1120,7 @@
Properties:
Source:
@@ -1344,7 +1344,7 @@
Properties:
Source:
@@ -1553,7 +1553,7 @@
Properties:
Source:
@@ -1931,7 +1931,7 @@
Properties:
Source:
@@ -2171,7 +2171,7 @@
Properties:
Source:
@@ -2553,7 +2553,7 @@
Properties:
- The Backbone.View that + The Backbone.View that will be displayed when the content of the panel is toggled to be visible. @@ -2656,7 +2656,7 @@
Properties:
Source:
@@ -2806,7 +2806,7 @@
Properties:
Source:
@@ -3014,7 +3014,7 @@
Type:
Source:
@@ -3233,7 +3233,7 @@
Properties:
Source:
@@ -3625,7 +3625,7 @@
Properties:
Source:
@@ -3950,7 +3950,7 @@
Properties:
Source:
@@ -4220,7 +4220,7 @@
Properties:
Source:
@@ -4378,7 +4378,7 @@
Properties:
Source:
@@ -4506,7 +4506,7 @@
Properties:
Source:
@@ -4656,7 +4656,7 @@
Properties:
Source:
@@ -4860,7 +4860,7 @@
Properties:
Source:
@@ -5122,7 +5122,7 @@
Properties:
- A function that determines whether this + A function that determines whether this section should be visible in the toolbar. @@ -5162,7 +5162,7 @@
Properties:
Source:
@@ -5463,7 +5463,7 @@
Properties:
Source:
@@ -5930,7 +5930,7 @@
Properties:
Source:
@@ -6169,7 +6169,7 @@
Properties:
Source:
@@ -6319,7 +6319,7 @@
Properties:
Source:
@@ -6445,7 +6445,7 @@
Properties:
Source:
@@ -6572,7 +6572,7 @@
Properties:
Source:
@@ -6794,7 +6794,7 @@
Properties:
Source:
@@ -6945,7 +6945,7 @@
Properties:
Source:
@@ -7055,7 +7055,7 @@
Properties:
- The callback function for + The callback function for selecting a zoom preset. @@ -7095,7 +7095,7 @@
Properties:
Source:
diff --git a/docs/docs/scripts/linenumber.js b/docs/docs/scripts/linenumber.js index bdc5b4a8c..2e055f538 100644 --- a/docs/docs/scripts/linenumber.js +++ b/docs/docs/scripts/linenumber.js @@ -1,25 +1,25 @@ /* global document */ (() => { - const source = document.getElementsByClassName('prettyprint source linenums'); - let i = 0; - let lineNumber = 0; - let lineId; - let lines; - let totalLines; - let anchorHash; + const source = document.getElementsByClassName("prettyprint source linenums"); + let i = 0; + let lineNumber = 0; + let lineId; + let lines; + let totalLines; + let anchorHash; - if (source && source[0]) { - anchorHash = document.location.hash.substring(1); - lines = source[0].getElementsByTagName('li'); - totalLines = lines.length; + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName("li"); + totalLines = lines.length; - for (; i < totalLines; i++) { - lineNumber++; - lineId = `line${lineNumber}`; - lines[i].id = lineId; - if (lineId === anchorHash) { - lines[i].className += ' selected'; - } - } + for (; i < totalLines; i++) { + lineNumber++; + lineId = `line${lineNumber}`; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += " selected"; + } } + } })(); diff --git a/docs/docs/src_js_app.js.html b/docs/docs/src_js_app.js.html index a329cb113..a9e08cfd6 100644 --- a/docs/docs/src_js_app.js.html +++ b/docs/docs/src_js_app.js.html @@ -44,145 +44,163 @@

Source: src/js/app.js

-
/*global require */
-/*jshint unused:false */
-'use strict';
-
-MetacatUI.recaptchaURL = 'https://www.google.com/recaptcha/api/js/recaptcha_ajax';
-if( MetacatUI.mapKey ){
-	var gmapsURL = 'https://maps.googleapis.com/maps/api/js?v=3&libraries=places&key=' + MetacatUI.mapKey;
-	define('gmaps',
-			['async!' + gmapsURL],
-			function() {
-				return google.maps;
-			});
-
+            
"use strict";
+
+MetacatUI.recaptchaURL =
+  "https://www.google.com/recaptcha/api/js/recaptcha_ajax";
+if (MetacatUI.mapKey) {
+  var gmapsURL =
+    "https://maps.googleapis.com/maps/api/js?v=3&libraries=places&key=" +
+    MetacatUI.mapKey;
+  define("gmaps", ["async!" + gmapsURL], function () {
+    return google.maps;
+  });
 } else {
-	define('gmaps', null);
-
+  define("gmaps", null);
 }
 
-MetacatUI.d3URL = '../components/d3.v3.min';
-
+MetacatUI.d3URL = "../components/d3.v3.min";
 
 /* Configure the app to use requirejs, and map dependency aliases to their
    directory location (.js is ommitted). Shim libraries that don't natively
    support requirejs. */
 require.config({
-  baseUrl: MetacatUI.root + '/js/',
+  baseUrl: MetacatUI.root + "/js/",
   waitSeconds: 180, //wait 3 minutes before throwing a timeout error
   map: MetacatUI.themeMap,
-  urlArgs: "v=" + (MetacatUI.AppConfig.cachebuster || MetacatUI.metacatUIVersion),
+  urlArgs:
+    "v=" + (MetacatUI.AppConfig.cachebuster || MetacatUI.metacatUIVersion),
   paths: {
-    jquery: MetacatUI.root + '/components/jquery-1.9.1.min',
-    jqueryui: MetacatUI.root + '/components/jquery-ui.min',
-    jqueryform: MetacatUI.root + '/components/jquery.form',
-    underscore: MetacatUI.root + '/components/underscore-min',
-    backbone: MetacatUI.root + '/components/backbone-min',
-    localforage: MetacatUI.root + '/components/localforage.min',
-    bootstrap: MetacatUI.root + '/components/bootstrap.min',
-    text: MetacatUI.root + '/components/require-text',
-    jws: MetacatUI.root + '/components/jws-3.2.min',
-    jsrasign: MetacatUI.root + '/components/jsrsasign-4.9.0.min',
-    async: MetacatUI.root + '/components/async',
-    recaptcha: [MetacatUI.recaptchaURL, 'scripts/placeholder'],
-	nGeohash: MetacatUI.root + '/components/geohash/main',
-	fancybox: MetacatUI.root + '/components/fancybox/jquery.fancybox.pack', //v. 2.1.5
-    annotator: MetacatUI.root + '/components/annotator/v1.2.10/annotator-full',
-    bioportal: MetacatUI.root + '/components/bioportal/jquery.ncbo.tree-2.0.2',
-    clipboard: MetacatUI.root + '/components/clipboard.min',
-    uuid: MetacatUI.root + '/components/uuid',
-    md5: MetacatUI.root + '/components/md5',
-    rdflib: MetacatUI.root + '/components/rdflib.min',
-    x2js: MetacatUI.root + '/components/xml2json',
-    he: MetacatUI.root + '/components/he',
-    citation: MetacatUI.root + '/components/citation.min',
-    promise: MetacatUI.root + '/components/es6-promise.min',
-	metacatuiConnectors: MetacatUI.root + "/js/connectors/Filters-Search",
-	// showdown + extensions (used in the MarkdownView to convert markdown to html)
-	showdown: MetacatUI.root + '/components/showdown/showdown.min',
-	showdownHighlight: MetacatUI.root + '/components/showdown/extensions/showdown-highlight/showdown-highlight',
-	highlight: MetacatUI.root + '/components/showdown/extensions/showdown-highlight/highlight.pack',
-	showdownFootnotes: MetacatUI.root + '/components/showdown/extensions/showdown-footnotes',
-	showdownBootstrap: MetacatUI.root + '/components/showdown/extensions/showdown-bootstrap',
-	showdownDocbook: MetacatUI.root + '/components/showdown/extensions/showdown-docbook',
-	showdownKatex: MetacatUI.root + '/components/showdown/extensions/showdown-katex/showdown-katex.min',
-	showdownCitation:  MetacatUI.root + '/components/showdown/extensions/showdown-citation/showdown-citation',
-	showdownImages:  MetacatUI.root + '/components/showdown/extensions/showdown-images',
-	showdownXssFilter: MetacatUI.root + '/components/showdown/extensions/showdown-xss-filter/showdown-xss-filter',
-	xss: MetacatUI.root + '/components/showdown/extensions/showdown-xss-filter/xss.min',
-	showdownHtags: MetacatUI.root + '/components/showdown/extensions/showdown-htags',
-	// woofmark - markdown editor
-	woofmark: MetacatUI.root + '/components/woofmark.min',
-	// drop zone creates drag and drop areas
-	Dropzone: MetacatUI.root + '/components/dropzone-amd-module',
-	// Packages that convert between json data to markdown table
-	markdownTableFromJson: MetacatUI.root + '/components/markdown-table-from-json.min',
-	markdownTableToJson: MetacatUI.root + '/components/markdown-table-to-json',
-	// Polyfill required for using dropzone with older browsers
-	corejs: MetacatUI.root + '/components/core-js',
-	// Searchable multi-select dropdown component
-	semanticUItransition: MetacatUI.root + '/components/semanticUI/transition.min',
-	semanticUIdropdown: MetacatUI.root + '/components/semanticUI/dropdown.min',
-	// To make elements drag and drop, sortable
-	sortable: MetacatUI.root + '/components/sortable.min',
-  //Cesium
-  cesium: 'https://cesium.com/downloads/cesiumjs/releases/1.91/Build/Cesium/Cesium',
-	//Have a null fallback for our d3 components for browsers that don't support SVG
-	d3: MetacatUI.d3URL,
-	LineChart: ['views/LineChartView', null],
-	BarChart: ['views/BarChartView', null],
-	CircleBadge: ['views/CircleBadgeView', null],
-	DonutChart: ['views/DonutChartView', null],
-	MetricsChart: ['views/MetricsChartView', null],
+    jquery: MetacatUI.root + "/components/jquery-1.9.1.min",
+    jqueryui: MetacatUI.root + "/components/jquery-ui.min",
+    jqueryform: MetacatUI.root + "/components/jquery.form",
+    underscore: MetacatUI.root + "/components/underscore-min",
+    backbone: MetacatUI.root + "/components/backbone-min",
+    localforage: MetacatUI.root + "/components/localforage.min",
+    bootstrap: MetacatUI.root + "/components/bootstrap.min",
+    text: MetacatUI.root + "/components/require-text",
+    jws: MetacatUI.root + "/components/jws-3.2.min",
+    jsrasign: MetacatUI.root + "/components/jsrsasign-4.9.0.min",
+    async: MetacatUI.root + "/components/async",
+    recaptcha: [MetacatUI.recaptchaURL, "scripts/placeholder"],
+    nGeohash: MetacatUI.root + "/components/geohash/main",
+    fancybox: MetacatUI.root + "/components/fancybox/jquery.fancybox.pack", //v. 2.1.5
+    annotator: MetacatUI.root + "/components/annotator/v1.2.10/annotator-full",
+    bioportal: MetacatUI.root + "/components/bioportal/jquery.ncbo.tree-2.0.2",
+    clipboard: MetacatUI.root + "/components/clipboard.min",
+    uuid: MetacatUI.root + "/components/uuid",
+    md5: MetacatUI.root + "/components/md5",
+    rdflib: MetacatUI.root + "/components/rdflib.min",
+    x2js: MetacatUI.root + "/components/xml2json",
+    he: MetacatUI.root + "/components/he",
+    citation: MetacatUI.root + "/components/citation.min",
+    promise: MetacatUI.root + "/components/es6-promise.min",
+    metacatuiConnectors: MetacatUI.root + "/js/connectors/Filters-Search",
+    // showdown + extensions (used in the MarkdownView to convert markdown to html)
+    showdown: MetacatUI.root + "/components/showdown/showdown.min",
+    showdownHighlight:
+      MetacatUI.root +
+      "/components/showdown/extensions/showdown-highlight/showdown-highlight",
+    highlight:
+      MetacatUI.root +
+      "/components/showdown/extensions/showdown-highlight/highlight.pack",
+    showdownFootnotes:
+      MetacatUI.root + "/components/showdown/extensions/showdown-footnotes",
+    showdownBootstrap:
+      MetacatUI.root + "/components/showdown/extensions/showdown-bootstrap",
+    showdownDocbook:
+      MetacatUI.root + "/components/showdown/extensions/showdown-docbook",
+    showdownKatex:
+      MetacatUI.root +
+      "/components/showdown/extensions/showdown-katex/showdown-katex.min",
+    showdownCitation:
+      MetacatUI.root +
+      "/components/showdown/extensions/showdown-citation/showdown-citation",
+    showdownImages:
+      MetacatUI.root + "/components/showdown/extensions/showdown-images",
+    showdownXssFilter:
+      MetacatUI.root +
+      "/components/showdown/extensions/showdown-xss-filter/showdown-xss-filter",
+    xss:
+      MetacatUI.root +
+      "/components/showdown/extensions/showdown-xss-filter/xss.min",
+    showdownHtags:
+      MetacatUI.root + "/components/showdown/extensions/showdown-htags",
+    // woofmark - markdown editor
+    woofmark: MetacatUI.root + "/components/woofmark.min",
+    // drop zone creates drag and drop areas
+    Dropzone: MetacatUI.root + "/components/dropzone-amd-module",
+    // Packages that convert between json data to markdown table
+    markdownTableFromJson:
+      MetacatUI.root + "/components/markdown-table-from-json.min",
+    markdownTableToJson: MetacatUI.root + "/components/markdown-table-to-json",
+    // Polyfill required for using dropzone with older browsers
+    corejs: MetacatUI.root + "/components/core-js",
+    // Searchable multi-select dropdown component
+    semanticUItransition:
+      MetacatUI.root + "/components/semanticUI/transition.min",
+    semanticUIdropdown: MetacatUI.root + "/components/semanticUI/dropdown.min",
+    // To make elements drag and drop, sortable
+    sortable: MetacatUI.root + "/components/sortable.min",
+    //Cesium
+    cesium:
+      "https://cesium.com/downloads/cesiumjs/releases/1.91/Build/Cesium/Cesium",
+    //Have a null fallback for our d3 components for browsers that don't support SVG
+    d3: MetacatUI.d3URL,
+    LineChart: ["views/LineChartView", null],
+    BarChart: ["views/BarChartView", null],
+    CircleBadge: ["views/CircleBadgeView", null],
+    DonutChart: ["views/DonutChartView", null],
+    MetricsChart: ["views/MetricsChartView", null],
   },
-  shim: { /* used for libraries without native AMD support */
+  shim: {
+    /* used for libraries without native AMD support */
     underscore: {
-      exports: '_',
+      exports: "_",
     },
     backbone: {
-      deps: ['underscore', 'jquery'],
-      exports: 'Backbone'
+      deps: ["underscore", "jquery"],
+      exports: "Backbone",
     },
     bootstrap: {
-    	deps: ['jquery'],
-    	exports: 'Bootstrap'
+      deps: ["jquery"],
+      exports: "Bootstrap",
     },
     annotator: {
-    	exports: 'Annotator'
+      exports: "Annotator",
     },
     bioportal: {
-    	exports: 'Bioportal'
+      exports: "Bioportal",
     },
     jws: {
-    	exports: 'JWS',
-        deps: ['jsrasign'],
+      exports: "JWS",
+      deps: ["jsrasign"],
     },
-	nGeohash: {
-		exports: "geohash"
-	},
-	fancybox: {
-		deps: ['jquery']
-	},
-	uuid: {
-        exports: 'uuid'
+    nGeohash: {
+      exports: "geohash",
+    },
+    fancybox: {
+      deps: ["jquery"],
+    },
+    uuid: {
+      exports: "uuid",
     },
     rdflib: {
-        exports: 'rdf'
+      exports: "rdf",
+    },
+    xss: {
+      exports: "filterXSS",
     },
-	xss: {
-		exports: 'filterXSS'
-	},
-	citation: {
-		exports: 'citationRequire'
-	},
-	promise: {
-	 	exports: 'Promise'
-	},
-	metacatuiConnectors: {
-		exports: "FiltersSearchConnector"
-	}
-  }
+    citation: {
+      exports: "citationRequire",
+    },
+    promise: {
+      exports: "Promise",
+    },
+    metacatuiConnectors: {
+      exports: "FiltersSearchConnector",
+    },
+  },
 });
 
 MetacatUI.appModel = MetacatUI.appModel || {};
@@ -191,10 +209,10 @@ 

Source: src/js/app.js

MetacatUI.appSearchResults = MetacatUI.appSearchResults || {}; MetacatUI.appSearchModel = MetacatUI.appSearchModel || {}; /** -* @name MetacatUI.rootDataPackage -* @type {string} -* @description The top-level {@link DataPackage} that is currently being viewed or edited in MetacatUI. -*/ + * @name MetacatUI.rootDataPackage + * @type {string} + * @description The top-level {@link DataPackage} that is currently being viewed or edited in MetacatUI. + */ MetacatUI.rootDataPackage = MetacatUI.rootDataPackage || {}; MetacatUI.statsModel = MetacatUI.statsModel || {}; MetacatUI.mapModel = MetacatUI.mapModel || {}; @@ -204,182 +222,230 @@

Source: src/js/app.js

MetacatUI.analytics = MetacatUI.analytics || {}; /* Setup the application scaffolding first */ -require(['bootstrap', 'views/AppView', 'models/AppModel'], -function(Bootstrap, AppView, AppModel) { - 'use strict'; - - // Create an AppModel, which controls the global app configuration and app states +require(["bootstrap", "views/AppView", "models/AppModel"], function ( + Bootstrap, + AppView, + AppModel, +) { + "use strict"; + + // Create an AppModel, which controls the global app configuration and app states // To be compatible with MetacatUI 2.11.X and earlier, we need to set the metacat context attribute here. // This supports the old way tof configuring the app via the index.html file. // As of MetacatUI 2.12.0, it is recommended that you configure MetacatUI via an AppConfig file. - MetacatUI.appModel = new AppModel({ context: MetacatUI.AppConfig.metacatContext }); - - //Check for custom settings in the theme config file - if(typeof MetacatUI.customAppConfig == "function") MetacatUI.customAppConfig(); - - /* Now require the rest of the libraries for the application */ - require(['underscore', 'backbone', 'routers/router', 'collections/SolrResults', 'models/Search', - 'models/Stats', 'models/Map', 'models/LookupModel', 'models/NodeModel', - 'models/UserModel', 'models/DataONEObject', 'collections/DataPackage' - ], - function(_, Backbone, UIRouter, SolrResultList, Search, Stats, MapModel, LookupModel, NodeModel, UserModel, DataONEObject, DataPackage) { - 'use strict'; - - //Create all the other models and collections first - MetacatUI.appSearchResults = new SolrResultList([], {}); - - MetacatUI.appSearchModel = new Search(); - - MetacatUI.statsModel = new Stats(); - - MetacatUI.mapModel = (typeof customMapModelOptions == "object")? new MapModel(customMapModelOptions) : new MapModel(); - - MetacatUI.appLookupModel = new LookupModel(); - - MetacatUI.nodeModel = new NodeModel(); - - MetacatUI.appUserModel = new UserModel(); - - require(['models/analytics/GoogleAnalytics'], function (Analytics) { - MetacatUI.analytics = new Analytics(); - }); - - /* Create a general event dispatcher to enable + MetacatUI.appModel = new AppModel({ + context: MetacatUI.AppConfig.metacatContext, + }); + + //Check for custom settings in the theme config file + if (typeof MetacatUI.customAppConfig == "function") + MetacatUI.customAppConfig(); + + /* Now require the rest of the libraries for the application */ + require([ + "underscore", + "backbone", + "routers/router", + "collections/SolrResults", + "models/Search", + "models/Stats", + "models/Map", + "models/LookupModel", + "models/NodeModel", + "models/UserModel", + "models/DataONEObject", + "collections/DataPackage", + ], function ( + _, + Backbone, + UIRouter, + SolrResultList, + Search, + Stats, + MapModel, + LookupModel, + NodeModel, + UserModel, + DataONEObject, + DataPackage, + ) { + "use strict"; + + //Create all the other models and collections first + MetacatUI.appSearchResults = new SolrResultList([], {}); + + MetacatUI.appSearchModel = new Search(); + + MetacatUI.statsModel = new Stats(); + + MetacatUI.mapModel = + typeof customMapModelOptions == "object" + ? new MapModel(customMapModelOptions) + : new MapModel(); + + MetacatUI.appLookupModel = new LookupModel(); + + MetacatUI.nodeModel = new NodeModel(); + + MetacatUI.appUserModel = new UserModel(); + + require(["models/analytics/GoogleAnalytics"], function (Analytics) { + MetacatUI.analytics = new Analytics(); + }); + + /* Create a general event dispatcher to enable communication across app components */ - MetacatUI.eventDispatcher = _.clone(Backbone.Events); + MetacatUI.eventDispatcher = _.clone(Backbone.Events); - //Load the App View now - MetacatUI.appView = new AppView(); + //Load the App View now + MetacatUI.appView = new AppView(); MetacatUI.appView.render(); - // Initialize routing and start Backbone.history() - (function() { - /** - * Backbone.routeNotFound - * - * Simple plugin that listens for false returns on Backbone.history.loadURL and fires an event - * to let the application know that no routes matched. - * - * @author STRML - */ - var oldLoadUrl = Backbone.History.prototype.loadUrl; - - _.extend(Backbone.History.prototype, { - - /* - * Override loadUrl & watch return value. Trigger event if no route was matched. + // Initialize routing and start Backbone.history() + (function () { + /** + * Backbone.routeNotFound + * + * Simple plugin that listens for false returns on Backbone.history.loadURL and fires an event + * to let the application know that no routes matched. + * + * @author STRML + */ + var oldLoadUrl = Backbone.History.prototype.loadUrl; + + _.extend(Backbone.History.prototype, { + /* + * Override loadUrl & watch return value. Trigger event if no route was matched. * @extends Backbone.History - * @return {Boolean} True if a route was matched - */ - loadUrl : function(fragment) { - if (!this.matchRoot()) return false; - fragment = this.fragment = this.getFragment(fragment); - var match = _.some(this.handlers, function(handler) { - if (handler.route.test(fragment)) { - handler.callback(fragment); - return true; - } - }); - - if(!match) this.trigger("routeNotFound"); - return match; - }, - matchRoot: function() { - var path = this.decodeFragment(this.location.pathname); - var rootPath = path.slice(0, this.root.length - 1) + '/'; - return rootPath === this.root; - }, - decodeFragment: function(fragment) { - return decodeURI(fragment.replace(/%25/g, '%2525')); - } - }); - }).call(this); - - //Make the router and begin the Backbone history - //The router will figure out which view to load first based on window location - MetacatUI.uiRouter = new UIRouter(); - - //Take the protocol and origin out of the root URL when sending it to Backbone.history. - // The root URL sent to Backbone.history should be either `/` or `/directory/...` - var historyRoot = MetacatUI.root; - - //If there is a protocol - if( historyRoot.indexOf("://") > -1 ){ - //Get the substring after the ``://`` - historyRoot = historyRoot.substring(historyRoot.indexOf("://") + 3); - - //If there is no `/`, this must be the root directory - if( historyRoot.indexOf("/") == -1 ) - historyRoot = "/"; - //Otherwise get the substring after the first / - else - historyRoot = historyRoot.substring( historyRoot.indexOf("/") ); - } - //If there are no colons, periods, or slashes, this is a directory name - else if( historyRoot.indexOf(":") == -1 && - historyRoot.indexOf(".") == -1 && - historyRoot.indexOf("/") == -1 ){ - //So the root is a leading slash and the directory name - historyRoot = "/" + historyRoot; - } - //If there is a slash, get the path name starting with the slash - else if( historyRoot.indexOf("/") > -1 ){ - historyRoot = historyRoot.substring( historyRoot.indexOf("/") ); - } - //All other strings are the root directory - else{ - historyRoot = "/"; - } - - Backbone.history.start({ - pushState: true, - root: historyRoot - }); - - $(document).on("click", "a:not([data-toggle],[target])", function(evt) { - // Don't hijack the event if the user had Control or Command held down - if (evt.ctrlKey || evt.metaKey) { - return; - } - - var href = { prop: $(this).prop("href"), attr: $(this).attr("href") }; - - // Stop if the click happened on an a w/o an href - // This is kind of a weird edge case where. This could be removed if - // we remove these instances from the codebase - if (typeof href === "undefined" || typeof href.attr === "undefined" || - href.attr === "") { - return; - } - - //Don't route to URLs with the DataONE API, which are sometimes proxied - // via Apache ProxyPass so start with the MetacatUI origin - if( href.attr.indexOf("/cn/v2/") > 0 || href.attr.indexOf("/mn/v2/") > 0 ){ - return; - } - - var root = location.protocol + "//" + location.host + Backbone.history.options.root; - // Remove the MetacatUI (plus a trailing /) from the value in the 'href' - // attribute of the clicked element so Backbone.history.navigate works. - // Note that a RegExp was used here to anchor the .replace call to the - // front of the string so that this code works when MetacatUI.root is "". - var route = href.attr.replace(new RegExp("^" + MetacatUI.root + "/"), ""); - - // Catch routes hrefs that start with # and don't do anything with them - if (href.attr.indexOf("#") == 0) { return; } - - //If the URL is not a route defined in the app router, then follow the link - //If the URL is not at the MetacatUI root, then follow the link - if (href.prop && href.prop.slice(0, root.length) === root && - _.contains(MetacatUI.uiRouter.getRouteNames(), MetacatUI.uiRouter.getRouteName(route))) { - evt.preventDefault(); - Backbone.history.navigate(route, true); - } - }); - - MetacatUI.appModel.trigger("appInitialized"); - }); + * @return {Boolean} True if a route was matched + */ + loadUrl: function (fragment) { + if (!this.matchRoot()) return false; + fragment = this.fragment = this.getFragment(fragment); + var match = _.some(this.handlers, function (handler) { + if (handler.route.test(fragment)) { + handler.callback(fragment); + return true; + } + }); + + if (!match) this.trigger("routeNotFound"); + return match; + }, + matchRoot: function () { + var path = this.decodeFragment(this.location.pathname); + var rootPath = path.slice(0, this.root.length - 1) + "/"; + return rootPath === this.root; + }, + decodeFragment: function (fragment) { + return decodeURI(fragment.replace(/%25/g, "%2525")); + }, + }); + }).call(this); + + //Make the router and begin the Backbone history + //The router will figure out which view to load first based on window location + MetacatUI.uiRouter = new UIRouter(); + + //Take the protocol and origin out of the root URL when sending it to Backbone.history. + // The root URL sent to Backbone.history should be either `/` or `/directory/...` + var historyRoot = MetacatUI.root; + + //If there is a protocol + if (historyRoot.indexOf("://") > -1) { + //Get the substring after the ``://`` + historyRoot = historyRoot.substring(historyRoot.indexOf("://") + 3); + + //If there is no `/`, this must be the root directory + if (historyRoot.indexOf("/") == -1) historyRoot = "/"; + //Otherwise get the substring after the first / + else historyRoot = historyRoot.substring(historyRoot.indexOf("/")); + } + //If there are no colons, periods, or slashes, this is a directory name + else if ( + historyRoot.indexOf(":") == -1 && + historyRoot.indexOf(".") == -1 && + historyRoot.indexOf("/") == -1 + ) { + //So the root is a leading slash and the directory name + historyRoot = "/" + historyRoot; + } + //If there is a slash, get the path name starting with the slash + else if (historyRoot.indexOf("/") > -1) { + historyRoot = historyRoot.substring(historyRoot.indexOf("/")); + } + //All other strings are the root directory + else { + historyRoot = "/"; + } + + Backbone.history.start({ + pushState: true, + root: historyRoot, + }); + + $(document).on("click", "a:not([data-toggle],[target])", function (evt) { + // Don't hijack the event if the user had Control or Command held down + if (evt.ctrlKey || evt.metaKey) { + return; + } + + var href = { prop: $(this).prop("href"), attr: $(this).attr("href") }; + + // Stop if the click happened on an a w/o an href + // This is kind of a weird edge case where. This could be removed if + // we remove these instances from the codebase + if ( + typeof href === "undefined" || + typeof href.attr === "undefined" || + href.attr === "" + ) { + return; + } + + //Don't route to URLs with the DataONE API, which are sometimes proxied + // via Apache ProxyPass so start with the MetacatUI origin + if ( + href.attr.indexOf("/cn/v2/") > 0 || + href.attr.indexOf("/mn/v2/") > 0 + ) { + return; + } + + var root = + location.protocol + + "//" + + location.host + + Backbone.history.options.root; + // Remove the MetacatUI (plus a trailing /) from the value in the 'href' + // attribute of the clicked element so Backbone.history.navigate works. + // Note that a RegExp was used here to anchor the .replace call to the + // front of the string so that this code works when MetacatUI.root is "". + var route = href.attr.replace(new RegExp("^" + MetacatUI.root + "/"), ""); + + // Catch routes hrefs that start with # and don't do anything with them + if (href.attr.indexOf("#") == 0) { + return; + } + + //If the URL is not a route defined in the app router, then follow the link + //If the URL is not at the MetacatUI root, then follow the link + if ( + href.prop && + href.prop.slice(0, root.length) === root && + _.contains( + MetacatUI.uiRouter.getRouteNames(), + MetacatUI.uiRouter.getRouteName(route), + ) + ) { + evt.preventDefault(); + Backbone.history.navigate(route, true); + } + }); + + MetacatUI.appModel.trigger("appInitialized"); + }); });
diff --git a/docs/docs/src_js_collections_AccessPolicy.js.html b/docs/docs/src_js_collections_AccessPolicy.js.html index 52167887f..2ed11d357 100644 --- a/docs/docs/src_js_collections_AccessPolicy.js.html +++ b/docs/docs/src_js_collections_AccessPolicy.js.html @@ -46,385 +46,359 @@

Source: src/js/collections/AccessPolicy.js

"use strict";
 
-define(["jquery", "underscore", "backbone", "models/AccessRule"],
-    function($, _, Backbone, AccessRule) {
+define(["jquery", "underscore", "backbone", "models/AccessRule"], function (
+  $,
+  _,
+  Backbone,
+  AccessRule,
+) {
+  /**
+   * @class AccessPolicy
+   * @classdesc An AccessPolicy collection is a collection of AccessRules that specify
+   * the permissions set on a DataONEObject
+   * @classcategory Collections
+   * @extends Backbone.Collection
+   */
+  var AccessPolicy = Backbone.Collection.extend(
+    /** @lends AccessPolicy.prototype */
+    {
+      model: AccessRule,
 
       /**
-       * @class AccessPolicy
-       * @classdesc An AccessPolicy collection is a collection of AccessRules that specify
-       * the permissions set on a DataONEObject
-       * @classcategory Collections
-       * @extends Backbone.Collection
+       * The DataONEObject that will be saved with this AccessPolicy
+       * @type {DataONEObject}
        */
-      var AccessPolicy = Backbone.Collection.extend(
-        /** @lends AccessPolicy.prototype */
-        {
+      dataONEObject: null,
 
-          model: AccessRule,
+      initialize: function () {
+        //When a model triggers the event "removeMe", remove it from this collection
+        this.on("removeMe", this.removeAccessRule);
+      },
 
-          /**
-          * The DataONEObject that will be saved with this AccessPolicy
-          * @type {DataONEObject}
-          */
-          dataONEObject: null,
-
-          initialize: function(){
-
-            //When a model triggers the event "removeMe", remove it from this collection
-            this.on("removeMe", this.removeAccessRule);
-
-          },
-
-          /**
-          * Parses the given access policy XML and creates AccessRule models for
-          * each rule in the access policy XML. Adds these models to this collection.
-          * @param {Element} The <accessPolicy> XML DOM that contains a set of
-          *   access rules.
-          */
-          parse: function(accessPolicyXML){
-
-            var originalLength = this.length,
-                newLength      = 0;
-
-            //Parse each "allow" access rule
-      			_.each( $(accessPolicyXML).children(), function(accessRuleXML, i){
-
-              var accessRuleModel;
-
-              //Update the AccessRule models that already exist in the collection, first.
-              // This is important to keep listeners thoughout the app intact.
-              if( AccessRule.prototype.isPrototypeOf(this.models[i]) ){
-                accessRuleModel = this.models[i];
-              }
-              //Create new AccessRules for all others
-              else{
-                accessRuleModel = new AccessRule();
-                this.add( accessRuleModel );
-              }
-
-              newLength++;
-
-              //Reset all the values first
-              accessRuleModel.set( accessRuleModel.defaults() );
-              //Parse the AccessRule model and update the model attributes
-              accessRuleModel.set( accessRuleModel.parse(accessRuleXML) );
-              //Save a reference to the DataONEObbject
-              accessRuleModel.set("dataONEObject", this.dataONEObject);
-
-      			}, this);
-
-            //If there are more AccessRules in this collection than were in the
-            // system metadata XML, then remove the extras
-            if( originalLength > newLength ){
-              for(var i=0; i < (originalLength - newLength); i++){
-                this.pop();
-              }
-            }
-
-          },
-
-          /**
-          * Creates AccessRule member models from the `defaultAccessPolicy`
-          * setting in the AppModel.
-          */
-          createDefaultPolicy: function(){
-
-            //For each access policy in the AppModel, create an AccessRule model
-            _.each(MetacatUI.appModel.get("defaultAccessPolicy"), function(accessRule){
-
-              accessRule.dataONEObject = this.dataONEObject;
-
-              this.add( new AccessRule(accessRule) );
-
-            }, this);
-
-          },
-
-          /**
-          * Copies all the AccessRules from the given AccessPolicy and replaces this AccessPolicy
-          * @param {AccessPolicy} otherAccessPolicy
-          * @fires Backbone.Collection#reset
-          * @since 2.15.0
-          */
-          copyAccessPolicy: function(otherAccessPolicy){
-
-            try{
-
-              let accessRules = [];
-
-              //For each access policy in the AppModel, create an AccessRule model
-              otherAccessPolicy.each(function(accessRule){
-
-                //Convert the AccessRule model to JSON and update the reference to the DataONEObject
-                let accessRuleJSON = accessRule.toJSON();
-                accessRuleJSON.dataONEObject = this.dataONEObject;
-                accessRules.push(accessRuleJSON);
-
-              }, this);
-
-              //Reset the Collection with these AccessRules
-              this.reset(accessRules);
+      /**
+       * Parses the given access policy XML and creates AccessRule models for
+       * each rule in the access policy XML. Adds these models to this collection.
+       * @param {Element} The <accessPolicy> XML DOM that contains a set of
+       *   access rules.
+       */
+      parse: function (accessPolicyXML) {
+        var originalLength = this.length,
+          newLength = 0;
+
+        //Parse each "allow" access rule
+        _.each(
+          $(accessPolicyXML).children(),
+          function (accessRuleXML, i) {
+            var accessRuleModel;
+
+            //Update the AccessRule models that already exist in the collection, first.
+            // This is important to keep listeners thoughout the app intact.
+            if (AccessRule.prototype.isPrototypeOf(this.models[i])) {
+              accessRuleModel = this.models[i];
             }
-            catch(e){
-              console.error(e);
+            //Create new AccessRules for all others
+            else {
+              accessRuleModel = new AccessRule();
+              this.add(accessRuleModel);
             }
-          },
 
-          /**
-           * Creates an access policy XML from the values set on the member
-           * AccessRule models.
-           * @returns {object} A XML object of the access policy or null if empty
-           */
-          serialize: function() {
-            if (this.length === 0) {
-                return null;
-            }
-
-            // Create the access policy node which will contain all the rules
-            var accessPolicyElement = document.createElement('accesspolicy');
-
-            // Serialize each AccessRule member model and add to the policy DOM
-            this.each(function(accessRule) {
-                var accessRuleNode = accessRule.serialize();
-                if (accessRuleNode) {
-                    accessPolicyElement.appendChild(accessRuleNode);
-                }
-            });
-
-            return accessPolicyElement;
-          },
-
-          /**
-          * Removes access rules that grant public access and sets an access rule
-          * that denies public read.
-          */
-          makePrivate: function(){
-
-            var alreadyPrivate = false;
-
-            //Find the public access rules and remove them
-            this.each( function(accessRule){
-
-              if( typeof accessRule === "undefined" )
-                return;
-
-              //If the access rule subject is `public` and they are given any kind of access,
-              if( accessRule.get("subject") == "public" &&
-                (accessRule.get("read") || accessRule.get("write") || accessRule.get("changePermission")) ){
-
-                  //Remove this AccessRule model from the collection
-                  this.remove(accessRule);
-
-              }
-
-            }, this);
+            newLength++;
 
+            //Reset all the values first
+            accessRuleModel.set(accessRuleModel.defaults());
+            //Parse the AccessRule model and update the model attributes
+            accessRuleModel.set(accessRuleModel.parse(accessRuleXML));
+            //Save a reference to the DataONEObbject
+            accessRuleModel.set("dataONEObject", this.dataONEObject);
           },
+          this,
+        );
+
+        //If there are more AccessRules in this collection than were in the
+        // system metadata XML, then remove the extras
+        if (originalLength > newLength) {
+          for (var i = 0; i < originalLength - newLength; i++) {
+            this.pop();
+          }
+        }
+      },
 
-          /**
-          * Removes any AccessRule that denies public read and adds an AccessRule
-          * that allows public read
-          */
-          makePublic: function(){
-
-            var alreadyPublic = false;
-
-            //Find any public read rule and set read=true
-            this.each( function(accessRule){
-
-              if( typeof accessRule === "undefined" )
-                return;
-
-              //If the access rule subject is `public` and they are denied read access
-              if( accessRule.get("subject") == "public" ){
-
-                  //Remove this AccessRule model from the collection
-                  accessRule.set("read", true);
-                  alreadyPublic = true;
-
-              }
-
-            }, this);
-
-            //If this policy does not already allow the public read access, then add that rule
-            if( !alreadyPublic ){
-              //Create an access rule that allows public read
-              var publicAllow = new AccessRule({
-                subject: "public",
-                read: true,
-                dataONEObject: this.dataONEObject
-              });
-              //Add this access rule
-              this.add(publicAllow);
-            }
-
+      /**
+       * Creates AccessRule member models from the `defaultAccessPolicy`
+       * setting in the AppModel.
+       */
+      createDefaultPolicy: function () {
+        //For each access policy in the AppModel, create an AccessRule model
+        _.each(
+          MetacatUI.appModel.get("defaultAccessPolicy"),
+          function (accessRule) {
+            accessRule.dataONEObject = this.dataONEObject;
+
+            this.add(new AccessRule(accessRule));
           },
+          this,
+        );
+      },
 
-          /**
-          * Returns true if this access policy specifies that it is accessible to
-          * the public in any way
-          * @return {boolean}
-          */
-          isPublic: function(){
-
-            var isPublic = false;
-
-            this.each(function(accessRule){
-
-              if( accessRule.get("subject") == "public" &&
-                (accessRule.get("read") || accessRule.get("write") || accessRule.get("changePermission")) ){
-                isPublic = true;
-              }
-
-            });
-
-            return isPublic;
+      /**
+       * Copies all the AccessRules from the given AccessPolicy and replaces this AccessPolicy
+       * @param {AccessPolicy} otherAccessPolicy
+       * @fires Backbone.Collection#reset
+       * @since 2.15.0
+       */
+      copyAccessPolicy: function (otherAccessPolicy) {
+        try {
+          let accessRules = [];
+
+          //For each access policy in the AppModel, create an AccessRule model
+          otherAccessPolicy.each(function (accessRule) {
+            //Convert the AccessRule model to JSON and update the reference to the DataONEObject
+            let accessRuleJSON = accessRule.toJSON();
+            accessRuleJSON.dataONEObject = this.dataONEObject;
+            accessRules.push(accessRuleJSON);
+          }, this);
+
+          //Reset the Collection with these AccessRules
+          this.reset(accessRules);
+        } catch (e) {
+          console.error(e);
+        }
+      },
 
-          },
+      /**
+       * Creates an access policy XML from the values set on the member
+       * AccessRule models.
+       * @returns {object} A XML object of the access policy or null if empty
+       */
+      serialize: function () {
+        if (this.length === 0) {
+          return null;
+        }
+
+        // Create the access policy node which will contain all the rules
+        var accessPolicyElement = document.createElement("accesspolicy");
+
+        // Serialize each AccessRule member model and add to the policy DOM
+        this.each(function (accessRule) {
+          var accessRuleNode = accessRule.serialize();
+          if (accessRuleNode) {
+            accessPolicyElement.appendChild(accessRuleNode);
+          }
+        });
 
-          /**
-          * Checks if the current user is authorized to perform the given action
-          * based on the current access rules in this collection
-          *
-          * @param {string} action - The action to check authorization for. Can
-          *   be either `read`, `write`, or `changePermission`
-          * @return {boolean} - Returns true is the user can perform this action,
-          *   false if not.
-          */
-          isAuthorized: function(action){
-            if( typeof action == "undefined" || !action )
-              return false;
-
-            //Get the access rules for the user's subject or groups
-            var allSubjects = [];
-            if( !MetacatUI.appUserModel.get("loggedIn") )
-              allSubjects = "public";
-            else{
-
-              allSubjects = _.union(MetacatUI.appUserModel.get("identities"),
-                                    _.pluck(MetacatUI.appUserModel.get("isMemberOf"), "groupId"),
-                                    [MetacatUI.appUserModel.get("username")]);
+        return accessPolicyElement;
+      },
 
+      /**
+       * Removes access rules that grant public access and sets an access rule
+       * that denies public read.
+       */
+      makePrivate: function () {
+        var alreadyPrivate = false;
+
+        //Find the public access rules and remove them
+        this.each(function (accessRule) {
+          if (typeof accessRule === "undefined") return;
+
+          //If the access rule subject is `public` and they are given any kind of access,
+          if (
+            accessRule.get("subject") == "public" &&
+            (accessRule.get("read") ||
+              accessRule.get("write") ||
+              accessRule.get("changePermission"))
+          ) {
+            //Remove this AccessRule model from the collection
+            this.remove(accessRule);
+          }
+        }, this);
+      },
 
-            }
+      /**
+       * Removes any AccessRule that denies public read and adds an AccessRule
+       * that allows public read
+       */
+      makePublic: function () {
+        var alreadyPublic = false;
+
+        //Find any public read rule and set read=true
+        this.each(function (accessRule) {
+          if (typeof accessRule === "undefined") return;
+
+          //If the access rule subject is `public` and they are denied read access
+          if (accessRule.get("subject") == "public") {
+            //Remove this AccessRule model from the collection
+            accessRule.set("read", true);
+            alreadyPublic = true;
+          }
+        }, this);
+
+        //If this policy does not already allow the public read access, then add that rule
+        if (!alreadyPublic) {
+          //Create an access rule that allows public read
+          var publicAllow = new AccessRule({
+            subject: "public",
+            read: true,
+            dataONEObject: this.dataONEObject,
+          });
+          //Add this access rule
+          this.add(publicAllow);
+        }
+      },
 
-            //Find the access rules that match the given action and user subjects
-            var applicableRules = this.filter(function(accessRule){
-              if( accessRule.get(action) && _.contains(allSubjects, accessRule.get("subject")) ) {
-                return true;
-              }
-            }, this);
+      /**
+       * Returns true if this access policy specifies that it is accessible to
+       * the public in any way
+       * @return {boolean}
+       */
+      isPublic: function () {
+        var isPublic = false;
+
+        this.each(function (accessRule) {
+          if (
+            accessRule.get("subject") == "public" &&
+            (accessRule.get("read") ||
+              accessRule.get("write") ||
+              accessRule.get("changePermission"))
+          ) {
+            isPublic = true;
+          }
+        });
 
-            if( applicableRules.length )
-              return true;
-            else if( _.contains(allSubjects, this.dataONEObject.get("rightsHolder")) )
-              return true;
-            else
-              return false;
+        return isPublic;
+      },
 
-          },
+      /**
+       * Checks if the current user is authorized to perform the given action
+       * based on the current access rules in this collection
+       *
+       * @param {string} action - The action to check authorization for. Can
+       *   be either `read`, `write`, or `changePermission`
+       * @return {boolean} - Returns true is the user can perform this action,
+       *   false if not.
+       */
+      isAuthorized: function (action) {
+        if (typeof action == "undefined" || !action) return false;
+
+        //Get the access rules for the user's subject or groups
+        var allSubjects = [];
+        if (!MetacatUI.appUserModel.get("loggedIn")) allSubjects = "public";
+        else {
+          allSubjects = _.union(
+            MetacatUI.appUserModel.get("identities"),
+            _.pluck(MetacatUI.appUserModel.get("isMemberOf"), "groupId"),
+            [MetacatUI.appUserModel.get("username")],
+          );
+        }
+
+        //Find the access rules that match the given action and user subjects
+        var applicableRules = this.filter(function (accessRule) {
+          if (
+            accessRule.get(action) &&
+            _.contains(allSubjects, accessRule.get("subject"))
+          ) {
+            return true;
+          }
+        }, this);
 
-          /**
-          * Checks if the user is authorized to update the system metadata.
-          * Updates to system metadata will fail if the user doesn't have changePermission permission,
-          * *unless* the user is performing an update() at the same time and has `write` permission
-          * @returns {boolean}
-          * @since 2.15.0
-          */
-          isAuthorizedUpdateSysMeta: function(){
-            try{
-              //Yes, if the user has changePermission
-              if( this.isAuthorized("changePermission")  ){
-                return true;
-              }
-              //Yes, if the user just uploaded this object and is saving it for the first time
-              else if( this.isAuthorized("write") && this.dataONEObject.isNew() ){
-                return true;
-              }
-              else{
-                return false;
-              }
-            }
-            catch(e){
-              console.error("Failed to determing authorization: " , e);
-              return false;
-            }
-          },
+        if (applicableRules.length) return true;
+        else if (
+          _.contains(allSubjects, this.dataONEObject.get("rightsHolder"))
+        )
+          return true;
+        else return false;
+      },
 
-          /**
-          * Gets the subject info for all of the subjects in this access policy.
-          * Sets the subject info on each corresponding model.
-          */
-          getSubjectInfo: function(){
+      /**
+       * Checks if the user is authorized to update the system metadata.
+       * Updates to system metadata will fail if the user doesn't have changePermission permission,
+       * *unless* the user is performing an update() at the same time and has `write` permission
+       * @returns {boolean}
+       * @since 2.15.0
+       */
+      isAuthorizedUpdateSysMeta: function () {
+        try {
+          //Yes, if the user has changePermission
+          if (this.isAuthorized("changePermission")) {
+            return true;
+          }
+          //Yes, if the user just uploaded this object and is saving it for the first time
+          else if (this.isAuthorized("write") && this.dataONEObject.isNew()) {
+            return true;
+          } else {
+            return false;
+          }
+        } catch (e) {
+          console.error("Failed to determing authorization: ", e);
+          return false;
+        }
+      },
 
-            //If there are more than 5 subjects in the access policy, then get the entire list of subjects in the DataONE/CN system
-          /*  if( this.length > 5 ){
+      /**
+       * Gets the subject info for all of the subjects in this access policy.
+       * Sets the subject info on each corresponding model.
+       */
+      getSubjectInfo: function () {
+        //If there are more than 5 subjects in the access policy, then get the entire list of subjects in the DataONE/CN system
+        /*  if( this.length > 5 ){
               //TODO: Get everything from the /accounts endpoint
             }
             */
-            //If there are less than 5, then send individual requests to get the subject info
-            this.invoke("getSubjectInfo");
-
-          },
-
-          /**
-          * Remove the given AccessRule from this AccessPolicy
-          * @param {AccessRule} accessRule - The AccessRule model to remove
-          */
-          removeAccessRule: function(accessRule){
+        //If there are less than 5, then send individual requests to get the subject info
+        this.invoke("getSubjectInfo");
+      },
 
-            this.remove(accessRule);
-
-          },
-
-          /**
-          * Checks if there is at least one AccessRule with changePermission permission
-          * in this AccessPolicy.
-          * @returns {boolean}
-          */
-          hasOwner: function(){
-            try{
-              var owners = this.where({ changePermission: true });
-
-              //Check if there are any other subjects with ownership levels
-              if( !owners || owners.length == 0 ){
+      /**
+       * Remove the given AccessRule from this AccessPolicy
+       * @param {AccessRule} accessRule - The AccessRule model to remove
+       */
+      removeAccessRule: function (accessRule) {
+        this.remove(accessRule);
+      },
 
-                //If there is a rightsHolder, that counts as an owner
-              /*  if( this.dataONEObject && this.dataONEObject.get("rightsHolder") ){
+      /**
+       * Checks if there is at least one AccessRule with changePermission permission
+       * in this AccessPolicy.
+       * @returns {boolean}
+       */
+      hasOwner: function () {
+        try {
+          var owners = this.where({ changePermission: true });
+
+          //Check if there are any other subjects with ownership levels
+          if (!owners || owners.length == 0) {
+            //If there is a rightsHolder, that counts as an owner
+            /*  if( this.dataONEObject && this.dataONEObject.get("rightsHolder") ){
                   return true;
                 }
                 */
-                return false;
-              }
-              else{
-                return true;
-              }
-            }
-            catch(e){
-              console.error("Error getting the owners of this AccessPolicy: ", e);
-            }
-          },
-
-          replaceRightsHolder: function(){
-            var owner = this.findWhere({ changePermission: true });
-
-            //Make sure the owner model was found
-            if( !owner ){
-              return;
-            }
-
-            //Set this other owner as the rightsHolder
-            this.dataONEObject.set("rightsHolder", owner.get("subject"));
-
-            //Remove them as an AccessRule in the AccessPolicy
-            this.remove(owner);
+            return false;
+          } else {
+            return true;
           }
-
-      });
-
-      return AccessPolicy;
-
-    });
+        } catch (e) {
+          console.error("Error getting the owners of this AccessPolicy: ", e);
+        }
+      },
+
+      replaceRightsHolder: function () {
+        var owner = this.findWhere({ changePermission: true });
+
+        //Make sure the owner model was found
+        if (!owner) {
+          return;
+        }
+
+        //Set this other owner as the rightsHolder
+        this.dataONEObject.set("rightsHolder", owner.get("subject"));
+
+        //Remove them as an AccessRule in the AccessPolicy
+        this.remove(owner);
+      },
+    },
+  );
+
+  return AccessPolicy;
+});
 
diff --git a/docs/docs/src_js_collections_Citations.js.html b/docs/docs/src_js_collections_Citations.js.html index e18951a46..b7280caec 100644 --- a/docs/docs/src_js_collections_Citations.js.html +++ b/docs/docs/src_js_collections_Citations.js.html @@ -44,45 +44,45 @@

Source: src/js/collections/Citations.js

-
/* global define */
-"use strict";
-
-define(['jquery', 'underscore', 'backbone', 'models/CitationModel'],
-    function($, _, Backbone, CitationModel) {
-
-    /**
-     * @class Citations
-     * @classdesc Citations represents the Citations list
-     * found at https://app.swaggerhub.com/apis/nenuji/data-metrics/1.0.0.3.
-     * For details regarding a single Citation Entity, refer `models/CitationModel`
-     * @classcategory Collections
-     * @name Citations
-     * @extends Backbone.Collection
-     * @constructor
-     */
-    var Citations = Backbone.Collection.extend(
-      /** @lends Citations.prototype */{
-
-        model: function (attrs, options) {
-            // We use the inline require here in addition to the define above to
-            // avoid an issue caused by the circular dependency between
-            // CitationModel and Citations
-            var CitationModel = require('models/CitationModel');
-            return new CitationModel(attrs, options)
-        },
-
-        //The name of this type of collection
-        type: "Citations",
-
-
-        // Used for sorting the year in the reverse Chronological order
-        comparator : function(model) {
-            return -model.get("year_of_publishing"); // Note the minus!
-        }
-
-    });
-
-    return Citations;
+            
"use strict";
+
+define(["jquery", "underscore", "backbone", "models/CitationModel"], function (
+  $,
+  _,
+  Backbone,
+  CitationModel,
+) {
+  /**
+   * @class Citations
+   * @classdesc Citations represents the Citations list
+   * found at https://app.swaggerhub.com/apis/nenuji/data-metrics/1.0.0.3.
+   * For details regarding a single Citation Entity, refer `models/CitationModel`
+   * @classcategory Collections
+   * @name Citations
+   * @extends Backbone.Collection
+   * @constructor
+   */
+  var Citations = Backbone.Collection.extend(
+    /** @lends Citations.prototype */ {
+      model: function (attrs, options) {
+        // We use the inline require here in addition to the define above to
+        // avoid an issue caused by the circular dependency between
+        // CitationModel and Citations
+        var CitationModel = require("models/CitationModel");
+        return new CitationModel(attrs, options);
+      },
+
+      //The name of this type of collection
+      type: "Citations",
+
+      // Used for sorting the year in the reverse Chronological order
+      comparator: function (model) {
+        return -model.get("year_of_publishing"); // Note the minus!
+      },
+    },
+  );
+
+  return Citations;
 });
 
diff --git a/docs/docs/src_js_collections_DataPackage.js.html b/docs/docs/src_js_collections_DataPackage.js.html index 21c18d3da..f8dda6c89 100644 --- a/docs/docs/src_js_collections_DataPackage.js.html +++ b/docs/docs/src_js_collections_DataPackage.js.html @@ -44,35 +44,34 @@

Source: src/js/collections/DataPackage.js

-
/* global define */
-"use strict";
+            
"use strict";
 
 define([
-    "jquery",
-    "underscore",
-    "backbone",
-    "rdflib",
-    "uuid",
-    "md5",
-    "collections/SolrResults",
-    "models/filters/Filter",
-    "models/DataONEObject",
-    "models/metadata/ScienceMetadata",
-    "models/metadata/eml211/EML211",
+  "jquery",
+  "underscore",
+  "backbone",
+  "rdflib",
+  "uuid",
+  "md5",
+  "collections/SolrResults",
+  "models/filters/Filter",
+  "models/DataONEObject",
+  "models/metadata/ScienceMetadata",
+  "models/metadata/eml211/EML211",
 ], function (
-    $,
-    _,
-    Backbone,
-    rdf,
-    uuid,
-    md5,
-    SolrResults,
-    Filter,
-    DataONEObject,
-    ScienceMetadata,
-    EML211
+  $,
+  _,
+  Backbone,
+  rdf,
+  uuid,
+  md5,
+  SolrResults,
+  Filter,
+  DataONEObject,
+  ScienceMetadata,
+  EML211,
 ) {
-    /**
+  /**
        * @class DataPackage
        * @classdesc A DataPackage represents a hierarchical collection of
        packages, metadata, and data objects, modeling an OAI-ORE RDF graph.
@@ -82,1101 +81,1052 @@ 

Source: src/js/collections/DataPackage.js

* @extends Backbone.Collection * @constructor */ - var DataPackage = Backbone.Collection.extend( - /** @lends DataPackage.prototype */ { - /** - * The name of this type of collection - * @type {string} - */ - type: "DataPackage", - - /** - * The package identifier - * @type {string} - */ - id: null, - - /** - * The type of the object (DataPackage, Metadata, Data) - * Simple queue to enqueue file transfers. Use push() and shift() - * to add and remove items. If this gets to large/slow, possibly - * switch to http://code.stephenmorley.org/javascript/queues/ - * @type {DataPackage|Metadata|Data[]} - */ - transferQueue: [], - - /** A flag ued for the package's edit status. Can be - * set to false to 'lock' the package - * @type {boolean} - */ - editable: true, - - /** - * The RDF graph representing this data package - * @type {RDFGraph} - */ - dataPackageGraph: null, - - /** - * A DataONEObject representing the resource map itself - * @type {DataONEObject} - */ - packageModel: null, - - /** The science data identifiers associated with this - * data package (from cito:documents), mapped to the science metadata - * identifier that documents it - * Not to be changed after initial fetch - this is to keep track of the relationships in their original state - * @type {object} - */ - originalIsDocBy: {}, - - /** An array of ids that are aggregated in the resource map on the server. - * Taken from the original RDF XML that was fetched from the server. - * Used for comparing the original aggregation with the aggregation of this collection. - * @type {string[]} - */ - originalMembers: [], - - /** - * Keep the collection sorted by model "sortOrder". The three model types are ordered as: - * Metadata: 1 - * Data: 2 - * DataPackage: 3 - * See getMember(). We do this so that Metadata get rendered first, and Data are - * rendered as DOM siblings of the Metadata rows of the DataPackage table. - * @type {string} - */ - comparator: "sortOrder", - - /** - * The nesting level in a data package hierarchy - * @type {number} - */ - nodeLevel: 0, - - /** - * The SolrResults collection associated with this DataPackage. - * This can be used to fetch the package from Solr by passing the 'fromIndex' option - * to fetch(). - * @type {SolrResults} - */ - solrResults: new SolrResults(), - - /** - * A Filter model that should filter the Solr index for only the - * objects aggregated by this package. - * @type {Filter} - */ - filterModel: null, - - /** Define the namespaces used in the RDF XML - * @type {object} - */ - namespaces: { - RDF: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - FOAF: "http://xmlns.com/foaf/0.1/", - OWL: "http://www.w3.org/2002/07/owl#", - DC: "http://purl.org/dc/elements/1.1/", - ORE: "http://www.openarchives.org/ore/terms/", - DCTERMS: "http://purl.org/dc/terms/", - CITO: "http://purl.org/spar/cito/", - XSD: "http://www.w3.org/2001/XMLSchema#", - PROV: "http://www.w3.org/ns/prov#", - PROVONE: "http://purl.dataone.org/provone/2015/01/15/ontology#", - }, + var DataPackage = Backbone.Collection.extend( + /** @lends DataPackage.prototype */ { + /** + * The name of this type of collection + * @type {string} + */ + type: "DataPackage", - sources: [], - derivations: [], - provenanceFlag: null, - sourcePackages: [], - derivationPackages: [], - relatedModels: [], + /** + * The package identifier + * @type {string} + */ + id: null, + + /** + * The type of the object (DataPackage, Metadata, Data) + * Simple queue to enqueue file transfers. Use push() and shift() + * to add and remove items. If this gets to large/slow, possibly + * switch to http://code.stephenmorley.org/javascript/queues/ + * @type {DataPackage|Metadata|Data[]} + */ + transferQueue: [], - /** - * Contains provenance relationships added or deleted to this DataONEObject. - * Each entry is [operation ('add' or 'delete'), prov field name, object id], i.e. ['add', 'prov_used', 'urn:uuid:5678'] - */ - provEdits: [], + /** A flag ued for the package's edit status. Can be + * set to false to 'lock' the package + * @type {boolean} + */ + editable: true, - /** - * The number of models that have been updated during the current save(). - * This is reset to zero after the current save() is complete. - */ - numSaves: 0, + /** + * The RDF graph representing this data package + * @type {RDFGraph} + */ + dataPackageGraph: null, - // Constructor: Initialize a new DataPackage - initialize: function (models, options) { - if (typeof options == "undefined") var options = {}; + /** + * A DataONEObject representing the resource map itself + * @type {DataONEObject} + */ + packageModel: null, - // Create an rdflib reference - this.rdf = rdf; + /** The science data identifiers associated with this + * data package (from cito:documents), mapped to the science metadata + * identifier that documents it + * Not to be changed after initial fetch - this is to keep track of the relationships in their original state + * @type {object} + */ + originalIsDocBy: {}, - // Create an initial RDF graph - this.dataPackageGraph = this.rdf.graph(); + /** An array of ids that are aggregated in the resource map on the server. + * Taken from the original RDF XML that was fetched from the server. + * Used for comparing the original aggregation with the aggregation of this collection. + * @type {string[]} + */ + originalMembers: [], + + /** + * Keep the collection sorted by model "sortOrder". The three model types are ordered as: + * Metadata: 1 + * Data: 2 + * DataPackage: 3 + * See getMember(). We do this so that Metadata get rendered first, and Data are + * rendered as DOM siblings of the Metadata rows of the DataPackage table. + * @type {string} + */ + comparator: "sortOrder", - //Set the id or create a new one - this.id = options.id || "resource_map_urn:uuid:" + uuid.v4(); + /** + * The nesting level in a data package hierarchy + * @type {number} + */ + nodeLevel: 0, - let packageModelAttrs = options.packageModelAttrs || {}; + /** + * The SolrResults collection associated with this DataPackage. + * This can be used to fetch the package from Solr by passing the 'fromIndex' option + * to fetch(). + * @type {SolrResults} + */ + solrResults: new SolrResults(), - if (typeof options.packageModel !== "undefined") { - // use the given package model - this.packageModel = new DataONEObject(options.packageModel); - } else { - // Create a DataONEObject to represent this resource map - this.packageModel = new DataONEObject( - _.extend(packageModelAttrs, { - formatType: "RESOURCE", - type: "DataPackage", - formatId: "http://www.openarchives.org/ore/terms", - childPackages: {}, - id: this.id, - latestVersion: this.id, - }) - ); - } + /** + * A Filter model that should filter the Solr index for only the + * objects aggregated by this package. + * @type {Filter} + */ + filterModel: null, - this.id = this.packageModel.id; + /** Define the namespaces used in the RDF XML + * @type {object} + */ + namespaces: { + RDF: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + FOAF: "http://xmlns.com/foaf/0.1/", + OWL: "http://www.w3.org/2002/07/owl#", + DC: "http://purl.org/dc/elements/1.1/", + ORE: "http://www.openarchives.org/ore/terms/", + DCTERMS: "http://purl.org/dc/terms/", + CITO: "http://purl.org/spar/cito/", + XSD: "http://www.w3.org/2001/XMLSchema#", + PROV: "http://www.w3.org/ns/prov#", + PROVONE: "http://purl.dataone.org/provone/2015/01/15/ontology#", + }, + + sources: [], + derivations: [], + provenanceFlag: null, + sourcePackages: [], + derivationPackages: [], + relatedModels: [], + + /** + * Contains provenance relationships added or deleted to this DataONEObject. + * Each entry is [operation ('add' or 'delete'), prov field name, object id], i.e. ['add', 'prov_used', 'urn:uuid:5678'] + */ + provEdits: [], - //Create a Filter for this DataPackage using the id - this.filterModel = new Filter({ - fields: ["resourceMap"], - values: [this.id], - matchSubstring: false, - }); - //If the id is ever changed, update the id in the Filter - this.listenTo(this.packageModel, "change:id", function () { - this.filterModel.set("values", [ - this.packageModel.get("id"), - ]); - }); + /** + * The number of models that have been updated during the current save(). + * This is reset to zero after the current save() is complete. + */ + numSaves: 0, + + // Constructor: Initialize a new DataPackage + initialize: function (models, options) { + if (typeof options == "undefined") var options = {}; + + // Create an rdflib reference + this.rdf = rdf; + + // Create an initial RDF graph + this.dataPackageGraph = this.rdf.graph(); + + //Set the id or create a new one + this.id = options.id || "resource_map_urn:uuid:" + uuid.v4(); + + let packageModelAttrs = options.packageModelAttrs || {}; + + if (typeof options.packageModel !== "undefined") { + // use the given package model + this.packageModel = new DataONEObject(options.packageModel); + } else { + // Create a DataONEObject to represent this resource map + this.packageModel = new DataONEObject( + _.extend(packageModelAttrs, { + formatType: "RESOURCE", + type: "DataPackage", + formatId: "http://www.openarchives.org/ore/terms", + childPackages: {}, + id: this.id, + latestVersion: this.id, + }), + ); + } - this.on("add", this.handleAdd); - this.on("add", this.triggerComplete); - this.on("successSaving", this.updateRelationships); + this.id = this.packageModel.id; + + //Create a Filter for this DataPackage using the id + this.filterModel = new Filter({ + fields: ["resourceMap"], + values: [this.id], + matchSubstring: false, + }); + //If the id is ever changed, update the id in the Filter + this.listenTo(this.packageModel, "change:id", function () { + this.filterModel.set("values", [this.packageModel.get("id")]); + }); + + this.on("add", this.handleAdd); + this.on("add", this.triggerComplete); + this.on("successSaving", this.updateRelationships); + + return this; + }, + + // Build the DataPackage URL based on the MetacatUI.appModel.objectServiceUrl + // and id or seriesid + url: function (options) { + if (options && options.update) { + return ( + MetacatUI.appModel.get("objectServiceUrl") + + (encodeURIComponent(this.packageModel.get("oldPid")) || + encodeURIComponent(this.packageModel.get("seriesid"))) + ); + } else { + //URL encode the id or seriesId + var encodedId = + encodeURIComponent(this.packageModel.get("id")) || + encodeURIComponent(this.packageModel.get("seriesid")); + //Use the object service URL if it is available (when pointing to a MN) + if (MetacatUI.appModel.get("objectServiceUrl")) { + return MetacatUI.appModel.get("objectServiceUrl") + encodedId; + } + //Otherwise, use the resolve service URL (when pointing to a CN) + else { + return MetacatUI.appModel.get("resolveServiceUrl") + encodedId; + } + } + }, - return this; - }, + /* + * The DataPackage collection stores DataPackages and + * DataONEObjects, including Metadata and Data objects. + * Return the correct model based on the type + */ + model: function (attrs, options) { + switch (attrs.formatid) { + case "http://www.openarchives.org/ore/terms": + return new DataPackage(null, { packageModel: attrs }); // TODO: is this correct? - // Build the DataPackage URL based on the MetacatUI.appModel.objectServiceUrl - // and id or seriesid - url: function (options) { - if (options && options.update) { - return ( - MetacatUI.appModel.get("objectServiceUrl") + - (encodeURIComponent(this.packageModel.get("oldPid")) || - encodeURIComponent( - this.packageModel.get("seriesid") - )) - ); - } else { - //URL encode the id or seriesId - var encodedId = - encodeURIComponent(this.packageModel.get("id")) || - encodeURIComponent(this.packageModel.get("seriesid")); - //Use the object service URL if it is available (when pointing to a MN) - if (MetacatUI.appModel.get("objectServiceUrl")) { - return ( - MetacatUI.appModel.get("objectServiceUrl") + - encodedId - ); - } - //Otherwise, use the resolve service URL (when pointing to a CN) - else { - return ( - MetacatUI.appModel.get("resolveServiceUrl") + - encodedId - ); - } - } - }, + case "eml://ecoinformatics.org/eml-2.0.0": + return new EML211(attrs, options); - /* - * The DataPackage collection stores DataPackages and - * DataONEObjects, including Metadata and Data objects. - * Return the correct model based on the type - */ - model: function (attrs, options) { - switch (attrs.formatid) { - case "http://www.openarchives.org/ore/terms": - return new DataPackage(null, { packageModel: attrs }); // TODO: is this correct? + case "eml://ecoinformatics.org/eml-2.0.1": + return new EML211(attrs, options); - case "eml://ecoinformatics.org/eml-2.0.0": - return new EML211(attrs, options); + case "eml://ecoinformatics.org/eml-2.1.0": + return new EML211(attrs, options); - case "eml://ecoinformatics.org/eml-2.0.1": - return new EML211(attrs, options); + case "eml://ecoinformatics.org/eml-2.1.1": + return new EML211(attrs, options); - case "eml://ecoinformatics.org/eml-2.1.0": - return new EML211(attrs, options); + case "eml://ecoinformatics.org/eml-2.1.1": + return new EML211(attrs, options); - case "eml://ecoinformatics.org/eml-2.1.1": - return new EML211(attrs, options); + case "-//ecoinformatics.org//eml-access-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "eml://ecoinformatics.org/eml-2.1.1": - return new EML211(attrs, options); + case "-//ecoinformatics.org//eml-access-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-access-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-attribute-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-access-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-attribute-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-attribute-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-constraint-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-attribute-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-constraint-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-constraint-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-coverage-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-constraint-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-coverage-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-coverage-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-dataset-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-coverage-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-dataset-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-dataset-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-distribution-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-dataset-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-distribution-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-distribution-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-entity-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-distribution-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-entity-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-entity-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-literature-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-entity-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-literature-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-literature-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-party-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-literature-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-party-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-party-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-physical-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-party-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-physical-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-physical-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-project-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-physical-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-project-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-project-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-protocol-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-project-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-protocol-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-protocol-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-resource-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-protocol-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-resource-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-resource-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-software-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-resource-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-software-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "FGDC-STD-001-1998": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "FGDC-STD-001.1-1999": + return new ScienceMetadata(attrs, options); - case "FGDC-STD-001-1998": - return new ScienceMetadata(attrs, options); + case "FGDC-STD-001.2-1999": + return new ScienceMetadata(attrs, options); - case "FGDC-STD-001.1-1999": - return new ScienceMetadata(attrs, options); + case "INCITS-453-2009": + return new ScienceMetadata(attrs, options); - case "FGDC-STD-001.2-1999": - return new ScienceMetadata(attrs, options); + case "ddi:codebook:2_5": + return new ScienceMetadata(attrs, options); - case "INCITS-453-2009": - return new ScienceMetadata(attrs, options); + case "http://datacite.org/schema/kernel-3.0": + return new ScienceMetadata(attrs, options); - case "ddi:codebook:2_5": - return new ScienceMetadata(attrs, options); + case "http://datacite.org/schema/kernel-3.1": + return new ScienceMetadata(attrs, options); - case "http://datacite.org/schema/kernel-3.0": - return new ScienceMetadata(attrs, options); + case "http://datadryad.org/profile/v3.1": + return new ScienceMetadata(attrs, options); - case "http://datacite.org/schema/kernel-3.1": - return new ScienceMetadata(attrs, options); + case "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd": + return new ScienceMetadata(attrs, options); - case "http://datadryad.org/profile/v3.1": - return new ScienceMetadata(attrs, options); + case "http://ns.dataone.org/metadata/schema/onedcx/v1.0": + return new ScienceMetadata(attrs, options); - case "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd": - return new ScienceMetadata(attrs, options); + case "http://purl.org/dryad/terms/": + return new ScienceMetadata(attrs, options); - case "http://ns.dataone.org/metadata/schema/onedcx/v1.0": - return new ScienceMetadata(attrs, options); + case "http://purl.org/ornl/schema/mercury/terms/v1.0": + return new ScienceMetadata(attrs, options); - case "http://purl.org/dryad/terms/": - return new ScienceMetadata(attrs, options); + case "http://rs.tdwg.org/dwc/xsd/simpledarwincore/": + return new ScienceMetadata(attrs, options); - case "http://purl.org/ornl/schema/mercury/terms/v1.0": - return new ScienceMetadata(attrs, options); + case "http://www.cuahsi.org/waterML/1.0/": + return new ScienceMetadata(attrs, options); - case "http://rs.tdwg.org/dwc/xsd/simpledarwincore/": - return new ScienceMetadata(attrs, options); + case "http://www.cuahsi.org/waterML/1.1/": + return new ScienceMetadata(attrs, options); - case "http://www.cuahsi.org/waterML/1.0/": - return new ScienceMetadata(attrs, options); + case "http://www.esri.com/metadata/esriprof80.dtd": + return new ScienceMetadata(attrs, options); - case "http://www.cuahsi.org/waterML/1.1/": - return new ScienceMetadata(attrs, options); + case "http://www.icpsr.umich.edu/DDI": + return new ScienceMetadata(attrs, options); - case "http://www.esri.com/metadata/esriprof80.dtd": - return new ScienceMetadata(attrs, options); + case "http://www.isotc211.org/2005/gmd": + return new ScienceMetadata(attrs, options); - case "http://www.icpsr.umich.edu/DDI": - return new ScienceMetadata(attrs, options); + case "http://www.isotc211.org/2005/gmd-noaa": + return new ScienceMetadata(attrs, options); - case "http://www.isotc211.org/2005/gmd": - return new ScienceMetadata(attrs, options); + case "http://www.loc.gov/METS/": + return new ScienceMetadata(attrs, options); - case "http://www.isotc211.org/2005/gmd-noaa": - return new ScienceMetadata(attrs, options); + case "http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2": + return new ScienceMetadata(attrs, options); - case "http://www.loc.gov/METS/": - return new ScienceMetadata(attrs, options); + default: + return new DataONEObject(attrs, options); + } + }, + + /** + * Overload fetch calls for a DataPackage + * + * @param {Object} [options] - Optional options for this fetch that get sent with the XHR request + * @property {boolean} fetchModels - If false, this fetch will not fetch + * each model in the collection. It will only get the resource map object. + * @property {boolean} fromIndex - If true, the collection will be fetched from Solr rather than + * fetching the system metadata of each model. Useful when you only need to retrieve limited information about + * each package member. Set query-specific parameters on the `solrResults` SolrResults set on this collection. + */ + fetch: function (options) { + // Fetch the system metadata for this resource map + this.packageModel.fetch(); + + if (typeof options == "object") { + // If the fetchModels property is set to false, + if (options.fetchModels === false) { + // Save the property to the Collection itself so it is accessible in other functions + this.fetchModels = false; + // Remove the property from the options Object since we don't want to send it with the XHR + delete options.fetchModels; + this.once("reset", this.triggerComplete); + } + // If the fetchFromIndex property is set to true + else if (options.fromIndex) { + this.fetchFromIndex(); + return; + } + } - case "http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2": - return new ScienceMetadata(attrs, options); + // Set some custom fetch options + var fetchOptions = _.extend({ dataType: "text" }, options); + + var thisPackage = this; + + // Function to retry fetching with user login details if the initial fetch fails + var retryFetch = function () { + // Add the authorization options + var authFetchOptions = _.extend( + fetchOptions, + MetacatUI.appUserModel.createAjaxSettings(), + ); + + // Fetch the resource map RDF XML with user login details + return Backbone.Collection.prototype.fetch + .call(thisPackage, authFetchOptions) + .fail(function () { + // trigger failure() + console.log("Fetch failed"); + + thisPackage.trigger("fetchFailed", thisPackage); + }); + }; + + // Fetch the resource map RDF XML + return Backbone.Collection.prototype.fetch + .call(this, fetchOptions) + .fail(function () { + // If the initial fetch fails, retry with user login details + return retryFetch(); + }); + }, + + /* + * Deserialize a Package from OAI-ORE RDF XML + */ + parse: function (response, options) { + //Save the raw XML in case it needs to be used later + this.objectXML = response; + + var RDF = this.rdf.Namespace(this.namespaces.RDF), + FOAF = this.rdf.Namespace(this.namespaces.FOAF), + OWL = this.rdf.Namespace(this.namespaces.OWL), + DC = this.rdf.Namespace(this.namespaces.DC), + ORE = this.rdf.Namespace(this.namespaces.ORE), + DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), + CITO = this.rdf.Namespace(this.namespaces.CITO), + PROV = this.rdf.Namespace(this.namespaces.PROV), + XSD = this.rdf.Namespace(this.namespaces.XSD); + + var memberStatements = [], + atLocationStatements = [], // array to store atLocation statements + memberURIParts, + memberPIDStr, + memberPID, + memberPIDs = [], + memberModel, + documentsStatements, + objectParts, + objectPIDStr, + objectPID, + objectAtLocationValue, + scimetaID, // documentor + scidataID, // documentee + models = []; // the models returned by parse() + + try { + //First, make sure we are only using one CN Base URL in the RDF or the RDF parsing will fail. + var cnResolveUrl = MetacatUI.appModel.get("resolveServiceUrl"); + + var cnURLs = _.uniq( + response.match( + /cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g, + ), + ); + if (cnURLs.length > 1) { + response = response.replace( + /cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g, + cnResolveUrl.substring(cnResolveUrl.indexOf("https://") + 8), + ); + } + + this.rdf.parse( + response, + this.dataPackageGraph, + this.url(), + "application/rdf+xml", + ); + + // List the package members + memberStatements = this.dataPackageGraph.statementsMatching( + undefined, + ORE("aggregates"), + undefined, + undefined, + ); + + // Get system metadata for each member to eval the formatId + _.each( + memberStatements, + function (memberStatement) { + memberURIParts = memberStatement.object.value.split("/"); + memberPIDStr = _.last(memberURIParts); + memberPID = decodeURIComponent(memberPIDStr); + + if (memberPID) memberPIDs.push(memberPID); + + //TODO: Test passing merge:true when adding a model and this if statement may not be necessary + //Create a DataONEObject model to represent this collection member and add to the collection + if (!_.contains(this.pluck("id"), memberPID)) { + memberModel = new DataONEObject({ + id: memberPID, + resourceMap: [this.packageModel.get("id")], + collections: [this], + }); - default: - return new DataONEObject(attrs, options); - } - }, + models.push(memberModel); + } + //If the model already exists, add this resource map ID to it's list of resource maps + else { + memberModel = this.get(memberPID); + models.push(memberModel); - /** - * Overload fetch calls for a DataPackage - * - * @param {Object} [options] - Optional options for this fetch that get sent with the XHR request - * @property {boolean} fetchModels - If false, this fetch will not fetch - * each model in the collection. It will only get the resource map object. - * @property {boolean} fromIndex - If true, the collection will be fetched from Solr rather than - * fetching the system metadata of each model. Useful when you only need to retrieve limited information about - * each package member. Set query-specific parameters on the `solrResults` SolrResults set on this collection. - */ - fetch: function(options) { - // Fetch the system metadata for this resource map - this.packageModel.fetch(); - - if(typeof options == "object") { - // If the fetchModels property is set to false, - if(options.fetchModels === false) { - // Save the property to the Collection itself so it is accessible in other functions - this.fetchModels = false; - // Remove the property from the options Object since we don't want to send it with the XHR - delete options.fetchModels; - this.once("reset", this.triggerComplete); - } - // If the fetchFromIndex property is set to true - else if(options.fromIndex) { - this.fetchFromIndex(); - return; - } - } - - // Set some custom fetch options - var fetchOptions = _.extend({ dataType: "text" }, options); - - var thisPackage = this; - - // Function to retry fetching with user login details if the initial fetch fails - var retryFetch = function() { - // Add the authorization options - var authFetchOptions = _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); - - // Fetch the resource map RDF XML with user login details - return Backbone.Collection.prototype.fetch.call(thisPackage, authFetchOptions) - .fail(function() { - // trigger failure() - console.log("Fetch failed"); - - thisPackage.trigger("fetchFailed", thisPackage); - }); - }; - - // Fetch the resource map RDF XML - return Backbone.Collection.prototype.fetch.call(this, fetchOptions) - .fail(function() { - // If the initial fetch fails, retry with user login details - return retryFetch(); - }); + var rMaps = memberModel.get("resourceMap"); + if ( + rMaps && + Array.isArray(rMaps) && + !_.contains(rMaps, this.packageModel.get("id")) + ) + rMaps.push(this.packageModel.get("id")); + else if (rMaps && !Array.isArray(rMaps)) + rMaps = [rMaps, this.packageModel.get("id")]; + else rMaps = [this.packageModel.get("id")]; + } }, + this, + ); + + //Save the list of original ids + this.originalMembers = memberPIDs; + + // Get the isDocumentedBy relationships + documentsStatements = this.dataPackageGraph.statementsMatching( + undefined, + CITO("documents"), + undefined, + undefined, + ); + + var sciMetaPids = []; + + _.each( + documentsStatements, + function (documentsStatement) { + // Extract and URI-decode the metadata pid + scimetaID = decodeURIComponent( + _.last(documentsStatement.subject.value.split("/")), + ); + + sciMetaPids.push(scimetaID); + + // Extract and URI-decode the data pid + scidataID = decodeURIComponent( + _.last(documentsStatement.object.value.split("/")), + ); + + // Store the isDocumentedBy relationship + if (typeof this.originalIsDocBy[scidataID] == "undefined") + this.originalIsDocBy[scidataID] = [scimetaID]; + else if ( + Array.isArray(this.originalIsDocBy[scidataID]) && + !_.contains(this.originalIsDocBy[scidataID], scimetaID) + ) + this.originalIsDocBy[scidataID].push(scimetaID); + else + this.originalIsDocBy[scidataID] = _.uniq([ + this.originalIsDocBy[scidataID], + scimetaID, + ]); + + //Find the model in this collection for this data object + //var dataObj = this.get(scidataID); + var dataObj = _.find(models, function (m) { + return m.get("id") == scidataID; + }); + + if (dataObj) { + //Get the isDocumentedBy field + var isDocBy = dataObj.get("isDocumentedBy"); + if ( + isDocBy && + Array.isArray(isDocBy) && + !_.contains(isDocBy, scimetaID) + ) + isDocBy.push(scimetaID); + else if (isDocBy && !Array.isArray(isDocBy)) + isDocBy = [isDocBy, scimetaID]; + else isDocBy = [scimetaID]; - /* - * Deserialize a Package from OAI-ORE RDF XML - */ - parse: function (response, options) { - //Save the raw XML in case it needs to be used later - this.objectXML = response; - - var RDF = this.rdf.Namespace(this.namespaces.RDF), - FOAF = this.rdf.Namespace(this.namespaces.FOAF), - OWL = this.rdf.Namespace(this.namespaces.OWL), - DC = this.rdf.Namespace(this.namespaces.DC), - ORE = this.rdf.Namespace(this.namespaces.ORE), - DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), - CITO = this.rdf.Namespace(this.namespaces.CITO), - PROV = this.rdf.Namespace(this.namespaces.PROV), - XSD = this.rdf.Namespace(this.namespaces.XSD); - - var memberStatements = [], - atLocationStatements = [], // array to store atLocation statements - memberURIParts, - memberPIDStr, - memberPID, - memberPIDs = [], - memberModel, - documentsStatements, - objectParts, - objectPIDStr, - objectPID, - objectAtLocationValue, - scimetaID, // documentor - scidataID, // documentee - models = []; // the models returned by parse() - - try { - //First, make sure we are only using one CN Base URL in the RDF or the RDF parsing will fail. - var cnResolveUrl = - MetacatUI.appModel.get("resolveServiceUrl"); - - var cnURLs = _.uniq( - response.match( - /cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g - ) - ); - if (cnURLs.length > 1) { - response = response.replace( - /cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g, - cnResolveUrl.substring( - cnResolveUrl.indexOf("https://") + 8 - ) - ); - } - - this.rdf.parse( - response, - this.dataPackageGraph, - this.url(), - "application/rdf+xml" - ); - - // List the package members - memberStatements = this.dataPackageGraph.statementsMatching( - undefined, - ORE("aggregates"), - undefined, - undefined - ); + //Set the isDocumentedBy field + dataObj.set("isDocumentedBy", isDocBy); + } + }, + this, + ); + + //Save the list of science metadata pids + this.sciMetaPids = sciMetaPids; + + // Parse atLocation + var atLocationObject = {}; + atLocationStatements = this.dataPackageGraph.statementsMatching( + undefined, + PROV("atLocation"), + undefined, + undefined, + ); + + const ref = this; + + // Get atLocation information for each statement in the resourceMap + _.each( + atLocationStatements, + function (atLocationStatement) { + objectParts = atLocationStatement.subject.value.split("/"); + objectPIDStr = _.last(objectParts); + objectPID = decodeURIComponent(objectPIDStr); + objectAtLocationValue = atLocationStatement.object.value; + + atLocationObject[objectPID] = ref.getAbsolutePath( + objectAtLocationValue, + ); + }, + this, + ); + + this.atLocationObject = atLocationObject; + + //Put the science metadata pids first + memberPIDs = _.difference(memberPIDs, sciMetaPids); + _.each(_.uniq(sciMetaPids), function (id) { + memberPIDs.unshift(id); + }); + + //Don't fetch each member model if the fetchModels property on this Collection is set to false + if (this.fetchModels !== false) { + //Add the models to the collection now, silently + //this.add(models, {silent: true}); + + //Retrieve the model for each member + _.each( + models, + function (memberModel) { + var collection = this; - // Get system metadata for each member to eval the formatId - _.each( - memberStatements, - function (memberStatement) { - memberURIParts = - memberStatement.object.value.split("/"); - memberPIDStr = _.last(memberURIParts); - memberPID = decodeURIComponent(memberPIDStr); - - if (memberPID) memberPIDs.push(memberPID); - - //TODO: Test passing merge:true when adding a model and this if statement may not be necessary - //Create a DataONEObject model to represent this collection member and add to the collection - if (!_.contains(this.pluck("id"), memberPID)) { - memberModel = new DataONEObject({ - id: memberPID, - resourceMap: [this.packageModel.get("id")], - collections: [this], - }); - - models.push(memberModel); - } - //If the model already exists, add this resource map ID to it's list of resource maps - else { - memberModel = this.get(memberPID); - models.push(memberModel); - - var rMaps = memberModel.get("resourceMap"); - if ( - rMaps && - Array.isArray(rMaps) && - !_.contains( - rMaps, - this.packageModel.get("id") - ) - ) - rMaps.push(this.packageModel.get("id")); - else if (rMaps && !Array.isArray(rMaps)) - rMaps = [ - rMaps, - this.packageModel.get("id"), - ]; - else rMaps = [this.packageModel.get("id")]; - } - }, - this - ); + memberModel.fetch(); + memberModel.once("sync", function (oldModel) { + //Get the right model type based on the model values + var newModel = collection.getMember(oldModel); + + //If the model type has changed, then mark the model as unsynced, since there may be custom fetch() options for the new model + if (oldModel.type != newModel.type) { + // DataPackages shouldn't be fetched until we support nested packages better in the UI + if (newModel.type == "DataPackage") { + //Trigger a replace event so other parts of the app know when a model has been replaced with a different type + oldModel.trigger("replace", newModel); + } else { + newModel.set("synced", false); - //Save the list of original ids - this.originalMembers = memberPIDs; - - // Get the isDocumentedBy relationships - documentsStatements = - this.dataPackageGraph.statementsMatching( - undefined, - CITO("documents"), - undefined, - undefined - ); - - var sciMetaPids = []; - - _.each( - documentsStatements, - function (documentsStatement) { - // Extract and URI-decode the metadata pid - scimetaID = decodeURIComponent( - _.last( - documentsStatement.subject.value.split("/") - ) - ); - - sciMetaPids.push(scimetaID); - - // Extract and URI-decode the data pid - scidataID = decodeURIComponent( - _.last( - documentsStatement.object.value.split("/") - ) - ); - - // Store the isDocumentedBy relationship - if ( - typeof this.originalIsDocBy[scidataID] == - "undefined" - ) - this.originalIsDocBy[scidataID] = [scimetaID]; - else if ( - Array.isArray( - this.originalIsDocBy[scidataID] - ) && - !_.contains( - this.originalIsDocBy[scidataID], - scimetaID - ) - ) - this.originalIsDocBy[scidataID].push(scimetaID); - else - this.originalIsDocBy[scidataID] = _.uniq([ - this.originalIsDocBy[scidataID], - scimetaID, - ]); - - //Find the model in this collection for this data object - //var dataObj = this.get(scidataID); - var dataObj = _.find(models, function (m) { - return m.get("id") == scidataID; - }); - - if (dataObj) { - //Get the isDocumentedBy field - var isDocBy = dataObj.get("isDocumentedBy"); - if ( - isDocBy && - Array.isArray(isDocBy) && - !_.contains(isDocBy, scimetaID) - ) - isDocBy.push(scimetaID); - else if (isDocBy && !Array.isArray(isDocBy)) - isDocBy = [isDocBy, scimetaID]; - else isDocBy = [scimetaID]; - - //Set the isDocumentedBy field - dataObj.set("isDocumentedBy", isDocBy); - } - }, - this - ); + newModel.fetch(); + newModel.once("sync", function (fetchedModel) { + fetchedModel.set("synced", true); - //Save the list of science metadata pids - this.sciMetaPids = sciMetaPids; - - // Parse atLocation - var atLocationObject = {}; - atLocationStatements = - this.dataPackageGraph.statementsMatching( - undefined, - PROV("atLocation"), - undefined, - undefined - ); - - const ref = this; - - // Get atLocation information for each statement in the resourceMap - _.each( - atLocationStatements, - function (atLocationStatement) { - objectParts = - atLocationStatement.subject.value.split("/"); - objectPIDStr = _.last(objectParts); - objectPID = decodeURIComponent(objectPIDStr); - objectAtLocationValue = - atLocationStatement.object.value; - - atLocationObject[objectPID] = ref.getAbsolutePath( - objectAtLocationValue - ); - }, - this - ); + //Remove the model from the collection and add it back + collection.remove(oldModel); + collection.add(fetchedModel); - this.atLocationObject = atLocationObject; + //Trigger a replace event so other parts of the app know when a model has been replaced with a different type + oldModel.trigger("replace", newModel); - //Put the science metadata pids first - memberPIDs = _.difference(memberPIDs, sciMetaPids); - _.each(_.uniq(sciMetaPids), function (id) { - memberPIDs.unshift(id); + if (newModel.type == "EML") + collection.trigger("add:EML"); + }); + } + } else { + newModel.set("synced", true); + collection.add(newModel, { + merge: true, }); - //Don't fetch each member model if the fetchModels property on this Collection is set to false - if (this.fetchModels !== false) { - //Add the models to the collection now, silently - //this.add(models, {silent: true}); - - //Retrieve the model for each member - _.each( - models, - function (memberModel) { - var collection = this; - - memberModel.fetch(); - memberModel.once("sync", function (oldModel) { - //Get the right model type based on the model values - var newModel = - collection.getMember(oldModel); - - //If the model type has changed, then mark the model as unsynced, since there may be custom fetch() options for the new model - if (oldModel.type != newModel.type) { - // DataPackages shouldn't be fetched until we support nested packages better in the UI - if (newModel.type == "DataPackage") { - //Trigger a replace event so other parts of the app know when a model has been replaced with a different type - oldModel.trigger( - "replace", - newModel - ); - } else { - newModel.set("synced", false); - - newModel.fetch(); - newModel.once( - "sync", - function (fetchedModel) { - fetchedModel.set( - "synced", - true - ); - - //Remove the model from the collection and add it back - collection.remove(oldModel); - collection.add( - fetchedModel - ); - - //Trigger a replace event so other parts of the app know when a model has been replaced with a different type - oldModel.trigger( - "replace", - newModel - ); - - if (newModel.type == "EML") - collection.trigger( - "add:EML" - ); - } - ); - } - } else { - newModel.set("synced", true); - collection.add(newModel, { - merge: true, - }); - - if (newModel.type == "EML") - collection.trigger("add:EML"); - } - }); - }, - this - ); - } - } catch (error) { - console.log(error); - } + if (newModel.type == "EML") collection.trigger("add:EML"); + } + }); + }, + this, + ); + } + } catch (error) { + console.log(error); + } - // trigger complete if fetchModel is false and this is the only object in the package - if (this.fetchModels == false && models.length == 1) - this.triggerComplete(); + // trigger complete if fetchModel is false and this is the only object in the package + if (this.fetchModels == false && models.length == 1) + this.triggerComplete(); - return models; - }, + return models; + }, - /* Parse the provenance relationships from the RDF graph, after all DataPackage members + /* Parse the provenance relationships from the RDF graph, after all DataPackage members have been fetched, as the prov info will be stored in them. */ - parseProv: function () { - try { - /* Now run the SPARQL queries for the provenance relationships */ - var provQueries = []; - /* result: pidValue, wasDerivedFromValue (prov_wasDerivedFrom) */ - provQueries["prov_wasDerivedFrom"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_wasDerivedFrom \n" + - "WHERE { \n" + - "?derived_data prov:wasDerivedFrom ?primary_data . \n" + - "?derived_data dcterms:identifier ?pid . \n" + - "?primary_data dcterms:identifier ?prov_wasDerivedFrom . \n" + - "} \n" + - "]]>"; - - /* result: pidValue, generatedValue (prov_generated) */ - provQueries["prov_generated"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_generated \n" + - "WHERE { \n" + - "?result prov:wasGeneratedBy ?activity . \n" + - "?activity prov:qualifiedAssociation ?association . \n" + - "?association prov:hadPlan ?program . \n" + - "?result dcterms:identifier ?prov_generated . \n" + - "?program dcterms:identifier ?pid . \n" + - "} \n" + - "]]>"; - - /* result: pidValue, wasInformedByValue (prov_wasInformedBy) */ - provQueries["prov_wasInformedBy"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_wasInformedBy \n" + - "WHERE { \n" + - "?activity prov:wasInformedBy ?previousActivity . \n" + - "?activity dcterms:identifier ?pid . \n" + - "?previousActivity dcterms:identifier ?prov_wasInformedBy . \n" + - "} \n" + - "]]> \n"; - - /* result: pidValue, usedValue (prov_used) */ - provQueries["prov_used"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_used \n" + - "WHERE { \n" + - "?activity prov:used ?data . \n" + - "?activity prov:qualifiedAssociation ?association . \n" + - "?association prov:hadPlan ?program . \n" + - "?program dcterms:identifier ?pid . \n" + - "?data dcterms:identifier ?prov_used . \n" + - "} \n" + - "]]> \n"; - - /* result: pidValue, programPidValue (prov_generatesByProgram) */ - provQueries["prov_generatedByProgram"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_generatedByProgram \n" + - "WHERE { \n" + - "?derived_data prov:wasGeneratedBy ?execution . \n" + - "?execution prov:qualifiedAssociation ?association . \n" + - "?association prov:hadPlan ?program . \n" + - "?program dcterms:identifier ?prov_generatedByProgram . \n" + - "?derived_data dcterms:identifier ?pid . \n" + - "} \n" + - "]]> \n"; - - /* result: pidValue, executionPidValue */ - provQueries["prov_generatedByExecution"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_generatedByExecution \n" + - "WHERE { \n" + - "?derived_data prov:wasGeneratedBy ?execution . \n" + - "?execution dcterms:identifier ?prov_generatedByExecution . \n" + - "?derived_data dcterms:identifier ?pid . \n" + - "} \n" + - "]]> \n"; - - /* result: pidValue, pid (prov_generatedByProgram) */ - provQueries["prov_generatedByUser"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_generatedByUser \n" + - "WHERE { \n" + - "?derived_data prov:wasGeneratedBy ?execution . \n" + - "?execution prov:qualifiedAssociation ?association . \n" + - "?association prov:agent ?prov_generatedByUser . \n" + - "?derived_data dcterms:identifier ?pid . \n" + - "} \n" + - "]]> \n"; - - /* results: pidValue, programPidValue (prov_usedByProgram) */ - provQueries["prov_usedByProgram"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_usedByProgram \n" + - "WHERE { \n" + - "?execution prov:used ?primary_data . \n" + - "?execution prov:qualifiedAssociation ?association . \n" + - "?association prov:hadPlan ?program . \n" + - "?program dcterms:identifier ?prov_usedByProgram . \n" + - "?primary_data dcterms:identifier ?pid . \n" + - "} \n" + - "]]> \n"; - - /* results: pidValue, executionIdValue (prov_usedByExecution) */ - provQueries["prov_usedByExecution"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_usedByExecution \n" + - "WHERE { \n" + - "?execution prov:used ?primary_data . \n" + - "?primary_data dcterms:identifier ?pid . \n" + - "?execution dcterms:identifier ?prov_usedByExecution . \n" + - "} \n" + - "]]> \n"; - - /* results: pidValue, pid (prov_usedByUser) */ - provQueries["prov_usedByUser"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_usedByUser \n" + - "WHERE { \n" + - "?execution prov:used ?primary_data . \n" + - "?execution prov:qualifiedAssociation ?association . \n" + - "?association prov:agent ?prov_usedByUser . \n" + - "?primary_data dcterms:identifier ?pid . \n" + - "} \n" + - "]]> \n"; - /* results: pidValue, executionIdValue (prov_wasExecutedByExecution) */ - provQueries["prov_wasExecutedByExecution"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_wasExecutedByExecution \n" + - "WHERE { \n" + - "?execution prov:qualifiedAssociation ?association . \n" + - "?association prov:hadPlan ?program . \n" + - "?execution dcterms:identifier ?prov_wasExecutedByExecution . \n" + - "?program dcterms:identifier ?pid . \n" + - "} \n" + - "]]> \n"; - - /* results: pidValue, pid (prov_wasExecutedByUser) */ - provQueries["prov_wasExecutedByUser"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_wasExecutedByUser \n" + - "WHERE { \n" + - "?execution prov:qualifiedAssociation ?association . \n" + - "?association prov:hadPlan ?program . \n" + - "?association prov:agent ?prov_wasExecutedByUser . \n" + - "?program dcterms:identifier ?pid . \n" + - "} \n" + - "]]> \n"; - - /* results: pidValue, derivedDataPidValue (prov_hasDerivations) */ - provQueries["prov_hasDerivations"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "PREFIX cito: <http://purl.org/spar/cito/> \n" + - "SELECT ?pid ?prov_hasDerivations \n" + - "WHERE { \n" + - "?derived_data prov:wasDerivedFrom ?source_data . \n" + - "?source_data dcterms:identifier ?pid . \n" + - "?derived_data dcterms:identifier ?prov_hasDerivations . \n" + - "} \n" + - "]]> \n"; - - /* results: pidValue, pid (prov_instanceOfClass) */ - provQueries["prov_instanceOfClass"] = - "<![CDATA[ \n" + - "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + - "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + - "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + - "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + - "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + - "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + - "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + - "SELECT ?pid ?prov_instanceOfClass \n" + - "WHERE { \n" + - "?subject rdf:type ?prov_instanceOfClass . \n" + - "?subject dcterms:identifier ?pid . \n" + - "} \n" + - "]]> \n"; - - // These are the provenance fields that are currently searched for in the provenance queries, but - // not all of these fields are displayed by any view. - // Note: this list is different than the prov list returned by MetacatUI.appSearchModel.getProvFields() - this.provFields = [ - "prov_wasDerivedFrom", - "prov_generated", - "prov_wasInformedBy", - "prov_used", - "prov_generatedByProgram", - "prov_generatedByExecution", - "prov_generatedByUser", - "prov_usedByProgram", - "prov_usedByExecution", - "prov_usedByUser", - "prov_wasExecutedByExecution", - "prov_wasExecutedByUser", - "prov_hasDerivations", - "prov_instanceOfClass", - ]; - - // Process each SPARQL query - var keys = Object.keys(provQueries); - this.queriesToRun = keys.length; - - //Bind the onResult and onDone functions to the model so they can be called out of context - this.onResult = _.bind(this.onResult, this); - this.onDone = _.bind(this.onDone, this); - - /* Run queries for all provenance fields. + parseProv: function () { + try { + /* Now run the SPARQL queries for the provenance relationships */ + var provQueries = []; + /* result: pidValue, wasDerivedFromValue (prov_wasDerivedFrom) */ + provQueries["prov_wasDerivedFrom"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_wasDerivedFrom \n" + + "WHERE { \n" + + "?derived_data prov:wasDerivedFrom ?primary_data . \n" + + "?derived_data dcterms:identifier ?pid . \n" + + "?primary_data dcterms:identifier ?prov_wasDerivedFrom . \n" + + "} \n" + + "]]>"; + + /* result: pidValue, generatedValue (prov_generated) */ + provQueries["prov_generated"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_generated \n" + + "WHERE { \n" + + "?result prov:wasGeneratedBy ?activity . \n" + + "?activity prov:qualifiedAssociation ?association . \n" + + "?association prov:hadPlan ?program . \n" + + "?result dcterms:identifier ?prov_generated . \n" + + "?program dcterms:identifier ?pid . \n" + + "} \n" + + "]]>"; + + /* result: pidValue, wasInformedByValue (prov_wasInformedBy) */ + provQueries["prov_wasInformedBy"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_wasInformedBy \n" + + "WHERE { \n" + + "?activity prov:wasInformedBy ?previousActivity . \n" + + "?activity dcterms:identifier ?pid . \n" + + "?previousActivity dcterms:identifier ?prov_wasInformedBy . \n" + + "} \n" + + "]]> \n"; + + /* result: pidValue, usedValue (prov_used) */ + provQueries["prov_used"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_used \n" + + "WHERE { \n" + + "?activity prov:used ?data . \n" + + "?activity prov:qualifiedAssociation ?association . \n" + + "?association prov:hadPlan ?program . \n" + + "?program dcterms:identifier ?pid . \n" + + "?data dcterms:identifier ?prov_used . \n" + + "} \n" + + "]]> \n"; + + /* result: pidValue, programPidValue (prov_generatesByProgram) */ + provQueries["prov_generatedByProgram"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_generatedByProgram \n" + + "WHERE { \n" + + "?derived_data prov:wasGeneratedBy ?execution . \n" + + "?execution prov:qualifiedAssociation ?association . \n" + + "?association prov:hadPlan ?program . \n" + + "?program dcterms:identifier ?prov_generatedByProgram . \n" + + "?derived_data dcterms:identifier ?pid . \n" + + "} \n" + + "]]> \n"; + + /* result: pidValue, executionPidValue */ + provQueries["prov_generatedByExecution"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_generatedByExecution \n" + + "WHERE { \n" + + "?derived_data prov:wasGeneratedBy ?execution . \n" + + "?execution dcterms:identifier ?prov_generatedByExecution . \n" + + "?derived_data dcterms:identifier ?pid . \n" + + "} \n" + + "]]> \n"; + + /* result: pidValue, pid (prov_generatedByProgram) */ + provQueries["prov_generatedByUser"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_generatedByUser \n" + + "WHERE { \n" + + "?derived_data prov:wasGeneratedBy ?execution . \n" + + "?execution prov:qualifiedAssociation ?association . \n" + + "?association prov:agent ?prov_generatedByUser . \n" + + "?derived_data dcterms:identifier ?pid . \n" + + "} \n" + + "]]> \n"; + + /* results: pidValue, programPidValue (prov_usedByProgram) */ + provQueries["prov_usedByProgram"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_usedByProgram \n" + + "WHERE { \n" + + "?execution prov:used ?primary_data . \n" + + "?execution prov:qualifiedAssociation ?association . \n" + + "?association prov:hadPlan ?program . \n" + + "?program dcterms:identifier ?prov_usedByProgram . \n" + + "?primary_data dcterms:identifier ?pid . \n" + + "} \n" + + "]]> \n"; + + /* results: pidValue, executionIdValue (prov_usedByExecution) */ + provQueries["prov_usedByExecution"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_usedByExecution \n" + + "WHERE { \n" + + "?execution prov:used ?primary_data . \n" + + "?primary_data dcterms:identifier ?pid . \n" + + "?execution dcterms:identifier ?prov_usedByExecution . \n" + + "} \n" + + "]]> \n"; + + /* results: pidValue, pid (prov_usedByUser) */ + provQueries["prov_usedByUser"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_usedByUser \n" + + "WHERE { \n" + + "?execution prov:used ?primary_data . \n" + + "?execution prov:qualifiedAssociation ?association . \n" + + "?association prov:agent ?prov_usedByUser . \n" + + "?primary_data dcterms:identifier ?pid . \n" + + "} \n" + + "]]> \n"; + /* results: pidValue, executionIdValue (prov_wasExecutedByExecution) */ + provQueries["prov_wasExecutedByExecution"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_wasExecutedByExecution \n" + + "WHERE { \n" + + "?execution prov:qualifiedAssociation ?association . \n" + + "?association prov:hadPlan ?program . \n" + + "?execution dcterms:identifier ?prov_wasExecutedByExecution . \n" + + "?program dcterms:identifier ?pid . \n" + + "} \n" + + "]]> \n"; + + /* results: pidValue, pid (prov_wasExecutedByUser) */ + provQueries["prov_wasExecutedByUser"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_wasExecutedByUser \n" + + "WHERE { \n" + + "?execution prov:qualifiedAssociation ?association . \n" + + "?association prov:hadPlan ?program . \n" + + "?association prov:agent ?prov_wasExecutedByUser . \n" + + "?program dcterms:identifier ?pid . \n" + + "} \n" + + "]]> \n"; + + /* results: pidValue, derivedDataPidValue (prov_hasDerivations) */ + provQueries["prov_hasDerivations"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "PREFIX cito: <http://purl.org/spar/cito/> \n" + + "SELECT ?pid ?prov_hasDerivations \n" + + "WHERE { \n" + + "?derived_data prov:wasDerivedFrom ?source_data . \n" + + "?source_data dcterms:identifier ?pid . \n" + + "?derived_data dcterms:identifier ?prov_hasDerivations . \n" + + "} \n" + + "]]> \n"; + + /* results: pidValue, pid (prov_instanceOfClass) */ + provQueries["prov_instanceOfClass"] = + "<![CDATA[ \n" + + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + + "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" + + "PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" + + "PREFIX prov: <http://www.w3.org/ns/prov#> \n" + + "PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" + + "PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" + + "PREFIX dcterms: <http://purl.org/dc/terms/> \n" + + "SELECT ?pid ?prov_instanceOfClass \n" + + "WHERE { \n" + + "?subject rdf:type ?prov_instanceOfClass . \n" + + "?subject dcterms:identifier ?pid . \n" + + "} \n" + + "]]> \n"; + + // These are the provenance fields that are currently searched for in the provenance queries, but + // not all of these fields are displayed by any view. + // Note: this list is different than the prov list returned by MetacatUI.appSearchModel.getProvFields() + this.provFields = [ + "prov_wasDerivedFrom", + "prov_generated", + "prov_wasInformedBy", + "prov_used", + "prov_generatedByProgram", + "prov_generatedByExecution", + "prov_generatedByUser", + "prov_usedByProgram", + "prov_usedByExecution", + "prov_usedByUser", + "prov_wasExecutedByExecution", + "prov_wasExecutedByUser", + "prov_hasDerivations", + "prov_instanceOfClass", + ]; + + // Process each SPARQL query + var keys = Object.keys(provQueries); + this.queriesToRun = keys.length; + + //Bind the onResult and onDone functions to the model so they can be called out of context + this.onResult = _.bind(this.onResult, this); + this.onDone = _.bind(this.onDone, this); + + /* Run queries for all provenance fields. Each query may have multiple solutions and each solution will trigger a callback to the 'onResult' function. When each query has completed, the 'onDone' function is called for that query. */ - for (var iquery = 0; iquery < keys.length; iquery++) { - var eq = rdf.SPARQLToQuery( - provQueries[keys[iquery]], - false, - this.dataPackageGraph - ); - this.dataPackageGraph.query( - eq, - this.onResult, - this.url(), - this.onDone - ); - } - } catch (error) { - console.log(error); - } - }, - - // The return values have to be extracted from the result. - getValue: function (result, name) { - var res = result[name]; - // The result is of type 'NamedNode', just return the string value - if (res) { - return res.value; - } else return " "; - }, - - /* This callback is called for every query solution of the SPARQL queries. One + for (var iquery = 0; iquery < keys.length; iquery++) { + var eq = rdf.SPARQLToQuery( + provQueries[keys[iquery]], + false, + this.dataPackageGraph, + ); + this.dataPackageGraph.query( + eq, + this.onResult, + this.url(), + this.onDone, + ); + } + } catch (error) { + console.log(error); + } + }, + + // The return values have to be extracted from the result. + getValue: function (result, name) { + var res = result[name]; + // The result is of type 'NamedNode', just return the string value + if (res) { + return res.value; + } else return " "; + }, + + /* This callback is called for every query solution of the SPARQL queries. One query may result in multple queries solutions and calls to this function. Each query result returns two pids, i.e. pid: 1234 prov_generated: 5678, which corresponds to the RDF triple '5678 wasGeneratedBy 1234', or the @@ -1189,2670 +1139,2391 @@

Source: src/js/collections/DataPackage.js

?primary_data : t {termType: "NamedNode", value: "https://cn-stage.test.dataone.org/cn/v2/resolve/urn%3Auuid%3Aaae9d025-a331-4c3a-b399-a8ca0a2826ef"} ?prov_wasDerivedFrom : t {termType: "Literal", value: "urn:uuid:aae9d025-a331-4c3a-b399-a8ca0a2826ef", datatype: t}] */ - onResult: function (result) { - var currentPid = this.getValue(result, "?pid"); - var resval; - var provFieldResult; - var provFieldValues; - - // If there is a solution for this query, assign the value - // to the prov field attribute (e.g. "prov_generated") of the package member (a DataONEObject) - // with id = '?pid' - if (typeof currentPid !== "undefined" && currentPid !== " ") { - var currentMember = null; - var provFieldValues; - var fieldName = null; - var vals = []; - var resultMember = null; - currentMember = this.find(function (model) { - return model.get("id") === currentPid; - }); - - if (typeof currentMember === "undefined") { - return; - } - // Search for a provenenace field value (i.e. 'prov_wasDerivedFrom') that was - // returned from the query. The current prov queries all return one prov field each - // (see this.provFields). - // Note: dataPackage.provSources and dataPackage.provDerivations are accumulators for - // the entire DataPackage. member.sources and member.derivations are accumulators for - // each package member, and are used by functions such as ProvChartView(). - for (var iFld = 0; iFld < this.provFields.length; iFld++) { - fieldName = this.provFields[iFld]; - resval = "?" + fieldName; - // The pid corresponding to the object of the RDF triple, with the predicate - // of 'prov_generated', 'prov_used', etc. - // getValue returns a string value. - provFieldResult = this.getValue(result, resval); - if (provFieldResult != " ") { - // Find the Datapacakge member for the result 'pid' and add the result - // prov_* value to it. This is the package member that is the 'subject' of the - // prov relationship. - // The 'resultMember' could be in the current package, or could be in another 'related' package. - resultMember = this.find(function (model) { - return model.get("id") === provFieldResult; - }); - - if (typeof resultMember !== "undefined") { - // If this prov field is a 'source' field, add it to 'sources' - - if (currentMember.isSourceField(fieldName)) { - // Get the package member that the id of the prov field is associated with - if ( - _.findWhere(this.sources, { - id: provFieldResult, - }) == null - ) { - this.sources.push(resultMember); - } - // Only add the result member if it has not already been added. - if ( - _.findWhere( - currentMember.get("provSources"), - { id: provFieldResult } - ) == null - ) { - vals = currentMember.get("provSources"); - vals.push(resultMember); - currentMember.set("provSources", vals); - } - } else if ( - currentMember.isDerivationField(fieldName) - ) { - // If this prov field is a 'derivation' field, add it to 'derivations' - if ( - _.findWhere(this.derivations, { - id: provFieldResult, - }) == null - ) { - this.derivations.push(resultMember); - } - - if ( - _.findWhere( - currentMember.get( - "provDerivations" - ), - { id: provFieldResult } - ) == null - ) { - vals = - currentMember.get( - "provDerivations" - ); - vals.push(resultMember); - currentMember.set( - "provDerivations", - vals - ); - } - } - - // Get the existing values for this prov field in the package member - vals = currentMember.get(fieldName); - - // Push this result onto the prov file list if it is not there, i.e. - if (!_.contains(vals, resultMember)) { - vals.push(resultMember); - currentMember.set(fieldName, vals); - } - - //provFieldValues = _.uniq(provFieldValues); - // Add the current prov valid (a pid) to the current value in the member - //currentMember.set(fieldName, provFieldValues); - //this.add(currentMember, { merge: true }); - } else { - // The query result field is not the identifier of a packge member, so it may be the identifier - // of another 'related' package, or it may be a string value that is the object of a prov relationship, - // i.e. for 'prov_instanceOfClass' == 'http://purl.dataone.org/provone/2015/01/15/ontology#Data', - // so add the value to the current member. - vals = currentMember.get(fieldName); - if (!_.contains(vals, provFieldResult)) { - vals.push(provFieldResult); - currentMember.set(fieldName, vals); - } - } - } - } + onResult: function (result) { + var currentPid = this.getValue(result, "?pid"); + var resval; + var provFieldResult; + var provFieldValues; + + // If there is a solution for this query, assign the value + // to the prov field attribute (e.g. "prov_generated") of the package member (a DataONEObject) + // with id = '?pid' + if (typeof currentPid !== "undefined" && currentPid !== " ") { + var currentMember = null; + var provFieldValues; + var fieldName = null; + var vals = []; + var resultMember = null; + currentMember = this.find(function (model) { + return model.get("id") === currentPid; + }); + + if (typeof currentMember === "undefined") { + return; + } + // Search for a provenenace field value (i.e. 'prov_wasDerivedFrom') that was + // returned from the query. The current prov queries all return one prov field each + // (see this.provFields). + // Note: dataPackage.provSources and dataPackage.provDerivations are accumulators for + // the entire DataPackage. member.sources and member.derivations are accumulators for + // each package member, and are used by functions such as ProvChartView(). + for (var iFld = 0; iFld < this.provFields.length; iFld++) { + fieldName = this.provFields[iFld]; + resval = "?" + fieldName; + // The pid corresponding to the object of the RDF triple, with the predicate + // of 'prov_generated', 'prov_used', etc. + // getValue returns a string value. + provFieldResult = this.getValue(result, resval); + if (provFieldResult != " ") { + // Find the Datapacakge member for the result 'pid' and add the result + // prov_* value to it. This is the package member that is the 'subject' of the + // prov relationship. + // The 'resultMember' could be in the current package, or could be in another 'related' package. + resultMember = this.find(function (model) { + return model.get("id") === provFieldResult; + }); + + if (typeof resultMember !== "undefined") { + // If this prov field is a 'source' field, add it to 'sources' + + if (currentMember.isSourceField(fieldName)) { + // Get the package member that the id of the prov field is associated with + if ( + _.findWhere(this.sources, { + id: provFieldResult, + }) == null + ) { + this.sources.push(resultMember); + } + // Only add the result member if it has not already been added. + if ( + _.findWhere(currentMember.get("provSources"), { + id: provFieldResult, + }) == null + ) { + vals = currentMember.get("provSources"); + vals.push(resultMember); + currentMember.set("provSources", vals); + } + } else if (currentMember.isDerivationField(fieldName)) { + // If this prov field is a 'derivation' field, add it to 'derivations' + if ( + _.findWhere(this.derivations, { + id: provFieldResult, + }) == null + ) { + this.derivations.push(resultMember); + } + + if ( + _.findWhere(currentMember.get("provDerivations"), { + id: provFieldResult, + }) == null + ) { + vals = currentMember.get("provDerivations"); + vals.push(resultMember); + currentMember.set("provDerivations", vals); + } } - }, - /* This callback is called when all queries have finished. */ - onDone: function () { - if (this.queriesToRun > 1) { - this.queriesToRun--; - } else { - // Signal that all prov queries have finished - this.provenanceFlag = "complete"; - this.trigger("queryComplete"); - } - }, - - /* - * Use the DataONEObject parseSysMeta() function - */ - parseSysMeta: function () { - return DataONEObject.parseSysMeta.call(this, arguments[0]); - }, + // Get the existing values for this prov field in the package member + vals = currentMember.get(fieldName); - /** - * Overwrite the Backbone.Collection.sync() function to set custom options - * @param {Object} [options] - Options for this DataPackage save - * @param {Boolean} [options.sysMetaOnly] - If true, only the system metadata of this Package will be saved. - * @param {Boolean} [options.resourceMapOnly] - If true, only the Resource Map/Package object will be saved. Metadata and Data objects aggregated by the package will be skipped. - */ - save: function (options) { - if (!options) var options = {}; - - this.packageModel.set("uploadStatus", "p"); - - //Get the system metadata first if we haven't retrieved it yet - if (!this.packageModel.get("sysMetaXML")) { - var collection = this; - this.packageModel.fetch({ - success: function () { - collection.save(options); - }, - }); - return; + // Push this result onto the prov file list if it is not there, i.e. + if (!_.contains(vals, resultMember)) { + vals.push(resultMember); + currentMember.set(fieldName, vals); } - //If we want to update the system metadata only, - // then update via the DataONEObject model and exit - if (options.sysMetaOnly) { - this.packageModel.save(null, options); - return; - } - - if (options.resourceMapOnly !== true) { - //Sort the models in the collection so the metadata is saved first - var metadataModels = this.where({ type: "Metadata" }); - var dataModels = _.difference(this.models, metadataModels); - var sortedModels = _.union(metadataModels, dataModels); - var modelsInProgress = _.filter(sortedModels, function (m) { - return ( - m.get("uploadStatus") == "p" || - m.get("sysMetaUploadStatus") == "p" - ); - }); - var modelsToBeSaved = _.filter(sortedModels, function (m) { - //Models should be saved if they are in the save queue, had an error saving earlier, - //or they are Science Metadata model that is NOT already in progress - return ( - (m.get("type") == "Metadata" && - m.get("uploadStatus") == "q") || - (m.get("type") == "Data" && - m.get("hasContentChanges") && - m.get("uploadStatus") != "p" && - m.get("uploadStatus") != "c" && - m.get("uploadStatus") != "e") || - (m.get("type") == "Metadata" && - m.get("uploadStatus") != "p" && - m.get("uploadStatus") != "c" && - m.get("uploadStatus") != "e" && - m.get("uploadStatus") !== null) - ); - }); - //Get an array of data objects whose system metadata should be updated. - var sysMetaToUpdate = _.reject(dataModels, function (m) { - // Find models that don't have any content changes to save, - // and whose system metadata is not already saving - return ( - !m.hasUpdates() || - m.get("hasContentChanges") || - m.get("sysMetaUploadStatus") == "p" || - m.get("sysMetaUploadStatus") == "c" || - m.get("sysMetaUploadStatus") == "e" - ); - }); - - //First quickly validate all the models before attempting to save any - var allValid = _.every(modelsToBeSaved, function (m) { - if (m.isValid()) { - m.trigger("valid"); - return true; - } else { - return false; - } - }); - - // If at least once model to be saved is invalid, - // or the metadata failed to save, cancel the save. - if ( - !allValid || - _.contains( - _.map(metadataModels, function (model) { - return model.get("uploadStatus"); - }), - "e" - ) - ) { - this.packageModel.set("changed", false); - this.packageModel.set("uploadStatus", "q"); - this.trigger("cancelSave"); - return; - } - - //If we are saving at least one model in this package, then serialize the Resource Map RDF XML - if (modelsToBeSaved.length) { - try { - //Set a new id and keep our old id - if (!this.packageModel.isNew()) { - //Update the identifier for this object - this.packageModel.updateID(); - } - - //Create the resource map XML - var mapXML = this.serialize(); - } catch (serializationException) { - //If serialization failed, revert back to our old id - this.packageModel.resetID(); - - //Cancel the save and show an error message - this.packageModel.set("changed", false); - this.packageModel.set("uploadStatus", "q"); - this.trigger( - "errorSaving", - "There was a Javascript error during the serialization process: " + - serializationException - ); - return; - } - } - - //First save all the models of the collection, if needed - _.each( - modelsToBeSaved, - function (model) { - //If the model is saved successfully, start this save function again - this.stopListening( - model, - "successSaving", - this.save - ); - this.listenToOnce( - model, - "successSaving", - this.save - ); - - //If the model fails to save, start this save function - this.stopListening(model, "errorSaving", this.save); - this.listenToOnce(model, "errorSaving", this.save); - - //If the model fails to save, start this save function - this.stopListening(model, "cancelSave", this.save); - this.listenToOnce(model, "cancelSave", this.save); - - //Save the model and watch for fails - model.save(); - - //Add it to the list of models in progress - modelsInProgress.push(model); - - this.numSaves++; - }, - this - ); - - //Save the system metadata of all the Data objects - _.each( - sysMetaToUpdate, - function (dataModel) { - //When the sytem metadata has been saved, save this resource map - this.listenTo( - dataModel, - "change:sysMetaUploadStatus", - this.save - ); - //Update the system metadata - dataModel.updateSysMeta(); - //Add it to the list of models in progress - modelsInProgress.push(dataModel); - this.numSaves++; - }, - this - ); - - //If there are still models in progress of uploading, then exit. (We will return when they are synced to upload the resource map) - if (modelsInProgress.length) return; - } - //If we are saving the resource map object only, and there are changes to save, serialize the RDF XML - else if (this.needsUpdate()) { - try { - //Set a new id and keep our old id - if (!this.packageModel.isNew()) { - //Update the identifier for this object - this.packageModel.updateID(); - } - - //Create the resource map XML - var mapXML = this.serialize(); - } catch (serializationException) { - //If serialization failed, revert back to our old id - this.packageModel.resetID(); - - //Cancel the save and show an error message - this.packageModel.set("changed", false); - this.packageModel.set("uploadStatus", "q"); - this.trigger( - "errorSaving", - "There was a Javascript error during the serialization process: " + - serializationException - ); - return; - } - } - //If we are saving the resource map object only, and there are no changes to save, exit the function - else if (!this.needsUpdate()) { - return; - } - - //If no models were saved and this package has no changes, we can exit without saving the resource map - if (this.numSaves < 1 && !this.needsUpdate()) { - this.numSaves = 0; - this.packageModel.set( - "uploadStatus", - this.packageModel.defaults().uploadStatus - ); - this.trigger("successSaving", this); - return; + //provFieldValues = _.uniq(provFieldValues); + // Add the current prov valid (a pid) to the current value in the member + //currentMember.set(fieldName, provFieldValues); + //this.add(currentMember, { merge: true }); + } else { + // The query result field is not the identifier of a packge member, so it may be the identifier + // of another 'related' package, or it may be a string value that is the object of a prov relationship, + // i.e. for 'prov_instanceOfClass' == 'http://purl.dataone.org/provone/2015/01/15/ontology#Data', + // so add the value to the current member. + vals = currentMember.get(fieldName); + if (!_.contains(vals, provFieldResult)) { + vals.push(provFieldResult); + currentMember.set(fieldName, vals); } + } + } + } + } + }, + + /* This callback is called when all queries have finished. */ + onDone: function () { + if (this.queriesToRun > 1) { + this.queriesToRun--; + } else { + // Signal that all prov queries have finished + this.provenanceFlag = "complete"; + this.trigger("queryComplete"); + } + }, - //Reset the number of models saved since they should all be completed by now - this.numSaves = 0; + /* + * Use the DataONEObject parseSysMeta() function + */ + parseSysMeta: function () { + return DataONEObject.parseSysMeta.call(this, arguments[0]); + }, + + /** + * Overwrite the Backbone.Collection.sync() function to set custom options + * @param {Object} [options] - Options for this DataPackage save + * @param {Boolean} [options.sysMetaOnly] - If true, only the system metadata of this Package will be saved. + * @param {Boolean} [options.resourceMapOnly] - If true, only the Resource Map/Package object will be saved. Metadata and Data objects aggregated by the package will be skipped. + */ + save: function (options) { + if (!options) var options = {}; - //Determine the HTTP request type - var requestType; - if (this.packageModel.isNew()) { - requestType = "POST"; - } else { - requestType = "PUT"; - } + this.packageModel.set("uploadStatus", "p"); - //Create a FormData object to send data with the XHR - var formData = new FormData(); + //Get the system metadata first if we haven't retrieved it yet + if (!this.packageModel.get("sysMetaXML")) { + var collection = this; + this.packageModel.fetch({ + success: function () { + collection.save(options); + }, + }); + return; + } - //Add the identifier to the XHR data - if (this.packageModel.isNew()) { - formData.append("pid", this.packageModel.get("id")); - } else { - //Add the ids to the form data - formData.append("newPid", this.packageModel.get("id")); - formData.append("pid", this.packageModel.get("oldPid")); - } + //If we want to update the system metadata only, + // then update via the DataONEObject model and exit + if (options.sysMetaOnly) { + this.packageModel.save(null, options); + return; + } - //Do a fresh re-serialization of the RDF XML, in case any pids in the package have changed. - //The hope is that any errors during the serialization process have already been caught during the first serialization above - try { - var mapXML = this.serialize(); - } catch (serializationException) { - //Cancel the save and show an error message - this.packageModel.set("changed", false); - this.packageModel.set("uploadStatus", "q"); - this.trigger( - "errorSaving", - "There was a Javascript error during the serialization process: " + - serializationException - ); - return; - } + if (options.resourceMapOnly !== true) { + //Sort the models in the collection so the metadata is saved first + var metadataModels = this.where({ type: "Metadata" }); + var dataModels = _.difference(this.models, metadataModels); + var sortedModels = _.union(metadataModels, dataModels); + var modelsInProgress = _.filter(sortedModels, function (m) { + return ( + m.get("uploadStatus") == "p" || + m.get("sysMetaUploadStatus") == "p" + ); + }); + var modelsToBeSaved = _.filter(sortedModels, function (m) { + //Models should be saved if they are in the save queue, had an error saving earlier, + //or they are Science Metadata model that is NOT already in progress + return ( + (m.get("type") == "Metadata" && m.get("uploadStatus") == "q") || + (m.get("type") == "Data" && + m.get("hasContentChanges") && + m.get("uploadStatus") != "p" && + m.get("uploadStatus") != "c" && + m.get("uploadStatus") != "e") || + (m.get("type") == "Metadata" && + m.get("uploadStatus") != "p" && + m.get("uploadStatus") != "c" && + m.get("uploadStatus") != "e" && + m.get("uploadStatus") !== null) + ); + }); + //Get an array of data objects whose system metadata should be updated. + var sysMetaToUpdate = _.reject(dataModels, function (m) { + // Find models that don't have any content changes to save, + // and whose system metadata is not already saving + return ( + !m.hasUpdates() || + m.get("hasContentChanges") || + m.get("sysMetaUploadStatus") == "p" || + m.get("sysMetaUploadStatus") == "c" || + m.get("sysMetaUploadStatus") == "e" + ); + }); + + //First quickly validate all the models before attempting to save any + var allValid = _.every(modelsToBeSaved, function (m) { + if (m.isValid()) { + m.trigger("valid"); + return true; + } else { + return false; + } + }); + + // If at least once model to be saved is invalid, + // or the metadata failed to save, cancel the save. + if ( + !allValid || + _.contains( + _.map(metadataModels, function (model) { + return model.get("uploadStatus"); + }), + "e", + ) + ) { + this.packageModel.set("changed", false); + this.packageModel.set("uploadStatus", "q"); + this.trigger("cancelSave"); + return; + } + + //If we are saving at least one model in this package, then serialize the Resource Map RDF XML + if (modelsToBeSaved.length) { + try { + //Set a new id and keep our old id + if (!this.packageModel.isNew()) { + //Update the identifier for this object + this.packageModel.updateID(); + } - //Make a Blob object from the serialized RDF XML - var mapBlob = new Blob([mapXML], { type: "application/xml" }); + //Create the resource map XML + var mapXML = this.serialize(); + } catch (serializationException) { + //If serialization failed, revert back to our old id + this.packageModel.resetID(); + + //Cancel the save and show an error message + this.packageModel.set("changed", false); + this.packageModel.set("uploadStatus", "q"); + this.trigger( + "errorSaving", + "There was a Javascript error during the serialization process: " + + serializationException, + ); + return; + } + } + + //First save all the models of the collection, if needed + _.each( + modelsToBeSaved, + function (model) { + //If the model is saved successfully, start this save function again + this.stopListening(model, "successSaving", this.save); + this.listenToOnce(model, "successSaving", this.save); + + //If the model fails to save, start this save function + this.stopListening(model, "errorSaving", this.save); + this.listenToOnce(model, "errorSaving", this.save); + + //If the model fails to save, start this save function + this.stopListening(model, "cancelSave", this.save); + this.listenToOnce(model, "cancelSave", this.save); + + //Save the model and watch for fails + model.save(); + + //Add it to the list of models in progress + modelsInProgress.push(model); + + this.numSaves++; + }, + this, + ); + + //Save the system metadata of all the Data objects + _.each( + sysMetaToUpdate, + function (dataModel) { + //When the sytem metadata has been saved, save this resource map + this.listenTo(dataModel, "change:sysMetaUploadStatus", this.save); + //Update the system metadata + dataModel.updateSysMeta(); + //Add it to the list of models in progress + modelsInProgress.push(dataModel); + this.numSaves++; + }, + this, + ); - //Get the size of the new resource map - this.packageModel.set("size", mapBlob.size); + //If there are still models in progress of uploading, then exit. (We will return when they are synced to upload the resource map) + if (modelsInProgress.length) return; + } + //If we are saving the resource map object only, and there are changes to save, serialize the RDF XML + else if (this.needsUpdate()) { + try { + //Set a new id and keep our old id + if (!this.packageModel.isNew()) { + //Update the identifier for this object + this.packageModel.updateID(); + } + + //Create the resource map XML + var mapXML = this.serialize(); + } catch (serializationException) { + //If serialization failed, revert back to our old id + this.packageModel.resetID(); + + //Cancel the save and show an error message + this.packageModel.set("changed", false); + this.packageModel.set("uploadStatus", "q"); + this.trigger( + "errorSaving", + "There was a Javascript error during the serialization process: " + + serializationException, + ); + return; + } + } + //If we are saving the resource map object only, and there are no changes to save, exit the function + else if (!this.needsUpdate()) { + return; + } - //Get the new checksum of the resource map - var checksum = md5(mapXML); - this.packageModel.set("checksum", checksum); - this.packageModel.set("checksumAlgorithm", "MD5"); + //If no models were saved and this package has no changes, we can exit without saving the resource map + if (this.numSaves < 1 && !this.needsUpdate()) { + this.numSaves = 0; + this.packageModel.set( + "uploadStatus", + this.packageModel.defaults().uploadStatus, + ); + this.trigger("successSaving", this); + return; + } - //Set the file name based on the id - this.packageModel.set( - "fileName", - this.packageModel.get("id").replace(/[^a-zA-Z0-9]/g, "_") + - ".rdf.xml" - ); + //Reset the number of models saved since they should all be completed by now + this.numSaves = 0; - //Create the system metadata - var sysMetaXML = this.packageModel.serializeSysMeta(); + //Determine the HTTP request type + var requestType; + if (this.packageModel.isNew()) { + requestType = "POST"; + } else { + requestType = "PUT"; + } - //Send the system metadata - var xmlBlob = new Blob([sysMetaXML], { - type: "application/xml", - }); + //Create a FormData object to send data with the XHR + var formData = new FormData(); - //Add the object XML and System Metadata XML to the form data - //Append the system metadata first, so we can take advantage of Metacat's streaming multipart handler - formData.append("sysmeta", xmlBlob, "sysmeta"); - formData.append("object", mapBlob); + //Add the identifier to the XHR data + if (this.packageModel.isNew()) { + formData.append("pid", this.packageModel.get("id")); + } else { + //Add the ids to the form data + formData.append("newPid", this.packageModel.get("id")); + formData.append("pid", this.packageModel.get("oldPid")); + } - var collection = this; - var requestSettings = { - url: this.packageModel.isNew() - ? this.url() - : this.url({ update: true }), - type: requestType, - cache: false, - contentType: false, - processData: false, - data: formData, - success: function (response) { - //Update the object XML - collection.objectXML = mapXML; - collection.packageModel.set( - "sysMetaXML", - collection.packageModel.serializeSysMeta() - ); - - //Reset the upload status for all members - _.each( - collection.where({ uploadStatus: "c" }), - function (m) { - m.set( - "uploadStatus", - m.defaults().uploadStatus - ); - } - ); - - // Reset oldPid to null so we know we need to update the ID - // in the future - collection.packageModel.set("oldPid", null); - - //Reset the upload status for the package - collection.packageModel.set( - "uploadStatus", - collection.packageModel.defaults().uploadStatus - ); - - // Reset the content changes status - collection.packageModel.set("hasContentChanges", false); - - // This package is no longer new, so mark it as such - collection.packageModel.set("isNew", false); - - collection.trigger("successSaving", collection); - - collection.packageModel.fetch({ merge: true }); - - _.each(sysMetaToUpdate, function (dataModel) { - dataModel.set("sysMetaUploadStatus", "c"); - }); - }, - error: function (data) { - //Reset the id back to its original state - collection.packageModel.resetID(); - - //Reset the upload status for all members - _.each( - collection.where({ uploadStatus: "c" }), - function (m) { - m.set( - "uploadStatus", - m.defaults().uploadStatus - ); - } - ); - - //When there is no network connection (status == 0), there will be no response text - if (data.status == 408 || data.status == 0) { - var parsedResponse = - "There was a network issue that prevented this file from uploading. " + - "Make sure you are connected to a reliable internet connection."; - } else { - var parsedResponse = $(data.responseText) - .not("style, title") - .text(); - } - - //Save the error message in the model - collection.packageModel.set( - "errorMessage", - parsedResponse - ); - - //Reset the upload status for the package - collection.packageModel.set("uploadStatus", "e"); - - collection.trigger("errorSaving", parsedResponse); - - // Track this error in our analytics - MetacatUI.analytics?.trackException( - `DataPackage save error: ${parsedResponse}`, - collection.packageModel.get("id"), - true - ); - }, - }; - $.ajax( - _.extend( - requestSettings, - MetacatUI.appUserModel.createAjaxSettings() - ) - ); - }, + //Do a fresh re-serialization of the RDF XML, in case any pids in the package have changed. + //The hope is that any errors during the serialization process have already been caught during the first serialization above + try { + var mapXML = this.serialize(); + } catch (serializationException) { + //Cancel the save and show an error message + this.packageModel.set("changed", false); + this.packageModel.set("uploadStatus", "q"); + this.trigger( + "errorSaving", + "There was a Javascript error during the serialization process: " + + serializationException, + ); + return; + } - /* - * When a data package member updates, we evaluate it for its formatid, - * and update it appropriately if it is not a data object only - */ - getMember: function (context, args) { - var memberModel = {}; - - switch (context.get("formatId")) { - case "http://www.openarchives.org/ore/terms": - context.attributes.id = context.id; - context.attributes.type = "DataPackage"; - context.attributes.childPackages = {}; - memberModel = new DataPackage(null, { - packageModel: context.attributes, - }); - this.packageModel.get("childPackages")[ - memberModel.packageModel.id - ] = memberModel; - break; - - case "eml://ecoinformatics.org/eml-2.0.0": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new EML211(context.attributes); - break; - - case "eml://ecoinformatics.org/eml-2.0.1": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new EML211(context.attributes); - break; - - case "eml://ecoinformatics.org/eml-2.1.0": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new EML211(context.attributes); - break; - - case "eml://ecoinformatics.org/eml-2.1.1": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new EML211(context.attributes); - break; - - case "https://eml.ecoinformatics.org/eml-2.2.0": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new EML211(context.attributes); - break; - - case "-//ecoinformatics.org//eml-access-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-access-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-attribute-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-attribute-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-constraint-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-constraint-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-coverage-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-coverage-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-dataset-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-dataset-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-distribution-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-distribution-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-entity-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-entity-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-literature-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-literature-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-party-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-party-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-physical-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-physical-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-project-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-project-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-protocol-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-protocol-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-resource-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-resource-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-software-2.0.0beta4//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "FGDC-STD-001-1998": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "FGDC-STD-001.1-1999": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "FGDC-STD-001.2-1999": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "INCITS-453-2009": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "ddi:codebook:2_5": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://datacite.org/schema/kernel-3.0": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://datacite.org/schema/kernel-3.1": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://datadryad.org/profile/v3.1": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://ns.dataone.org/metadata/schema/onedcx/v1.0": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://purl.org/dryad/terms/": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://purl.org/ornl/schema/mercury/terms/v1.0": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://rs.tdwg.org/dwc/xsd/simpledarwincore/": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://www.cuahsi.org/waterML/1.0/": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://www.cuahsi.org/waterML/1.1/": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://www.esri.com/metadata/esriprof80.dtd": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://www.icpsr.umich.edu/DDI": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://www.isotc211.org/2005/gmd": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://www.isotc211.org/2005/gmd-noaa": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://www.loc.gov/METS/": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - case "http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2": - context.set({ type: "Metadata", sortOrder: 1 }); - memberModel = new ScienceMetadata(context.attributes); - break; - - default: - // For other data formats, keep just the DataONEObject sysmeta - context.set({ type: "Data", sortOrder: 2 }); - memberModel = context; - } + //Make a Blob object from the serialized RDF XML + var mapBlob = new Blob([mapXML], { type: "application/xml" }); + + //Get the size of the new resource map + this.packageModel.set("size", mapBlob.size); + + //Get the new checksum of the resource map + var checksum = md5(mapXML); + this.packageModel.set("checksum", checksum); + this.packageModel.set("checksumAlgorithm", "MD5"); + + //Set the file name based on the id + this.packageModel.set( + "fileName", + this.packageModel.get("id").replace(/[^a-zA-Z0-9]/g, "_") + + ".rdf.xml", + ); + + //Create the system metadata + var sysMetaXML = this.packageModel.serializeSysMeta(); + + //Send the system metadata + var xmlBlob = new Blob([sysMetaXML], { + type: "application/xml", + }); + + //Add the object XML and System Metadata XML to the form data + //Append the system metadata first, so we can take advantage of Metacat's streaming multipart handler + formData.append("sysmeta", xmlBlob, "sysmeta"); + formData.append("object", mapBlob); + + var collection = this; + var requestSettings = { + url: this.packageModel.isNew() + ? this.url() + : this.url({ update: true }), + type: requestType, + cache: false, + contentType: false, + processData: false, + data: formData, + success: function (response) { + //Update the object XML + collection.objectXML = mapXML; + collection.packageModel.set( + "sysMetaXML", + collection.packageModel.serializeSysMeta(), + ); + + //Reset the upload status for all members + _.each(collection.where({ uploadStatus: "c" }), function (m) { + m.set("uploadStatus", m.defaults().uploadStatus); + }); + + // Reset oldPid to null so we know we need to update the ID + // in the future + collection.packageModel.set("oldPid", null); + + //Reset the upload status for the package + collection.packageModel.set( + "uploadStatus", + collection.packageModel.defaults().uploadStatus, + ); + + // Reset the content changes status + collection.packageModel.set("hasContentChanges", false); + + // This package is no longer new, so mark it as such + collection.packageModel.set("isNew", false); + + collection.trigger("successSaving", collection); + + collection.packageModel.fetch({ merge: true }); + + _.each(sysMetaToUpdate, function (dataModel) { + dataModel.set("sysMetaUploadStatus", "c"); + }); + }, + error: function (data) { + //Reset the id back to its original state + collection.packageModel.resetID(); + + //Reset the upload status for all members + _.each(collection.where({ uploadStatus: "c" }), function (m) { + m.set("uploadStatus", m.defaults().uploadStatus); + }); + + //When there is no network connection (status == 0), there will be no response text + if (data.status == 408 || data.status == 0) { + var parsedResponse = + "There was a network issue that prevented this file from uploading. " + + "Make sure you are connected to a reliable internet connection."; + } else { + var parsedResponse = $(data.responseText) + .not("style, title") + .text(); + } + + //Save the error message in the model + collection.packageModel.set("errorMessage", parsedResponse); + + //Reset the upload status for the package + collection.packageModel.set("uploadStatus", "e"); + + collection.trigger("errorSaving", parsedResponse); + + // Track this error in our analytics + MetacatUI.analytics?.trackException( + `DataPackage save error: ${parsedResponse}`, + collection.packageModel.get("id"), + true, + ); + }, + }; + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + }, + + /* + * When a data package member updates, we evaluate it for its formatid, + * and update it appropriately if it is not a data object only + */ + getMember: function (context, args) { + var memberModel = {}; + + switch (context.get("formatId")) { + case "http://www.openarchives.org/ore/terms": + context.attributes.id = context.id; + context.attributes.type = "DataPackage"; + context.attributes.childPackages = {}; + memberModel = new DataPackage(null, { + packageModel: context.attributes, + }); + this.packageModel.get("childPackages")[ + memberModel.packageModel.id + ] = memberModel; + break; + + case "eml://ecoinformatics.org/eml-2.0.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; + + case "eml://ecoinformatics.org/eml-2.0.1": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; + + case "eml://ecoinformatics.org/eml-2.1.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; + + case "eml://ecoinformatics.org/eml-2.1.1": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; + + case "https://eml.ecoinformatics.org/eml-2.2.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; + + case "-//ecoinformatics.org//eml-access-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-access-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-attribute-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-attribute-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-constraint-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-constraint-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-coverage-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-coverage-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-dataset-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-dataset-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-distribution-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-distribution-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-entity-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-entity-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-literature-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-literature-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-party-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-party-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-physical-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-physical-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-project-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-project-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-protocol-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-protocol-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-resource-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-resource-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-software-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "FGDC-STD-001-1998": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "FGDC-STD-001.1-1999": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "FGDC-STD-001.2-1999": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "INCITS-453-2009": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "ddi:codebook:2_5": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://datacite.org/schema/kernel-3.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://datacite.org/schema/kernel-3.1": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://datadryad.org/profile/v3.1": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://ns.dataone.org/metadata/schema/onedcx/v1.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://purl.org/dryad/terms/": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://purl.org/ornl/schema/mercury/terms/v1.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://rs.tdwg.org/dwc/xsd/simpledarwincore/": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://www.cuahsi.org/waterML/1.0/": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://www.cuahsi.org/waterML/1.1/": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://www.esri.com/metadata/esriprof80.dtd": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://www.icpsr.umich.edu/DDI": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://www.isotc211.org/2005/gmd": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://www.isotc211.org/2005/gmd-noaa": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://www.loc.gov/METS/": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + case "http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; + + default: + // For other data formats, keep just the DataONEObject sysmeta + context.set({ type: "Data", sortOrder: 2 }); + memberModel = context; + } - if (memberModel.type == "DataPackage") { - // We have a nested collection - memberModel.packageModel.set( - "nodeLevel", - this.packageModel.get("nodeLevel") + 1 - ); - } else { - // We have a model - memberModel.set( - "nodeLevel", - this.packageModel.get("nodeLevel") - ); // same level for all members - } + if (memberModel.type == "DataPackage") { + // We have a nested collection + memberModel.packageModel.set( + "nodeLevel", + this.packageModel.get("nodeLevel") + 1, + ); + } else { + // We have a model + memberModel.set("nodeLevel", this.packageModel.get("nodeLevel")); // same level for all members + } - return memberModel; - }, + return memberModel; + }, - triggerComplete: function (model) { - //If the last fetch did not fetch the models of the collection, then mark as complete now. - if (this.fetchModels === false) { - // Delete the fetchModels property since it is set only once per fetch. - delete this.fetchModels; + triggerComplete: function (model) { + //If the last fetch did not fetch the models of the collection, then mark as complete now. + if (this.fetchModels === false) { + // Delete the fetchModels property since it is set only once per fetch. + delete this.fetchModels; - this.trigger("complete", this); + this.trigger("complete", this); - return; - } + return; + } - //Check if the collection is done being retrieved - var notSynced = this.reject(function (m) { - return m.get("synced") || m.get("id") == model.get("id"); - }); + //Check if the collection is done being retrieved + var notSynced = this.reject(function (m) { + return m.get("synced") || m.get("id") == model.get("id"); + }); - //If there are any models that are not synced yet, the collection is not complete - if (notSynced.length > 0) { - return; - } + //If there are any models that are not synced yet, the collection is not complete + if (notSynced.length > 0) { + return; + } - //If the number of models in this collection does not equal the number of objects referenced in the RDF XML, the collection is not complete - if (this.originalMembers.length > this.length) return; + //If the number of models in this collection does not equal the number of objects referenced in the RDF XML, the collection is not complete + if (this.originalMembers.length > this.length) return; - this.sort(); - this.trigger("complete", this); - }, + this.sort(); + this.trigger("complete", this); + }, - /* Accumulate edits that are made to the provenance relationships via the ProvChartView. these + /* Accumulate edits that are made to the provenance relationships via the ProvChartView. these edits are accumulated here so that they are available to any package member or view. */ - recordProvEdit: function (operation, subject, predicate, object) { - if (!this.provEdits.length) { - this.provEdits = [[operation, subject, predicate, object]]; - } else { - // First check if the edit already exists in the list. If yes, then - // don't add it again! This could occur if an edit icon was clicked rapidly - // before it is dismissed. - var editFound = _.find(this.provEdits, function (edit) { - return ( - edit[0] == operation && - edit[1] == subject && - edit[2] == predicate && - edit[3] == object - ); - }); - - if (typeof editFound != "undefined") { - return; - } - - // If this is a delete operation, then check if a matching operation - // is in the edit list (i.e. the user may have changed their mind, and - // they just want to cancel an edit). If yes, then just delete the - // matching add edit request - var editListSize = this.provEdits.length; - var oppositeOp = operation == "delete" ? "add" : "delete"; - - this.provEdits = _.reject(this.provEdits, function (edit) { - var editOperation = edit[0]; - var editSubjectId = edit[1]; - var editPredicate = edit[2]; - var editObject = edit[3]; - if ( - editOperation == oppositeOp && - editSubjectId == subject && - editPredicate == predicate && - editObject == object - ) { - return true; - } - }); + recordProvEdit: function (operation, subject, predicate, object) { + if (!this.provEdits.length) { + this.provEdits = [[operation, subject, predicate, object]]; + } else { + // First check if the edit already exists in the list. If yes, then + // don't add it again! This could occur if an edit icon was clicked rapidly + // before it is dismissed. + var editFound = _.find(this.provEdits, function (edit) { + return ( + edit[0] == operation && + edit[1] == subject && + edit[2] == predicate && + edit[3] == object + ); + }); + + if (typeof editFound != "undefined") { + return; + } + + // If this is a delete operation, then check if a matching operation + // is in the edit list (i.e. the user may have changed their mind, and + // they just want to cancel an edit). If yes, then just delete the + // matching add edit request + var editListSize = this.provEdits.length; + var oppositeOp = operation == "delete" ? "add" : "delete"; + + this.provEdits = _.reject(this.provEdits, function (edit) { + var editOperation = edit[0]; + var editSubjectId = edit[1]; + var editPredicate = edit[2]; + var editObject = edit[3]; + if ( + editOperation == oppositeOp && + editSubjectId == subject && + editPredicate == predicate && + editObject == object + ) { + return true; + } + }); + + // If we cancelled out edit containing inverse of the current edit + // then the edit list will now be one edit shorter. Test for this + // and only save the current edit if we didn't remove the inverse. + if (editListSize >= this.provEdits.length) { + this.provEdits.push([operation, subject, predicate, object]); + } + } + }, - // If we cancelled out edit containing inverse of the current edit - // then the edit list will now be one edit shorter. Test for this - // and only save the current edit if we didn't remove the inverse. - if (editListSize >= this.provEdits.length) { - this.provEdits.push([ - operation, - subject, - predicate, - object, - ]); - } - } - }, + // Return true if the prov edits list is not empty + provEditsPending: function () { + if (this.provEdits.length) return true; + return false; + }, - // Return true if the prov edits list is not empty - provEditsPending: function () { - if (this.provEdits.length) return true; - return false; - }, - - /* If provenance relationships have been modified by the provenance editor (in ProvChartView), then + /* If provenance relationships have been modified by the provenance editor (in ProvChartView), then update the ORE Resource Map and save it to the server. */ - saveProv: function () { - var rdf = this.rdf; - var graph = this.dataPackageGraph; + saveProv: function () { + var rdf = this.rdf; + var graph = this.dataPackageGraph; - var provEdits = this.provEdits; - if (!provEdits.length) { - return; - } - var RDF = rdf.Namespace(this.namespaces.RDF), - PROV = rdf.Namespace(this.namespaces.PROV), - PROVONE = rdf.Namespace(this.namespaces.PROVONE), - DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), - CITO = rdf.Namespace(this.namespaces.CITO), - XSD = rdf.Namespace(this.namespaces.XSD); + var provEdits = this.provEdits; + if (!provEdits.length) { + return; + } + var RDF = rdf.Namespace(this.namespaces.RDF), + PROV = rdf.Namespace(this.namespaces.PROV), + PROVONE = rdf.Namespace(this.namespaces.PROVONE), + DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), + CITO = rdf.Namespace(this.namespaces.CITO), + XSD = rdf.Namespace(this.namespaces.XSD); - var cnResolveUrl = this.getCnURI(); + var cnResolveUrl = this.getCnURI(); - /* Check if this package member had provenance relationships added + /* Check if this package member had provenance relationships added or deleted by the provenance editor functionality of the ProvChartView */ - _.each( - provEdits, - function (edit) { - var operation, subject, predicate, object; - var provStatements; - operation = edit[0]; - subject = edit[1]; - predicate = edit[2]; - object = edit[3]; - - // The predicates of the provenance edits recorded by the ProvChartView - // indicate which W3C PROV relationship has been recorded. - // First check if this relationship alread exists in the RDF graph. - // See DataPackage.parseProv for a description of how relationships from an ORE resource map - // are parsed and stored in DataONEObjects. Here we are reversing the process, so may need - // The representation of the PROVONE data model is simplified in the ProvChartView, to aid - // legibility for users not familiar with the details of the PROVONE model. In this simplification, - // a provone:Program has direct inputs and outputs. In the actual model, a prov:Execution has - // inputs and outputs and is connected to a program via a prov:association. We must 'expand' the - // simplified provenance updates recorded by the editor into the fully detailed representation - // of the actual model. - var executionId, executionURI, executionNode; - var programId, programURI, programNode; - var dataId, dataURI, dataNode; - var derivedDataURI, derivedDataNode; - var lastRef = false; - //var graph = this.dataPackageGraph; - - //Create a node for the subject and object - var subjectNode = rdf.sym(this.getURIFromRDF(subject)), - objectNode = rdf.sym(this.getURIFromRDF(object)); - - switch (predicate) { - case "prov_wasDerivedFrom": - derivedDataNode = subjectNode; - dataNode = objectNode; - if (operation == "add") { - this.addToGraph( - dataNode, - RDF("type"), - PROVONE("Data") - ); - this.addToGraph( - derivedDataNode, - RDF("type"), - PROVONE("Data") - ); - this.addToGraph( - derivedDataNode, - PROV("wasDerivedFrom"), - dataNode - ); - } else { - graph.removeMatches( - derivedDataNode, - PROV("wasDerivedFrom"), - dataNode - ); - this.removeIfLastProvRef( - dataNode, - RDF("type"), - PROVONE("Data") - ); - this.removeIfLastProvRef( - derivedDataNode, - RDF("type"), - PROVONE("Data") - ); - } - break; - case "prov_generatedByProgram": - programId = object; - dataNode = subjectNode; - var removed = false; - if (operation == "add") { - // 'subject' is the program id, which is a simplification of the PROVONE model for display. - // In the PROVONE model, execution 'uses' and input, and is associated with a program. - executionId = - this.addProgramToGraph(programId); - //executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); - executionNode = - this.getExecutionNode(executionId); - this.addToGraph( - dataNode, - RDF("type"), - PROVONE("Data") - ); - this.addToGraph( - dataNode, - PROV("wasGeneratedBy"), - executionNode - ); - } else { - executionId = - this.getExecutionId(programId); - executionNode = - this.getExecutionNode(executionId); - - graph.removeMatches( - dataNode, - PROV("wasGeneratedBy"), - executionNode - ); - removed = - this.removeProgramFromGraph(programId); - this.removeIfLastProvRef( - dataNode, - RDF("type"), - PROVONE("Data") - ); - } - break; - case "prov_usedByProgram": - programId = object; - dataNode = subjectNode; - if (operation == "add") { - // 'subject' is the program id, which is a simplification of the PROVONE model for display. - // In the PROVONE model, execution 'uses' and input, and is associated with a program. - executionId = - this.addProgramToGraph(programId); - //executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); - executionNode = - this.getExecutionNode(executionId); - this.addToGraph( - dataNode, - RDF("type"), - PROVONE("Data") - ); - this.addToGraph( - executionNode, - PROV("used"), - dataNode - ); - } else { - executionId = - this.getExecutionId(programId); - executionNode = - this.getExecutionNode(executionId); - - graph.removeMatches( - executionNode, - PROV("used"), - dataNode - ); - removed = - this.removeProgramFromGraph(programId); - this.removeIfLastProvRef( - dataNode, - RDF("type"), - PROVONE("Data") - ); - } - break; - case "prov_hasDerivations": - dataNode = subjectNode; - derivedDataNode = objectNode; - if (operation == "add") { - this.addToGraph( - dataNode, - RDF("type"), - PROVONE("Data") - ); - this.addToGraph( - derivedDataNode, - RDF("type"), - PROVONE("Data") - ); - this.addToGraph( - derivedDataNode, - PROV("wasDerivedFrom"), - dataNode - ); - } else { - graph.removeMatches( - derivedDataNode, - PROV("wasDerivedFrom"), - dataNode - ); - this.removeIfLastProvRef( - dataNode, - RDF("type"), - PROVONE("Data") - ); - this.removeIfLastProvRef( - derivedDataNode, - RDF("type"), - PROVONE("Data") - ); - } - break; - case "prov_instanceOfClass": - var entityNode = subjectNode; - var classNode = PROVONE(object); - if (operation == "add") { - this.addToGraph( - entityNode, - RDF("type"), - classNode - ); - } else { - // Make sure there are no other references to this - this.removeIfLastProvRef( - entityNode, - RDF("type"), - classNode - ); - } - break; - default: - // Print error if predicate for prov edit not found. - } - }, - this - ); - - // When saving provenance only, we only have to save the Resource Map/Package object. - // So we will send the resourceMapOnly flag with the save function. - this.save({ - resourceMapOnly: true, - }); - }, - - /* Add the specified relationship to the RDF graph only if it + _.each( + provEdits, + function (edit) { + var operation, subject, predicate, object; + var provStatements; + operation = edit[0]; + subject = edit[1]; + predicate = edit[2]; + object = edit[3]; + + // The predicates of the provenance edits recorded by the ProvChartView + // indicate which W3C PROV relationship has been recorded. + // First check if this relationship alread exists in the RDF graph. + // See DataPackage.parseProv for a description of how relationships from an ORE resource map + // are parsed and stored in DataONEObjects. Here we are reversing the process, so may need + // The representation of the PROVONE data model is simplified in the ProvChartView, to aid + // legibility for users not familiar with the details of the PROVONE model. In this simplification, + // a provone:Program has direct inputs and outputs. In the actual model, a prov:Execution has + // inputs and outputs and is connected to a program via a prov:association. We must 'expand' the + // simplified provenance updates recorded by the editor into the fully detailed representation + // of the actual model. + var executionId, executionURI, executionNode; + var programId, programURI, programNode; + var dataId, dataURI, dataNode; + var derivedDataURI, derivedDataNode; + var lastRef = false; + //var graph = this.dataPackageGraph; + + //Create a node for the subject and object + var subjectNode = rdf.sym(this.getURIFromRDF(subject)), + objectNode = rdf.sym(this.getURIFromRDF(object)); + + switch (predicate) { + case "prov_wasDerivedFrom": + derivedDataNode = subjectNode; + dataNode = objectNode; + if (operation == "add") { + this.addToGraph(dataNode, RDF("type"), PROVONE("Data")); + this.addToGraph( + derivedDataNode, + RDF("type"), + PROVONE("Data"), + ); + this.addToGraph( + derivedDataNode, + PROV("wasDerivedFrom"), + dataNode, + ); + } else { + graph.removeMatches( + derivedDataNode, + PROV("wasDerivedFrom"), + dataNode, + ); + this.removeIfLastProvRef( + dataNode, + RDF("type"), + PROVONE("Data"), + ); + this.removeIfLastProvRef( + derivedDataNode, + RDF("type"), + PROVONE("Data"), + ); + } + break; + case "prov_generatedByProgram": + programId = object; + dataNode = subjectNode; + var removed = false; + if (operation == "add") { + // 'subject' is the program id, which is a simplification of the PROVONE model for display. + // In the PROVONE model, execution 'uses' and input, and is associated with a program. + executionId = this.addProgramToGraph(programId); + //executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); + executionNode = this.getExecutionNode(executionId); + this.addToGraph(dataNode, RDF("type"), PROVONE("Data")); + this.addToGraph( + dataNode, + PROV("wasGeneratedBy"), + executionNode, + ); + } else { + executionId = this.getExecutionId(programId); + executionNode = this.getExecutionNode(executionId); + + graph.removeMatches( + dataNode, + PROV("wasGeneratedBy"), + executionNode, + ); + removed = this.removeProgramFromGraph(programId); + this.removeIfLastProvRef( + dataNode, + RDF("type"), + PROVONE("Data"), + ); + } + break; + case "prov_usedByProgram": + programId = object; + dataNode = subjectNode; + if (operation == "add") { + // 'subject' is the program id, which is a simplification of the PROVONE model for display. + // In the PROVONE model, execution 'uses' and input, and is associated with a program. + executionId = this.addProgramToGraph(programId); + //executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); + executionNode = this.getExecutionNode(executionId); + this.addToGraph(dataNode, RDF("type"), PROVONE("Data")); + this.addToGraph(executionNode, PROV("used"), dataNode); + } else { + executionId = this.getExecutionId(programId); + executionNode = this.getExecutionNode(executionId); + + graph.removeMatches(executionNode, PROV("used"), dataNode); + removed = this.removeProgramFromGraph(programId); + this.removeIfLastProvRef( + dataNode, + RDF("type"), + PROVONE("Data"), + ); + } + break; + case "prov_hasDerivations": + dataNode = subjectNode; + derivedDataNode = objectNode; + if (operation == "add") { + this.addToGraph(dataNode, RDF("type"), PROVONE("Data")); + this.addToGraph( + derivedDataNode, + RDF("type"), + PROVONE("Data"), + ); + this.addToGraph( + derivedDataNode, + PROV("wasDerivedFrom"), + dataNode, + ); + } else { + graph.removeMatches( + derivedDataNode, + PROV("wasDerivedFrom"), + dataNode, + ); + this.removeIfLastProvRef( + dataNode, + RDF("type"), + PROVONE("Data"), + ); + this.removeIfLastProvRef( + derivedDataNode, + RDF("type"), + PROVONE("Data"), + ); + } + break; + case "prov_instanceOfClass": + var entityNode = subjectNode; + var classNode = PROVONE(object); + if (operation == "add") { + this.addToGraph(entityNode, RDF("type"), classNode); + } else { + // Make sure there are no other references to this + this.removeIfLastProvRef(entityNode, RDF("type"), classNode); + } + break; + default: + // Print error if predicate for prov edit not found. + } + }, + this, + ); + + // When saving provenance only, we only have to save the Resource Map/Package object. + // So we will send the resourceMapOnly flag with the save function. + this.save({ + resourceMapOnly: true, + }); + }, + + /* Add the specified relationship to the RDF graph only if it has not already been added. */ - addToGraph: function (subject, predicate, object) { - var graph = this.dataPackageGraph; - var statements = graph.statementsMatching( - subject, - predicate, - object - ); + addToGraph: function (subject, predicate, object) { + var graph = this.dataPackageGraph; + var statements = graph.statementsMatching(subject, predicate, object); - if (!statements.length) { - graph.add(subject, predicate, object); - } - }, + if (!statements.length) { + graph.add(subject, predicate, object); + } + }, - /* Remove the statement fromn the RDF graph only if the subject of this + /* Remove the statement fromn the RDF graph only if the subject of this relationship is not referenced by any other provenance relationship, i.e. for example, the prov relationship "id rdf:type provone:data" is only needed if the subject ('id') is referenced in another relationship. Also don't remove it if the subject is in any other prov statement, meaning it still references another prov object. */ - removeIfLastProvRef: function ( - subjectNode, - predicateNode, - objectNode - ) { - var graph = this.dataPackageGraph; - var stillUsed = false; - var PROV = rdf.Namespace(this.namespaces.PROV); - var PROVONE = rdf.Namespace(this.namespaces.PROVONE); - // PROV namespace value, used to identify PROV statements - var provStr = PROV("").value; - // PROVONE namespace value, used to identify PROVONE statements - var provoneStr = PROVONE("").value; - // Get the statements from the RDF graph that reference the subject of the - // statement to remove. - var statements = graph.statementsMatching( - undefined, - undefined, - subjectNode - ); - - var found = _.find( - statements, - function (statement) { - if ( - statement.subject == subjectNode && - statement.predicate == predicateNode && - statement.object == objectNode - ) - return false; - - var pVal = statement.predicate.value; - - // Now check if the subject is referenced in a prov statement - // There is another statement that references the subject of the - // statement to remove, so it is still being used and don't - // remove it. - if (pVal.indexOf(provStr) != -1) return true; - if (pVal.indexOf(provoneStr) != -1) return true; - return false; - }, - this - ); - - // IF not found in the first test, keep looking. - if (typeof found == "undefined") { - // Get the statements from the RDF where - var statements = graph.statementsMatching( - subjectNode, - undefined, - undefined - ); - - found = _.find( - statements, - function (statement) { - if ( - statement.subject == subjectNode && - statement.predicate == predicateNode && - statement.object == objectNode - ) - return false; - var pVal = statement.predicate.value; - - // Now check if the subject is referenced in a prov statement - if (pVal.indexOf(provStr) != -1) return true; - if (pVal.indexOf(provoneStr) != -1) return true; - // There is another statement that references the subject of the - // statement to remove, so it is still being used and don't - // remove it. - return false; - }, - this - ); - } - - // The specified statement term isn't being used for prov, so remove it. - if (typeof found == "undefined") { - graph.removeMatches( - subjectNode, - predicateNode, - objectNode, - undefined - ); - } + removeIfLastProvRef: function (subjectNode, predicateNode, objectNode) { + var graph = this.dataPackageGraph; + var stillUsed = false; + var PROV = rdf.Namespace(this.namespaces.PROV); + var PROVONE = rdf.Namespace(this.namespaces.PROVONE); + // PROV namespace value, used to identify PROV statements + var provStr = PROV("").value; + // PROVONE namespace value, used to identify PROVONE statements + var provoneStr = PROVONE("").value; + // Get the statements from the RDF graph that reference the subject of the + // statement to remove. + var statements = graph.statementsMatching( + undefined, + undefined, + subjectNode, + ); + + var found = _.find( + statements, + function (statement) { + if ( + statement.subject == subjectNode && + statement.predicate == predicateNode && + statement.object == objectNode + ) + return false; + + var pVal = statement.predicate.value; + + // Now check if the subject is referenced in a prov statement + // There is another statement that references the subject of the + // statement to remove, so it is still being used and don't + // remove it. + if (pVal.indexOf(provStr) != -1) return true; + if (pVal.indexOf(provoneStr) != -1) return true; + return false; + }, + this, + ); + + // IF not found in the first test, keep looking. + if (typeof found == "undefined") { + // Get the statements from the RDF where + var statements = graph.statementsMatching( + subjectNode, + undefined, + undefined, + ); + + found = _.find( + statements, + function (statement) { + if ( + statement.subject == subjectNode && + statement.predicate == predicateNode && + statement.object == objectNode + ) + return false; + var pVal = statement.predicate.value; + + // Now check if the subject is referenced in a prov statement + if (pVal.indexOf(provStr) != -1) return true; + if (pVal.indexOf(provoneStr) != -1) return true; + // There is another statement that references the subject of the + // statement to remove, so it is still being used and don't + // remove it. + return false; }, + this, + ); + } - /** - * Remove orphaned blank nodes from the model's current graph - * - * This was put in to support replacing package members who are - * referenced by provenance statements, specifically members typed as - * Programs. rdflib.js will throw an error when serializing if any - * statements in the graph have objects that are blank nodes when no - * other statements in the graph have subjects for the same blank node. - * i.e., blank nodes references that aren't defined. - * - * Should be called during a call to serialize() and mutates - * this.dataPackageGraph directly as a side-effect. - */ - removeOrphanedBlankNodes: function () { - if ( - !this.dataPackageGraph || - !this.dataPackageGraph.statements - ) { - return; - } - - // Collect an array of statements to be removed - var toRemove = []; - - _.each( - this.dataPackageGraph.statements, - function (statement) { - if (statement.object.termType !== "BlankNode") { - return; - } - - // For this statement, look for other statments about it - var matches = 0; - - _.each( - this.dataPackageGraph.statements, - function (other) { - if ( - other.subject.termType === "BlankNode" && - other.subject.id === statement.object.id - ) { - matches += 1; - } - } - ); - - // If none are found, add it to our list - if (matches === 0) { - toRemove.push(statement); - } - }, - this - ); - - // Remove collected statements - _.each( - toRemove, - function (statement) { - this.dataPackageGraph.removeStatement(statement); - }, - this - ); - }, + // The specified statement term isn't being used for prov, so remove it. + if (typeof found == "undefined") { + graph.removeMatches( + subjectNode, + predicateNode, + objectNode, + undefined, + ); + } + }, + + /** + * Remove orphaned blank nodes from the model's current graph + * + * This was put in to support replacing package members who are + * referenced by provenance statements, specifically members typed as + * Programs. rdflib.js will throw an error when serializing if any + * statements in the graph have objects that are blank nodes when no + * other statements in the graph have subjects for the same blank node. + * i.e., blank nodes references that aren't defined. + * + * Should be called during a call to serialize() and mutates + * this.dataPackageGraph directly as a side-effect. + */ + removeOrphanedBlankNodes: function () { + if (!this.dataPackageGraph || !this.dataPackageGraph.statements) { + return; + } - /* Get the execution identifier that is associated with a program id. + // Collect an array of statements to be removed + var toRemove = []; + + _.each( + this.dataPackageGraph.statements, + function (statement) { + if (statement.object.termType !== "BlankNode") { + return; + } + + // For this statement, look for other statments about it + var matches = 0; + + _.each(this.dataPackageGraph.statements, function (other) { + if ( + other.subject.termType === "BlankNode" && + other.subject.id === statement.object.id + ) { + matches += 1; + } + }); + + // If none are found, add it to our list + if (matches === 0) { + toRemove.push(statement); + } + }, + this, + ); + + // Remove collected statements + _.each( + toRemove, + function (statement) { + this.dataPackageGraph.removeStatement(statement); + }, + this, + ); + }, + + /* Get the execution identifier that is associated with a program id. This will either be in the 'prov_wasExecutedByExecution' of the package member for the program script, or available by tracing backward in the RDF graph from the program node, through the assocation to the related execution. */ - getExecutionId: function (programId) { - var rdf = this.rdf; - var graph = this.dataPackageGraph; - var stmts = null; - var cnResolveUrl = this.getCnURI(); - var RDF = rdf.Namespace(this.namespaces.RDF), - DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), - PROV = rdf.Namespace(this.namespaces.PROV), - PROVONE = rdf.Namespace(this.namespaces.PROVONE); - - var member = this.get(programId); - var executionId = member.get("prov_wasExecutedByExecution"); - if (executionId.length > 0) { - return executionId[0]; - } else { - var programNode = rdf.sym(this.getURIFromRDF(programId)); - // Get the executionId from the RDF graph - // There can be only one plan for an association - stmts = graph.statementsMatching( - undefined, - PROV("hadPlan"), - programNode - ); - if (typeof stmts == "undefined") return null; - var associationNode = stmts[0].subject; - // There should be only one execution for this assocation. - stmts = graph.statementsMatching( - undefined, - PROV("qualifiedAssociation"), - associationNode - ); - if (typeof stmts == "undefined") return null; - return stmts[0].subject; - } - }, + getExecutionId: function (programId) { + var rdf = this.rdf; + var graph = this.dataPackageGraph; + var stmts = null; + var cnResolveUrl = this.getCnURI(); + var RDF = rdf.Namespace(this.namespaces.RDF), + DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), + PROV = rdf.Namespace(this.namespaces.PROV), + PROVONE = rdf.Namespace(this.namespaces.PROVONE); + + var member = this.get(programId); + var executionId = member.get("prov_wasExecutedByExecution"); + if (executionId.length > 0) { + return executionId[0]; + } else { + var programNode = rdf.sym(this.getURIFromRDF(programId)); + // Get the executionId from the RDF graph + // There can be only one plan for an association + stmts = graph.statementsMatching( + undefined, + PROV("hadPlan"), + programNode, + ); + if (typeof stmts == "undefined") return null; + var associationNode = stmts[0].subject; + // There should be only one execution for this assocation. + stmts = graph.statementsMatching( + undefined, + PROV("qualifiedAssociation"), + associationNode, + ); + if (typeof stmts == "undefined") return null; + return stmts[0].subject; + } + }, - /* Get the RDF node for an execution that is associated with the execution identifier. + /* Get the RDF node for an execution that is associated with the execution identifier. The execution may have been created in the resource map as a 'bare' urn:uuid (no resolveURI), or as a resolve URL, so check for both until the id is found. */ - getExecutionNode: function (executionId) { - var rdf = this.rdf; - var graph = this.dataPackageGraph; - var stmts = null; - var testNode = null; - var cnResolveUrl = this.getCnURI(); - - // First see if the execution exists in the RDF graph as a 'bare' idenfier, i.e. - // a 'urn:uuid'. - stmts = graph.statementsMatching( - rdf.sym(executionId), - undefined, - undefined - ); - if (typeof stmts == "undefined" || !stmts.length) { - // The execution node as urn was not found, look for fully qualified version. - testNode = rdf.sym(this.getURIFromRDF(executionId)); - stmts = graph.statementsMatching( - rdf.sym(executionId), - undefined, - undefined - ); - if (typeof stmts == "undefined") { - // Couldn't find the execution, return the standard RDF node value - executionNode = rdf.sym( - this.getURIFromRDF(executionId) - ); - return executionNode; - } else { - return testNode; - } - } else { - // The executionNode was found in the RDF graph as a urn - var executionNode = stmts[0].subject; - return executionNode; - } - }, - - addProgramToGraph: function (programId) { - var rdf = this.rdf; - var graph = this.dataPackageGraph; - var RDF = rdf.Namespace(this.namespaces.RDF), - DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), - PROV = rdf.Namespace(this.namespaces.PROV), - PROVONE = rdf.Namespace(this.namespaces.PROVONE), - XSD = rdf.Namespace(this.namespaces.XSD); - var member = this.get(programId); - var executionId = member.get("prov_wasExecutedByExecution"); - var executionNode = null; - var programNode = null; - var associationId = null; - var associationNode = null; - var cnResolveUrl = this.getCnURI(); - - if (!executionId.length) { - // This is a new execution, so create new execution and association ids - executionId = "urn:uuid:" + uuid.v4(); - member.set("prov_wasExecutedByExecution", [executionId]); - // Blank node id. RDF validator doesn't like ':' so don't use in the id - //executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); - executionNode = this.getExecutionNode(executionId); - //associationId = "_" + uuid.v4(); - associationNode = graph.bnode(); - } else { - executionId = executionId[0]; - // Check if an association exists in the RDF graph for this execution id - //executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); - executionNode = this.getExecutionNode(executionId); - // Check if there is an association id for this execution. - // If this execution is newly created (via the editor (existing would - // be parsed from the resmap), then create a new association id. - var stmts = graph.statementsMatching( - executionNode, - PROV("qualifiedAssociation"), - undefined - ); - // IF an associati on was found, then use it, else geneate a new one - // (Associations aren't stored in the ) - if (stmts.length) { - associationNode = stmts[0].object; - //associationId = stmts[0].object.value; - } else { - //associationId = "_" + uuid.v4(); - associationNode = graph.bnode(); - } - } - //associationNode = graph.bnode(associationId); - //associationNode = graph.bnode(); - programNode = rdf.sym(this.getURIFromRDF(programId)); - try { - this.addToGraph( - executionNode, - PROV("qualifiedAssociation"), - associationNode - ); - this.addToGraph( - executionNode, - RDF("type"), - PROVONE("Execution") - ); - this.addToGraph( - executionNode, - DCTERMS("identifier"), - rdf.literal(executionId, undefined, XSD("string")) - ); - this.addToGraph( - associationNode, - PROV("hadPlan"), - programNode - ); - this.addToGraph( - programNode, - RDF("type"), - PROVONE("Program") - ); - } catch (error) { - console.log(error); - } - return executionId; - }, - - // Remove a program identifier from the RDF graph and remove associated - // linkage between the program id and the exection, if the execution is not - // being used by any other statements. - removeProgramFromGraph: function (programId) { - var graph = this.dataPackageGraph; - var rdf = this.rdf; - var stmts = null; - var cnResolveUrl = this.getCnURI(); - var RDF = rdf.Namespace(this.namespaces.RDF), - DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), - PROV = rdf.Namespace(this.namespaces.PROV), - PROVONE = rdf.Namespace(this.namespaces.PROVONE), - XSD = rdf.Namespace(this.namespaces.XSD); - var associationNode = null; - - var executionId = this.getExecutionId(programId); - if (executionId == null) return false; - - //var executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); - var executionNode = this.getExecutionNode(executionId); - var programNode = rdf.sym(this.getURIFromRDF(programId)); - - // In order to remove this program from the graph, we have to first determine that - // nothing else is using the execution that is associated with the program (the plan). - // There may be additional 'used', 'geneated', 'qualifiedGeneration', etc. items that - // may be pointing to the execution. If yes, then don't delete the execution or the - // program (the execution's plan). - try { - // Is the program in the graph? If the program is not in the graph, then - // we don't know how to remove the proper execution and assocation. - stmts = graph.statementsMatching( - undefined, - undefined, - programNode - ); - if (typeof stmts == "undefined" || !stmts.length) - return false; + getExecutionNode: function (executionId) { + var rdf = this.rdf; + var graph = this.dataPackageGraph; + var stmts = null; + var testNode = null; + var cnResolveUrl = this.getCnURI(); + + // First see if the execution exists in the RDF graph as a 'bare' idenfier, i.e. + // a 'urn:uuid'. + stmts = graph.statementsMatching( + rdf.sym(executionId), + undefined, + undefined, + ); + if (typeof stmts == "undefined" || !stmts.length) { + // The execution node as urn was not found, look for fully qualified version. + testNode = rdf.sym(this.getURIFromRDF(executionId)); + stmts = graph.statementsMatching( + rdf.sym(executionId), + undefined, + undefined, + ); + if (typeof stmts == "undefined") { + // Couldn't find the execution, return the standard RDF node value + executionNode = rdf.sym(this.getURIFromRDF(executionId)); + return executionNode; + } else { + return testNode; + } + } else { + // The executionNode was found in the RDF graph as a urn + var executionNode = stmts[0].subject; + return executionNode; + } + }, + + addProgramToGraph: function (programId) { + var rdf = this.rdf; + var graph = this.dataPackageGraph; + var RDF = rdf.Namespace(this.namespaces.RDF), + DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), + PROV = rdf.Namespace(this.namespaces.PROV), + PROVONE = rdf.Namespace(this.namespaces.PROVONE), + XSD = rdf.Namespace(this.namespaces.XSD); + var member = this.get(programId); + var executionId = member.get("prov_wasExecutedByExecution"); + var executionNode = null; + var programNode = null; + var associationId = null; + var associationNode = null; + var cnResolveUrl = this.getCnURI(); + + if (!executionId.length) { + // This is a new execution, so create new execution and association ids + executionId = "urn:uuid:" + uuid.v4(); + member.set("prov_wasExecutedByExecution", [executionId]); + // Blank node id. RDF validator doesn't like ':' so don't use in the id + //executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); + executionNode = this.getExecutionNode(executionId); + //associationId = "_" + uuid.v4(); + associationNode = graph.bnode(); + } else { + executionId = executionId[0]; + // Check if an association exists in the RDF graph for this execution id + //executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); + executionNode = this.getExecutionNode(executionId); + // Check if there is an association id for this execution. + // If this execution is newly created (via the editor (existing would + // be parsed from the resmap), then create a new association id. + var stmts = graph.statementsMatching( + executionNode, + PROV("qualifiedAssociation"), + undefined, + ); + // IF an associati on was found, then use it, else geneate a new one + // (Associations aren't stored in the ) + if (stmts.length) { + associationNode = stmts[0].object; + //associationId = stmts[0].object.value; + } else { + //associationId = "_" + uuid.v4(); + associationNode = graph.bnode(); + } + } + //associationNode = graph.bnode(associationId); + //associationNode = graph.bnode(); + programNode = rdf.sym(this.getURIFromRDF(programId)); + try { + this.addToGraph( + executionNode, + PROV("qualifiedAssociation"), + associationNode, + ); + this.addToGraph(executionNode, RDF("type"), PROVONE("Execution")); + this.addToGraph( + executionNode, + DCTERMS("identifier"), + rdf.literal(executionId, undefined, XSD("string")), + ); + this.addToGraph(associationNode, PROV("hadPlan"), programNode); + this.addToGraph(programNode, RDF("type"), PROVONE("Program")); + } catch (error) { + console.log(error); + } + return executionId; + }, + + // Remove a program identifier from the RDF graph and remove associated + // linkage between the program id and the exection, if the execution is not + // being used by any other statements. + removeProgramFromGraph: function (programId) { + var graph = this.dataPackageGraph; + var rdf = this.rdf; + var stmts = null; + var cnResolveUrl = this.getCnURI(); + var RDF = rdf.Namespace(this.namespaces.RDF), + DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), + PROV = rdf.Namespace(this.namespaces.PROV), + PROVONE = rdf.Namespace(this.namespaces.PROVONE), + XSD = rdf.Namespace(this.namespaces.XSD); + var associationNode = null; + + var executionId = this.getExecutionId(programId); + if (executionId == null) return false; + + //var executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); + var executionNode = this.getExecutionNode(executionId); + var programNode = rdf.sym(this.getURIFromRDF(programId)); + + // In order to remove this program from the graph, we have to first determine that + // nothing else is using the execution that is associated with the program (the plan). + // There may be additional 'used', 'geneated', 'qualifiedGeneration', etc. items that + // may be pointing to the execution. If yes, then don't delete the execution or the + // program (the execution's plan). + try { + // Is the program in the graph? If the program is not in the graph, then + // we don't know how to remove the proper execution and assocation. + stmts = graph.statementsMatching(undefined, undefined, programNode); + if (typeof stmts == "undefined" || !stmts.length) return false; + + // Is anything else linked to this execution? + stmts = graph.statementsMatching(executionNode, PROV("used")); + if (!typeof stmts == "undefined" || stmts.length) return false; + stmts = graph.statementsMatching( + undefined, + PROV("wasGeneratedBy"), + executionNode, + ); + if (!typeof stmts == "undefined" || stmts.length) return false; + stmts = graph.statementsMatching( + executionNode, + PROV("qualifiedGeneration"), + undefined, + ); + if (!typeof stmts == "undefined" || stmts.length) return false; + stmts = graph.statementsMatching( + undefined, + PROV("wasInformedBy"), + executionNode, + ); + if (!typeof stmts == "undefined" || stmts.length) return false; + stmts = graph.statementsMatching( + undefined, + PROV("wasPartOf"), + executionNode, + ); + if (!typeof stmts == "undefined" || stmts.length) return false; + + // get association + stmts = graph.statementsMatching( + undefined, + PROV("hadPlan"), + programNode, + ); + associationNode = stmts[0].subject; + } catch (error) { + console.log(error); + } - // Is anything else linked to this execution? - stmts = graph.statementsMatching( - executionNode, - PROV("used") - ); - if (!typeof stmts == "undefined" || stmts.length) - return false; - stmts = graph.statementsMatching( - undefined, - PROV("wasGeneratedBy"), - executionNode - ); - if (!typeof stmts == "undefined" || stmts.length) - return false; - stmts = graph.statementsMatching( - executionNode, - PROV("qualifiedGeneration"), - undefined - ); - if (!typeof stmts == "undefined" || stmts.length) - return false; - stmts = graph.statementsMatching( - undefined, - PROV("wasInformedBy"), - executionNode - ); - if (!typeof stmts == "undefined" || stmts.length) - return false; - stmts = graph.statementsMatching( - undefined, - PROV("wasPartOf"), - executionNode - ); - if (!typeof stmts == "undefined" || stmts.length) - return false; - - // get association - stmts = graph.statementsMatching( - undefined, - PROV("hadPlan"), - programNode - ); - associationNode = stmts[0].subject; - } catch (error) { - console.log(error); - } + // The execution isn't needed any longer, so remove it and the program. + try { + graph.removeMatches(programNode, RDF("type"), PROVONE("Program")); + graph.removeMatches(associationNode, PROV("hadPlan"), programNode); + graph.removeMatches( + associationNode, + RDF("type"), + PROV("Association"), + ); + graph.removeMatches(associationNode, PROV("Agent"), undefined); + graph.removeMatches(executionNode, RDF("type"), PROVONE("Execution")); + graph.removeMatches( + executionNode, + DCTERMS("identifier"), + rdf.literal(executionId, undefined, XSD("string")), + ); + graph.removeMatches( + executionNode, + PROV("qualifiedAssociation"), + associationNode, + ); + } catch (error) { + console.log(error); + } + return true; + }, - // The execution isn't needed any longer, so remove it and the program. - try { - graph.removeMatches( - programNode, - RDF("type"), - PROVONE("Program") - ); - graph.removeMatches( - associationNode, - PROV("hadPlan"), - programNode - ); - graph.removeMatches( - associationNode, - RDF("type"), - PROV("Association") - ); - graph.removeMatches( - associationNode, - PROV("Agent"), - undefined - ); - graph.removeMatches( - executionNode, - RDF("type"), - PROVONE("Execution") - ); - graph.removeMatches( - executionNode, - DCTERMS("identifier"), - rdf.literal(executionId, undefined, XSD("string")) - ); - graph.removeMatches( - executionNode, - PROV("qualifiedAssociation"), - associationNode - ); - } catch (error) { - console.log(error); - } - return true; + /* + * Serialize the DataPackage to OAI-ORE RDF XML + */ + serialize: function () { + //Create an RDF serializer + var serializer = this.rdf.Serializer(), + oldPidVariations, + modifiedDate, + subjectClone, + predicateClone, + objectClone; + + serializer.store = this.dataPackageGraph; + + //Define the namespaces + var ORE = this.rdf.Namespace(this.namespaces.ORE), + CITO = this.rdf.Namespace(this.namespaces.CITO), + DC = this.rdf.Namespace(this.namespaces.DC), + DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), + FOAF = this.rdf.Namespace(this.namespaces.FOAF), + RDF = this.rdf.Namespace(this.namespaces.RDF), + XSD = this.rdf.Namespace(this.namespaces.XSD); + + //Get the pid of this package - depends on whether we are updating or creating a resource map + var pid = this.packageModel.get("id"), + oldPid = this.packageModel.get("oldPid"), + cnResolveUrl = this.getCnURI(); + + //Get a list of the model pids that should be aggregated by this package + var idsFromModel = []; + this.each(function (packageMember) { + //If this object isn't done uploading, don't aggregate it. + //Or if it failed to upload, don't aggregate it. + //But if the system metadata failed to update, it can still be aggregated. + if ( + packageMember.get("uploadStatus") !== "p" || + packageMember.get("uploadStatus") !== "e" || + packageMember.get("sysMetaUploadStatus") == "e" + ) { + idsFromModel.push(packageMember.get("id")); + } + }); + + this.idsToAggregate = idsFromModel; + + //Update the pids in the RDF graph only if we are updating the resource map with a new pid + if (!this.packageModel.isNew()) { + // Remove all describes/isDescribedBy statements (they'll be rebuilt) + this.dataPackageGraph.removeMany( + undefined, + ORE("describes"), + undefined, + undefined, + undefined, + ); + this.dataPackageGraph.removeMany( + undefined, + ORE("isDescribedBy"), + undefined, + undefined, + undefined, + ); + + //Create variations of the resource map ID using the resolve URL so we can always find it in the RDF graph + oldPidVariations = [ + oldPid, + encodeURIComponent(oldPid), + cnResolveUrl + oldPid, + cnResolveUrl + encodeURIComponent(oldPid), + this.getURIFromRDF(oldPid), + ]; + + //Using the isAggregatedBy statements, find all the DataONE object ids in the RDF graph + var idsFromXML = []; + + var identifierStatements = this.dataPackageGraph.statementsMatching( + undefined, + DCTERMS("identifier"), + undefined, + ); + _.each( + identifierStatements, + function (statement) { + idsFromXML.push( + statement.object.value, + encodeURIComponent(statement.object.value), + cnResolveUrl + encodeURIComponent(statement.object.value), + cnResolveUrl + statement.object.value, + ); }, - - /* - * Serialize the DataPackage to OAI-ORE RDF XML - */ - serialize: function () { - //Create an RDF serializer - var serializer = this.rdf.Serializer(), - oldPidVariations, - modifiedDate, - subjectClone, - predicateClone, - objectClone; - - serializer.store = this.dataPackageGraph; - - //Define the namespaces - var ORE = this.rdf.Namespace(this.namespaces.ORE), - CITO = this.rdf.Namespace(this.namespaces.CITO), - DC = this.rdf.Namespace(this.namespaces.DC), - DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), - FOAF = this.rdf.Namespace(this.namespaces.FOAF), - RDF = this.rdf.Namespace(this.namespaces.RDF), - XSD = this.rdf.Namespace(this.namespaces.XSD); - - //Get the pid of this package - depends on whether we are updating or creating a resource map - var pid = this.packageModel.get("id"), - oldPid = this.packageModel.get("oldPid"), - cnResolveUrl = this.getCnURI(); - - //Get a list of the model pids that should be aggregated by this package - var idsFromModel = []; - this.each(function (packageMember) { - //If this object isn't done uploading, don't aggregate it. - //Or if it failed to upload, don't aggregate it. - //But if the system metadata failed to update, it can still be aggregated. - if ( - packageMember.get("uploadStatus") !== "p" || - packageMember.get("uploadStatus") !== "e" || - packageMember.get("sysMetaUploadStatus") == "e" - ) { - idsFromModel.push(packageMember.get("id")); - } - }); - - this.idsToAggregate = idsFromModel; - - //Update the pids in the RDF graph only if we are updating the resource map with a new pid - if (!this.packageModel.isNew()) { - // Remove all describes/isDescribedBy statements (they'll be rebuilt) - this.dataPackageGraph.removeMany( - undefined, - ORE("describes"), - undefined, - undefined, - undefined - ); - this.dataPackageGraph.removeMany( - undefined, - ORE("isDescribedBy"), - undefined, - undefined, - undefined - ); - - //Create variations of the resource map ID using the resolve URL so we can always find it in the RDF graph - oldPidVariations = [ - oldPid, - encodeURIComponent(oldPid), - cnResolveUrl + oldPid, - cnResolveUrl + encodeURIComponent(oldPid), - this.getURIFromRDF(oldPid), - ]; - - //Using the isAggregatedBy statements, find all the DataONE object ids in the RDF graph - var idsFromXML = []; - - var identifierStatements = - this.dataPackageGraph.statementsMatching( - undefined, - DCTERMS("identifier"), - undefined - ); - _.each( - identifierStatements, - function (statement) { - idsFromXML.push( - statement.object.value, - encodeURIComponent(statement.object.value), - cnResolveUrl + - encodeURIComponent(statement.object.value), - cnResolveUrl + statement.object.value - ); - }, - this - ); - - //Get all the child package ids - var childPackages = this.packageModel.get("childPackages"); - if (typeof childPackages == "object") { - idsFromModel = _.union( - idsFromModel, - Object.keys(childPackages) - ); - } - - //Find the difference between the model IDs and the XML IDs to get a list of added members - var addedIds = _.without( - _.difference(idsFromModel, idsFromXML), - oldPidVariations - ); - - //Start an array to track all the member id variations - var allMemberIds = idsFromModel; - - //Add the ids with the CN Resolve URLs - _.each(idsFromModel, function (id) { - allMemberIds.push( - cnResolveUrl + encodeURIComponent(id), - cnResolveUrl + id, - encodeURIComponent(id) - ); - }); - - //Find the identifier statement in the resource map - var idNode = this.rdf.lit(oldPid); - var idStatements = this.dataPackageGraph.statementsMatching( - undefined, - undefined, - idNode - ); - - //Change all the resource map identifier literal node in the RDF graph - if (idStatements.length) { - var idStatement = idStatements[0]; - - //Remove the identifier statement - try { - this.dataPackageGraph.remove(idStatement); - } catch (error) { - console.log(error); - } - - //Replace the id in the subject URI with the new id - var newRMapURI = ""; - if (idStatement.subject.value.indexOf(oldPid) > -1) { - newRMapURI = idStatement.subject.value.replace( - oldPid, - pid - ); - } else if ( - idStatement.subject.value.indexOf( - encodeURIComponent(oldPid) - ) > -1 - ) { - newRMapURI = idStatement.subject.value.replace( - encodeURIComponent(oldPid), - encodeURIComponent(pid) - ); - } - - //Create resource map nodes for the subject and object - var rMapNode = this.rdf.sym(newRMapURI), - rMapIdNode = this.rdf.lit(pid); - //Add the triple for the resource map id - this.dataPackageGraph.add( - rMapNode, - DCTERMS("identifier"), - rMapIdNode - ); - } - - //Get all the isAggregatedBy statements - var aggByStatements = $.extend( - true, - [], - this.dataPackageGraph.statementsMatching( - undefined, - ORE("isAggregatedBy") - ) - ); - - // Remove any other isAggregatedBy statements that are not listed as members of this model - _.each( - aggByStatements, - function (statement) { - if ( - !_.contains( - allMemberIds, - statement.subject.value - ) - ) { - this.removeFromAggregation( - statement.subject.value - ); - } - }, - this - ); - - // Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID - var aggregationNode; - _.each( - oldPidVariations, - function (oldPid) { - //Create a node for the old aggregation using this pid variation - aggregationNode = this.rdf.sym( - oldPid + "#aggregation" - ); - var aggregationLitNode = this.rdf.lit( - oldPid + "#aggregation", - "", - XSD("anyURI") - ); - - //Get all the triples where the old aggregation is the subject - var aggregationSubjStatements = _.union( - this.dataPackageGraph.statementsMatching( - aggregationNode - ), - this.dataPackageGraph.statementsMatching( - aggregationLitNode - ) - ); - - if (aggregationSubjStatements.length) { - _.each( - aggregationSubjStatements, - function (statement) { - //Clone the subject - subjectClone = this.cloneNode( - statement.subject - ); - //Clone the predicate - predicateClone = this.cloneNode( - statement.predicate - ); - //Clone the object - objectClone = this.cloneNode( - statement.object - ); - - //Set the subject value to the new aggregation id - subjectClone.value = - this.getURIFromRDF(pid) + - "#aggregation"; - - //Add a new statement with the new aggregation subject but the same predicate and object - this.dataPackageGraph.add( - subjectClone, - predicateClone, - objectClone - ); - }, - this - ); - - //Remove the old aggregation statements from the graph - this.dataPackageGraph.removeMany( - aggregationNode - ); - } - - // Change all the statements in the RDF where the aggregation is the object, to reflect the new resource map ID - var aggregationObjStatements = _.union( - this.dataPackageGraph.statementsMatching( - undefined, - undefined, - aggregationNode - ), - this.dataPackageGraph.statementsMatching( - undefined, - undefined, - aggregationLitNode - ) - ); - - if (aggregationObjStatements.length) { - _.each( - aggregationObjStatements, - function (statement) { - //Clone the subject, object, and predicate - subjectClone = this.cloneNode( - statement.subject - ); - predicateClone = this.cloneNode( - statement.predicate - ); - objectClone = this.cloneNode( - statement.object - ); - - //Set the object to the new aggregation pid - objectClone.value = - this.getURIFromRDF(pid) + - "#aggregation"; - - //Add the statement with the old subject and predicate but new aggregation object - this.dataPackageGraph.add( - subjectClone, - predicateClone, - objectClone - ); - }, - this - ); - - //Remove all the old aggregation statements from the graph - this.dataPackageGraph.removeMany( - undefined, - undefined, - aggregationNode - ); - } - - // Change all the resource map subject nodes in the RDF graph - var rMapNode = this.rdf.sym( - this.getURIFromRDF(oldPid) - ); - var rMapStatements = $.extend( - true, - [], - this.dataPackageGraph.statementsMatching( - rMapNode - ) - ); - - // then repopulate them with correct values - _.each( - rMapStatements, - function (statement) { - subjectClone = this.cloneNode( - statement.subject - ); - predicateClone = this.cloneNode( - statement.predicate - ); - objectClone = this.cloneNode( - statement.object - ); - - // In the case of modified date, reset it to now() - if ( - predicateClone.value === DC("modified") - ) { - objectClone.value = - new Date().toISOString(); - } - - //Update the subject to the new pid - subjectClone.value = - this.getURIFromRDF(pid); - - //Remove the old resource map statement - this.dataPackageGraph.remove(statement); - - //Add the statement with the new subject pid, but the same predicate and object - this.dataPackageGraph.add( - subjectClone, - predicateClone, - objectClone - ); - }, - this - ); - }, - this - ); - - // Add the describes/isDescribedBy statements back in - this.dataPackageGraph.add( - this.rdf.sym(this.getURIFromRDF(pid)), - ORE("describes"), - this.rdf.sym(this.getURIFromRDF(pid) + "#aggregation") - ); - this.dataPackageGraph.add( - this.rdf.sym(this.getURIFromRDF(pid) + "#aggregation"), - ORE("isDescribedBy"), - this.rdf.sym(this.getURIFromRDF(pid)) - ); - - //Add nodes for new package members - _.each( - addedIds, - function (id) { - this.addToAggregation(id); - }, - this - ); - } else { - // Create the OAI-ORE graph from scratch - this.dataPackageGraph = this.rdf.graph(); - cnResolveUrl = this.getCnURI(); - - //Create a resource map node - var rMapNode = this.rdf.sym( - this.getURIFromRDF(this.packageModel.id) - ); - //Create an aggregation node - var aggregationNode = this.rdf.sym( - this.getURIFromRDF(this.packageModel.id) + - "#aggregation" - ); - - // Describe the resource map with a Creator - var creatorNode = this.rdf.blankNode(); - var creatorName = this.rdf.lit( - (MetacatUI.appUserModel.get("firstName") || "") + - " " + - (MetacatUI.appUserModel.get("lastName") || ""), - "", - XSD("string") - ); - this.dataPackageGraph.add( - creatorNode, - FOAF("name"), - creatorName - ); - this.dataPackageGraph.add( - creatorNode, - RDF("type"), - DCTERMS("Agent") - ); - this.dataPackageGraph.add( - rMapNode, - DC("creator"), - creatorNode - ); - - // Set the modified date - modifiedDate = this.rdf.lit( - new Date().toISOString(), - "", - XSD("dateTime") - ); + this, + ); + + //Get all the child package ids + var childPackages = this.packageModel.get("childPackages"); + if (typeof childPackages == "object") { + idsFromModel = _.union(idsFromModel, Object.keys(childPackages)); + } + + //Find the difference between the model IDs and the XML IDs to get a list of added members + var addedIds = _.without( + _.difference(idsFromModel, idsFromXML), + oldPidVariations, + ); + + //Start an array to track all the member id variations + var allMemberIds = idsFromModel; + + //Add the ids with the CN Resolve URLs + _.each(idsFromModel, function (id) { + allMemberIds.push( + cnResolveUrl + encodeURIComponent(id), + cnResolveUrl + id, + encodeURIComponent(id), + ); + }); + + //Find the identifier statement in the resource map + var idNode = this.rdf.lit(oldPid); + var idStatements = this.dataPackageGraph.statementsMatching( + undefined, + undefined, + idNode, + ); + + //Change all the resource map identifier literal node in the RDF graph + if (idStatements.length) { + var idStatement = idStatements[0]; + + //Remove the identifier statement + try { + this.dataPackageGraph.remove(idStatement); + } catch (error) { + console.log(error); + } + + //Replace the id in the subject URI with the new id + var newRMapURI = ""; + if (idStatement.subject.value.indexOf(oldPid) > -1) { + newRMapURI = idStatement.subject.value.replace(oldPid, pid); + } else if ( + idStatement.subject.value.indexOf(encodeURIComponent(oldPid)) > -1 + ) { + newRMapURI = idStatement.subject.value.replace( + encodeURIComponent(oldPid), + encodeURIComponent(pid), + ); + } + + //Create resource map nodes for the subject and object + var rMapNode = this.rdf.sym(newRMapURI), + rMapIdNode = this.rdf.lit(pid); + //Add the triple for the resource map id + this.dataPackageGraph.add( + rMapNode, + DCTERMS("identifier"), + rMapIdNode, + ); + } + + //Get all the isAggregatedBy statements + var aggByStatements = $.extend( + true, + [], + this.dataPackageGraph.statementsMatching( + undefined, + ORE("isAggregatedBy"), + ), + ); + + // Remove any other isAggregatedBy statements that are not listed as members of this model + _.each( + aggByStatements, + function (statement) { + if (!_.contains(allMemberIds, statement.subject.value)) { + this.removeFromAggregation(statement.subject.value); + } + }, + this, + ); + + // Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID + var aggregationNode; + _.each( + oldPidVariations, + function (oldPid) { + //Create a node for the old aggregation using this pid variation + aggregationNode = this.rdf.sym(oldPid + "#aggregation"); + var aggregationLitNode = this.rdf.lit( + oldPid + "#aggregation", + "", + XSD("anyURI"), + ); + + //Get all the triples where the old aggregation is the subject + var aggregationSubjStatements = _.union( + this.dataPackageGraph.statementsMatching(aggregationNode), + this.dataPackageGraph.statementsMatching(aggregationLitNode), + ); + + if (aggregationSubjStatements.length) { + _.each( + aggregationSubjStatements, + function (statement) { + //Clone the subject + subjectClone = this.cloneNode(statement.subject); + //Clone the predicate + predicateClone = this.cloneNode(statement.predicate); + //Clone the object + objectClone = this.cloneNode(statement.object); + + //Set the subject value to the new aggregation id + subjectClone.value = + this.getURIFromRDF(pid) + "#aggregation"; + + //Add a new statement with the new aggregation subject but the same predicate and object this.dataPackageGraph.add( - rMapNode, - DCTERMS("modified"), - modifiedDate + subjectClone, + predicateClone, + objectClone, ); + }, + this, + ); - this.dataPackageGraph.add( - rMapNode, - RDF("type"), - ORE("ResourceMap") - ); - this.dataPackageGraph.add( - rMapNode, - ORE("describes"), - aggregationNode - ); - var idLiteral = this.rdf.lit( - this.packageModel.id, - "", - XSD("string") - ); - this.dataPackageGraph.add( - rMapNode, - DCTERMS("identifier"), - idLiteral - ); + //Remove the old aggregation statements from the graph + this.dataPackageGraph.removeMany(aggregationNode); + } - // Describe the aggregation + // Change all the statements in the RDF where the aggregation is the object, to reflect the new resource map ID + var aggregationObjStatements = _.union( + this.dataPackageGraph.statementsMatching( + undefined, + undefined, + aggregationNode, + ), + this.dataPackageGraph.statementsMatching( + undefined, + undefined, + aggregationLitNode, + ), + ); + + if (aggregationObjStatements.length) { + _.each( + aggregationObjStatements, + function (statement) { + //Clone the subject, object, and predicate + subjectClone = this.cloneNode(statement.subject); + predicateClone = this.cloneNode(statement.predicate); + objectClone = this.cloneNode(statement.object); + + //Set the object to the new aggregation pid + objectClone.value = + this.getURIFromRDF(pid) + "#aggregation"; + + //Add the statement with the old subject and predicate but new aggregation object this.dataPackageGraph.add( - aggregationNode, - ORE("isDescribedBy"), - rMapNode - ); - - // Aggregate each package member - _.each( - idsFromModel, - function (id) { - this.addToAggregation(id); - }, - this + subjectClone, + predicateClone, + objectClone, ); - } - - // Remove any references to blank nodes not already cleaned up. - // rdflib.js will fail to serialize an IndexedFormula (graph) with - // statements whose object is a blank node when the blank node - // is not the subject of any other statements. - this.removeOrphanedBlankNodes(); + }, + this, + ); - var xmlString = serializer.statementsToXML( - this.dataPackageGraph.statements + //Remove all the old aggregation statements from the graph + this.dataPackageGraph.removeMany( + undefined, + undefined, + aggregationNode, ); + } - return xmlString; + // Change all the resource map subject nodes in the RDF graph + var rMapNode = this.rdf.sym(this.getURIFromRDF(oldPid)); + var rMapStatements = $.extend( + true, + [], + this.dataPackageGraph.statementsMatching(rMapNode), + ); + + // then repopulate them with correct values + _.each( + rMapStatements, + function (statement) { + subjectClone = this.cloneNode(statement.subject); + predicateClone = this.cloneNode(statement.predicate); + objectClone = this.cloneNode(statement.object); + + // In the case of modified date, reset it to now() + if (predicateClone.value === DC("modified")) { + objectClone.value = new Date().toISOString(); + } + + //Update the subject to the new pid + subjectClone.value = this.getURIFromRDF(pid); + + //Remove the old resource map statement + this.dataPackageGraph.remove(statement); + + //Add the statement with the new subject pid, but the same predicate and object + this.dataPackageGraph.add( + subjectClone, + predicateClone, + objectClone, + ); + }, + this, + ); }, - - // Clone an rdflib.js Node by creaing a new node based on the - // original node RDF term type and data type. - cloneNode: function (nodeToClone) { - switch (nodeToClone.termType) { - case "NamedNode": - return this.rdf.sym(nodeToClone.value); - break; - case "Literal": - // Check for the datatype for this literal value, e.g. http://www.w3.org/2001/XMLSchema#string" - if (typeof nodeToClone.datatype !== "undefined") { - return this.rdf.literal( - nodeToClone.value, - undefined, - nodeToClone.datatype - ); - } else { - return this.rdf.literal(nodeToClone.value); - } - break; - case "BlankNode": - //Blank nodes don't need to be cloned - return nodeToClone; //(this.rdf.blankNode(nodeToClone.value)); - break; - case "Collection": - // TODO: construct a list of nodes for this term type. - return this.rdf.list(nodeToClone.value); - break; - default: - console.log( - "ERROR: unknown node type to clone: " + - nodeToClone.termType - ); - } + this, + ); + + // Add the describes/isDescribedBy statements back in + this.dataPackageGraph.add( + this.rdf.sym(this.getURIFromRDF(pid)), + ORE("describes"), + this.rdf.sym(this.getURIFromRDF(pid) + "#aggregation"), + ); + this.dataPackageGraph.add( + this.rdf.sym(this.getURIFromRDF(pid) + "#aggregation"), + ORE("isDescribedBy"), + this.rdf.sym(this.getURIFromRDF(pid)), + ); + + //Add nodes for new package members + _.each( + addedIds, + function (id) { + this.addToAggregation(id); }, + this, + ); + } else { + // Create the OAI-ORE graph from scratch + this.dataPackageGraph = this.rdf.graph(); + cnResolveUrl = this.getCnURI(); + + //Create a resource map node + var rMapNode = this.rdf.sym(this.getURIFromRDF(this.packageModel.id)); + //Create an aggregation node + var aggregationNode = this.rdf.sym( + this.getURIFromRDF(this.packageModel.id) + "#aggregation", + ); + + // Describe the resource map with a Creator + var creatorNode = this.rdf.blankNode(); + var creatorName = this.rdf.lit( + (MetacatUI.appUserModel.get("firstName") || "") + + " " + + (MetacatUI.appUserModel.get("lastName") || ""), + "", + XSD("string"), + ); + this.dataPackageGraph.add(creatorNode, FOAF("name"), creatorName); + this.dataPackageGraph.add(creatorNode, RDF("type"), DCTERMS("Agent")); + this.dataPackageGraph.add(rMapNode, DC("creator"), creatorNode); + + // Set the modified date + modifiedDate = this.rdf.lit( + new Date().toISOString(), + "", + XSD("dateTime"), + ); + this.dataPackageGraph.add( + rMapNode, + DCTERMS("modified"), + modifiedDate, + ); + + this.dataPackageGraph.add(rMapNode, RDF("type"), ORE("ResourceMap")); + this.dataPackageGraph.add( + rMapNode, + ORE("describes"), + aggregationNode, + ); + var idLiteral = this.rdf.lit(this.packageModel.id, "", XSD("string")); + this.dataPackageGraph.add(rMapNode, DCTERMS("identifier"), idLiteral); + + // Describe the aggregation + this.dataPackageGraph.add( + aggregationNode, + ORE("isDescribedBy"), + rMapNode, + ); + + // Aggregate each package member + _.each( + idsFromModel, + function (id) { + this.addToAggregation(id); + }, + this, + ); + } - // Adds a new object to the resource map RDF graph - addToAggregation: function (id) { - // Initialize the namespaces - var ORE = this.rdf.Namespace(this.namespaces.ORE), - DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), - XSD = this.rdf.Namespace(this.namespaces.XSD), - CITO = this.rdf.Namespace(this.namespaces.CITO); - - // Create a node for this object, the identifier, the resource map, and the aggregation - var objectNode = this.rdf.sym(this.getURIFromRDF(id)), - rMapURI = this.getURIFromRDF(this.packageModel.get("id")), - mapNode = this.rdf.sym(rMapURI), - aggNode = this.rdf.sym(rMapURI + "#aggregation"), - idNode = this.rdf.literal(id, undefined, XSD("string")), - idStatements = [], - aggStatements = [], - aggByStatements = [], - documentsStatements = [], - isDocumentedByStatements = []; - - // Add the statement: this object isAggregatedBy the resource map aggregation - aggByStatements = this.dataPackageGraph.statementsMatching( - objectNode, - ORE("isAggregatedBy"), - aggNode - ); - if (aggByStatements.length < 1) { - this.dataPackageGraph.add( - objectNode, - ORE("isAggregatedBy"), - aggNode - ); - } - - // Add the statement: The resource map aggregation aggregates this object - aggStatements = this.dataPackageGraph.statementsMatching( - aggNode, - ORE("aggregates"), - objectNode - ); - if (aggStatements.length < 1) { - this.dataPackageGraph.add( - aggNode, - ORE("aggregates"), - objectNode - ); - } + // Remove any references to blank nodes not already cleaned up. + // rdflib.js will fail to serialize an IndexedFormula (graph) with + // statements whose object is a blank node when the blank node + // is not the subject of any other statements. + this.removeOrphanedBlankNodes(); + + var xmlString = serializer.statementsToXML( + this.dataPackageGraph.statements, + ); + + return xmlString; + }, + + // Clone an rdflib.js Node by creaing a new node based on the + // original node RDF term type and data type. + cloneNode: function (nodeToClone) { + switch (nodeToClone.termType) { + case "NamedNode": + return this.rdf.sym(nodeToClone.value); + break; + case "Literal": + // Check for the datatype for this literal value, e.g. http://www.w3.org/2001/XMLSchema#string" + if (typeof nodeToClone.datatype !== "undefined") { + return this.rdf.literal( + nodeToClone.value, + undefined, + nodeToClone.datatype, + ); + } else { + return this.rdf.literal(nodeToClone.value); + } + break; + case "BlankNode": + //Blank nodes don't need to be cloned + return nodeToClone; //(this.rdf.blankNode(nodeToClone.value)); + break; + case "Collection": + // TODO: construct a list of nodes for this term type. + return this.rdf.list(nodeToClone.value); + break; + default: + console.log( + "ERROR: unknown node type to clone: " + nodeToClone.termType, + ); + } + }, + + // Adds a new object to the resource map RDF graph + addToAggregation: function (id) { + // Initialize the namespaces + var ORE = this.rdf.Namespace(this.namespaces.ORE), + DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), + XSD = this.rdf.Namespace(this.namespaces.XSD), + CITO = this.rdf.Namespace(this.namespaces.CITO); + + // Create a node for this object, the identifier, the resource map, and the aggregation + var objectNode = this.rdf.sym(this.getURIFromRDF(id)), + rMapURI = this.getURIFromRDF(this.packageModel.get("id")), + mapNode = this.rdf.sym(rMapURI), + aggNode = this.rdf.sym(rMapURI + "#aggregation"), + idNode = this.rdf.literal(id, undefined, XSD("string")), + idStatements = [], + aggStatements = [], + aggByStatements = [], + documentsStatements = [], + isDocumentedByStatements = []; + + // Add the statement: this object isAggregatedBy the resource map aggregation + aggByStatements = this.dataPackageGraph.statementsMatching( + objectNode, + ORE("isAggregatedBy"), + aggNode, + ); + if (aggByStatements.length < 1) { + this.dataPackageGraph.add(objectNode, ORE("isAggregatedBy"), aggNode); + } - // Add the statement: This object has the identifier {id} if it isn't present - idStatements = this.dataPackageGraph.statementsMatching( - objectNode, - DCTERMS("identifier"), - idNode - ); - if (idStatements.length < 1) { - this.dataPackageGraph.add( - objectNode, - DCTERMS("identifier"), - idNode - ); - } + // Add the statement: The resource map aggregation aggregates this object + aggStatements = this.dataPackageGraph.statementsMatching( + aggNode, + ORE("aggregates"), + objectNode, + ); + if (aggStatements.length < 1) { + this.dataPackageGraph.add(aggNode, ORE("aggregates"), objectNode); + } - // Find the metadata doc that describes this object - var model = this.findWhere({ id: id }), - isDocBy = model.get("isDocumentedBy"), - documents = model.get("documents"); + // Add the statement: This object has the identifier {id} if it isn't present + idStatements = this.dataPackageGraph.statementsMatching( + objectNode, + DCTERMS("identifier"), + idNode, + ); + if (idStatements.length < 1) { + this.dataPackageGraph.add(objectNode, DCTERMS("identifier"), idNode); + } - // Deal with Solr indexing bug where metadata-only packages must "document" themselves - if (isDocBy.length === 0 && documents.length === 0) { - documents.push(model.get("id")); - } + // Find the metadata doc that describes this object + var model = this.findWhere({ id: id }), + isDocBy = model.get("isDocumentedBy"), + documents = model.get("documents"); - // If this object is documented by any metadata... - if (isDocBy && isDocBy.length) { - // Get the ids of all the metadata objects in this package - var metadataInPackage = _.compact( - _.map(this.models, function (m) { - if (m.get("formatType") == "METADATA") return m; - }) - ), - metadataInPackageIDs = _.each( - metadataInPackage, - function (m) { - return m.get("id"); - } - ); - - // Find the metadata IDs that are in this package that also documents this data object - var metadataIds = Array.isArray(isDocBy) - ? _.intersection(metadataInPackageIDs, isDocBy) - : _.intersection(metadataInPackageIDs, [isDocBy]); - - // If this data object is not documented by one of these metadata docs, - // then we should check if it's documented by an obsoleted pid. If so, - // we'll want to change that so it's documented by a current metadata. - if (metadataIds.length == 0) { - for (var i = 0; i < metadataInPackage.length; i++) { - //If the previous version of this metadata documents this data, - if ( - _.contains( - isDocBy, - metadataInPackage[i].get("obsoletes") - ) - ) { - //Save the metadata id for serialization - metadataIds = [metadataInPackage[i].get("id")]; - - //Exit the for loop - break; - } - } - } + // Deal with Solr indexing bug where metadata-only packages must "document" themselves + if (isDocBy.length === 0 && documents.length === 0) { + documents.push(model.get("id")); + } - // For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement - _.each( - metadataIds, - function (metaId) { - //Create the named nodes and statements - var dataNode = this.rdf.sym(this.getURIFromRDF(id)), - metadataNode = this.rdf.sym( - this.getURIFromRDF(metaId) - ), - isDocByStatement = this.rdf.st( - dataNode, - CITO("isDocumentedBy"), - metadataNode - ), - documentsStatement = this.rdf.st( - metadataNode, - CITO("documents"), - dataNode - ); - - // Add the statements - documentsStatements = - this.dataPackageGraph.statementsMatching( - metadataNode, - CITO("documents"), - dataNode - ); - if (documentsStatements.length < 1) { - this.dataPackageGraph.add(documentsStatement); - } - isDocumentedByStatements = - this.dataPackageGraph.statementsMatching( - dataNode, - CITO("isDocumentedBy"), - metadataNode - ); - if (isDocumentedByStatements.length < 1) { - this.dataPackageGraph.add(isDocByStatement); - } - }, - this - ); - } + // If this object is documented by any metadata... + if (isDocBy && isDocBy.length) { + // Get the ids of all the metadata objects in this package + var metadataInPackage = _.compact( + _.map(this.models, function (m) { + if (m.get("formatType") == "METADATA") return m; + }), + ), + metadataInPackageIDs = _.each(metadataInPackage, function (m) { + return m.get("id"); + }); + + // Find the metadata IDs that are in this package that also documents this data object + var metadataIds = Array.isArray(isDocBy) + ? _.intersection(metadataInPackageIDs, isDocBy) + : _.intersection(metadataInPackageIDs, [isDocBy]); + + // If this data object is not documented by one of these metadata docs, + // then we should check if it's documented by an obsoleted pid. If so, + // we'll want to change that so it's documented by a current metadata. + if (metadataIds.length == 0) { + for (var i = 0; i < metadataInPackage.length; i++) { + //If the previous version of this metadata documents this data, + if (_.contains(isDocBy, metadataInPackage[i].get("obsoletes"))) { + //Save the metadata id for serialization + metadataIds = [metadataInPackage[i].get("id")]; + + //Exit the for loop + break; + } + } + } + + // For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement + _.each( + metadataIds, + function (metaId) { + //Create the named nodes and statements + var dataNode = this.rdf.sym(this.getURIFromRDF(id)), + metadataNode = this.rdf.sym(this.getURIFromRDF(metaId)), + isDocByStatement = this.rdf.st( + dataNode, + CITO("isDocumentedBy"), + metadataNode, + ), + documentsStatement = this.rdf.st( + metadataNode, + CITO("documents"), + dataNode, + ); - // If this object documents a data object - if (documents && documents.length) { - // Create a literal node for it - var metadataNode = this.rdf.sym(this.getURIFromRDF(id)); - - _.each( - documents, - function (dataID) { - //Make sure the id is one that will be aggregated - if (_.contains(this.idsToAggregate, dataID)) { - //Find the identifier statement for this data object - var dataURI = this.getURIFromRDF(dataID); - - //Create a data node using the exact way the identifier URI is written - var dataNode = this.rdf.sym(dataURI); - - //Get the statements for data isDocumentedBy metadata - isDocumentedByStatements = - this.dataPackageGraph.statementsMatching( - dataNode, - CITO("isDocumentedBy"), - metadataNode - ); - - //If that statement is not in the RDF already... - if (isDocumentedByStatements.length < 1) { - // Create a statement: This data is documented by this metadata - var isDocByStatement = this.rdf.st( - dataNode, - CITO("isDocumentedBy"), - metadataNode - ); - //Add the "isDocumentedBy" statement - this.dataPackageGraph.add(isDocByStatement); - } - - //Get the statements for metadata documents data - documentsStatements = - this.dataPackageGraph.statementsMatching( - metadataNode, - CITO("documents"), - dataNode - ); - - //If that statement is not in the RDF already... - if (documentsStatements.length < 1) { - // Create a statement: This metadata documents data - var documentsStatement = this.rdf.st( - metadataNode, - CITO("documents"), - dataNode - ); - //Add the "isDocumentedBy" statement - this.dataPackageGraph.add( - documentsStatement - ); - } - } - }, - this - ); - } + // Add the statements + documentsStatements = this.dataPackageGraph.statementsMatching( + metadataNode, + CITO("documents"), + dataNode, + ); + if (documentsStatements.length < 1) { + this.dataPackageGraph.add(documentsStatement); + } + isDocumentedByStatements = + this.dataPackageGraph.statementsMatching( + dataNode, + CITO("isDocumentedBy"), + metadataNode, + ); + if (isDocumentedByStatements.length < 1) { + this.dataPackageGraph.add(isDocByStatement); + } }, + this, + ); + } - /* - * Removes an object from the aggregation in the RDF graph - */ - removeFromAggregation: function (id) { - if (id.indexOf(this.dataPackageGraph.cnResolveUrl) == -1) { - id = this.getURIFromRDF(id); - } - - // Create a literal node for the removed object - var removedObjNode = this.rdf.sym(id), - // Get the statements from the RDF where the removed object is the subject or object - statements = $.extend( - true, - [], - _.union( - this.dataPackageGraph.statementsMatching( - undefined, - undefined, - removedObjNode - ), - this.dataPackageGraph.statementsMatching( - removedObjNode - ) - ) - ); - - // Remove all the statements mentioning this object - try { - this.dataPackageGraph.remove(statements); - } catch (error) { - console.log(error); + // If this object documents a data object + if (documents && documents.length) { + // Create a literal node for it + var metadataNode = this.rdf.sym(this.getURIFromRDF(id)); + + _.each( + documents, + function (dataID) { + //Make sure the id is one that will be aggregated + if (_.contains(this.idsToAggregate, dataID)) { + //Find the identifier statement for this data object + var dataURI = this.getURIFromRDF(dataID); + + //Create a data node using the exact way the identifier URI is written + var dataNode = this.rdf.sym(dataURI); + + //Get the statements for data isDocumentedBy metadata + isDocumentedByStatements = + this.dataPackageGraph.statementsMatching( + dataNode, + CITO("isDocumentedBy"), + metadataNode, + ); + + //If that statement is not in the RDF already... + if (isDocumentedByStatements.length < 1) { + // Create a statement: This data is documented by this metadata + var isDocByStatement = this.rdf.st( + dataNode, + CITO("isDocumentedBy"), + metadataNode, + ); + //Add the "isDocumentedBy" statement + this.dataPackageGraph.add(isDocByStatement); } - }, - - /** - * Finds the given identifier in the RDF graph and returns the subject - * URI of that statement. This is useful when adding additional statements - * to the RDF graph for an object that already exists in that graph. - * - * @param {string} id - The identifier to search for - * @return {string} - The full URI for the given id as it exists in the RDF. - */ - getURIFromRDF: function (id) { - //Exit if no id was given - if (!id) return ""; - - //Create a literal node with the identifier as the value - var XSD = this.rdf.Namespace(this.namespaces.XSD), - DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), - idNode = this.rdf.literal(id, undefined, XSD("string")), - //Find the identifier statements for the given id - idStatements = this.dataPackageGraph.statementsMatching( - undefined, - DCTERMS("identifier"), - idNode - ); - //If this object has an identifier statement, - if (idStatements.length > 0) { - //Return the subject of the statement - return idStatements[0].subject.value; - } else { - return this.getCnURI() + encodeURIComponent(id); - } - }, + //Get the statements for metadata documents data + documentsStatements = this.dataPackageGraph.statementsMatching( + metadataNode, + CITO("documents"), + dataNode, + ); - /** - * Parses out the CN Resolve URL from the existing statements in the RDF - * or if not found in the RDF, from the app configuration. - * - * @return {string} - The CN resolve URL - */ - getCnURI: function () { - //If the CN resolve URL was already found, return it - if (this.dataPackageGraph.cnResolveUrl) { - return this.dataPackageGraph.cnResolveUrl; - } else if (this.packageModel.get("oldPid")) { - //Find the identifier statement for the resource map in the RDF graph - var idNode = this.rdf.lit(this.packageModel.get("oldPid")), - idStatements = this.dataPackageGraph.statementsMatching( - undefined, - undefined, - idNode - ), - idStatement = idStatements.length - ? idStatements[0] - : null; - - if (idStatement) { - //Parse the CN resolve URL from the statement subject URI - this.dataPackageGraph.cnResolveUrl = - idStatement.subject.value.substring( - 0, - idStatement.subject.value.indexOf( - this.packageModel.get("oldPid") - ) - ) || - idStatement.subject.value.substring( - 0, - idStatement.subject.value.indexOf( - encodeURIComponent( - this.packageModel.get("oldPid") - ) - ) - ); - } else { - this.dataPackageGraph.cnResolveUrl = - MetacatUI.appModel.get("resolveServiceUrl"); - } - } else { - this.dataPackageGraph.cnResolveUrl = - MetacatUI.appModel.get("resolveServiceUrl"); + //If that statement is not in the RDF already... + if (documentsStatements.length < 1) { + // Create a statement: This metadata documents data + var documentsStatement = this.rdf.st( + metadataNode, + CITO("documents"), + dataNode, + ); + //Add the "isDocumentedBy" statement + this.dataPackageGraph.add(documentsStatement); } - - //Return the CN resolve URL - return this.dataPackageGraph.cnResolveUrl; + } }, + this, + ); + } + }, - /** - * Checks if this resource map has had any changes that requires an update - */ - needsUpdate: function () { - //Check for changes to the list of aggregated members - var ids = this.pluck("id"); - if ( - this.originalMembers.length != ids.length || - _.intersection(this.originalMembers, ids).length != - ids.length - ) - return true; - - // If the provenance relationships have been updated, then the resource map - // needs to be updated. - if (this.provEdits.length) return true; - //Check for changes to the isDocumentedBy relationships - var isDifferent = false, - i = 0; - - //Keep going until we find a difference - while (!isDifferent && i < this.length) { - //Get the original isDocBy relationships from the resource map, and the new isDocBy relationships from the models - var isDocBy = this.models[i].get("isDocumentedBy"), - id = this.models[i].get("id"), - origIsDocBy = this.originalIsDocBy[id]; - - //Make sure they are both formatted as arrays for these checks - isDocBy = _.uniq( - _.flatten( - _.compact( - Array.isArray(isDocBy) ? isDocBy : [isDocBy] - ) - ) - ); - origIsDocBy = _.uniq( - _.flatten( - _.compact( - Array.isArray(origIsDocBy) - ? origIsDocBy - : [origIsDocBy] - ) - ) - ); - - //Remove the id of this object so metadata can not be "isDocumentedBy" itself - isDocBy = _.without(isDocBy, id); - origIsDocBy = _.without(origIsDocBy, id); - - //Simply check if they are the same - if (origIsDocBy === isDocBy) { - i++; - continue; - } - //Are the number of relationships different? - else if (isDocBy.length != origIsDocBy.length) - isDifferent = true; - //Are the arrays the same? - else if ( - _.intersection(isDocBy, origIsDocBy).length != - origIsDocBy.length - ) - isDifferent = true; - - i++; - } + /* + * Removes an object from the aggregation in the RDF graph + */ + removeFromAggregation: function (id) { + if (id.indexOf(this.dataPackageGraph.cnResolveUrl) == -1) { + id = this.getURIFromRDF(id); + } - return isDifferent; - }, + // Create a literal node for the removed object + var removedObjNode = this.rdf.sym(id), + // Get the statements from the RDF where the removed object is the subject or object + statements = $.extend( + true, + [], + _.union( + this.dataPackageGraph.statementsMatching( + undefined, + undefined, + removedObjNode, + ), + this.dataPackageGraph.statementsMatching(removedObjNode), + ), + ); + + // Remove all the statements mentioning this object + try { + this.dataPackageGraph.remove(statements); + } catch (error) { + console.log(error); + } + }, + + /** + * Finds the given identifier in the RDF graph and returns the subject + * URI of that statement. This is useful when adding additional statements + * to the RDF graph for an object that already exists in that graph. + * + * @param {string} id - The identifier to search for + * @return {string} - The full URI for the given id as it exists in the RDF. + */ + getURIFromRDF: function (id) { + //Exit if no id was given + if (!id) return ""; + + //Create a literal node with the identifier as the value + var XSD = this.rdf.Namespace(this.namespaces.XSD), + DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), + idNode = this.rdf.literal(id, undefined, XSD("string")), + //Find the identifier statements for the given id + idStatements = this.dataPackageGraph.statementsMatching( + undefined, + DCTERMS("identifier"), + idNode, + ); + + //If this object has an identifier statement, + if (idStatements.length > 0) { + //Return the subject of the statement + return idStatements[0].subject.value; + } else { + return this.getCnURI() + encodeURIComponent(id); + } + }, - /* - * Returns an array of the models that are in the queue or in progress of uploading - */ - getQueue: function () { - return this.filter(function (m) { - return ( - m.get("uploadStatus") == "q" || - m.get("uploadStatus") == "p" - ); - }); - }, + /** + * Parses out the CN Resolve URL from the existing statements in the RDF + * or if not found in the RDF, from the app configuration. + * + * @return {string} - The CN resolve URL + */ + getCnURI: function () { + //If the CN resolve URL was already found, return it + if (this.dataPackageGraph.cnResolveUrl) { + return this.dataPackageGraph.cnResolveUrl; + } else if (this.packageModel.get("oldPid")) { + //Find the identifier statement for the resource map in the RDF graph + var idNode = this.rdf.lit(this.packageModel.get("oldPid")), + idStatements = this.dataPackageGraph.statementsMatching( + undefined, + undefined, + idNode, + ), + idStatement = idStatements.length ? idStatements[0] : null; + + if (idStatement) { + //Parse the CN resolve URL from the statement subject URI + this.dataPackageGraph.cnResolveUrl = + idStatement.subject.value.substring( + 0, + idStatement.subject.value.indexOf( + this.packageModel.get("oldPid"), + ), + ) || + idStatement.subject.value.substring( + 0, + idStatement.subject.value.indexOf( + encodeURIComponent(this.packageModel.get("oldPid")), + ), + ); + } else { + this.dataPackageGraph.cnResolveUrl = + MetacatUI.appModel.get("resolveServiceUrl"); + } + } else { + this.dataPackageGraph.cnResolveUrl = + MetacatUI.appModel.get("resolveServiceUrl"); + } - /* - * Adds a DataONEObject model to this DataPackage collection - */ - addNewModel: function (model) { - //Check that this collection doesn't already contain this model - if (!this.contains(model)) { - this.add(model); - - //Mark this data package as changed - this.packageModel.set("changed", true); - this.packageModel.trigger("change:changed"); - } - }, + //Return the CN resolve URL + return this.dataPackageGraph.cnResolveUrl; + }, - handleAdd: function (dataONEObject) { - var metadataModel = this.find(function (m) { - return m.get("type") == "Metadata"; - }); + /** + * Checks if this resource map has had any changes that requires an update + */ + needsUpdate: function () { + //Check for changes to the list of aggregated members + var ids = this.pluck("id"); + if ( + this.originalMembers.length != ids.length || + _.intersection(this.originalMembers, ids).length != ids.length + ) + return true; + + // If the provenance relationships have been updated, then the resource map + // needs to be updated. + if (this.provEdits.length) return true; + //Check for changes to the isDocumentedBy relationships + var isDifferent = false, + i = 0; + + //Keep going until we find a difference + while (!isDifferent && i < this.length) { + //Get the original isDocBy relationships from the resource map, and the new isDocBy relationships from the models + var isDocBy = this.models[i].get("isDocumentedBy"), + id = this.models[i].get("id"), + origIsDocBy = this.originalIsDocBy[id]; + + //Make sure they are both formatted as arrays for these checks + isDocBy = _.uniq( + _.flatten(_.compact(Array.isArray(isDocBy) ? isDocBy : [isDocBy])), + ); + origIsDocBy = _.uniq( + _.flatten( + _.compact( + Array.isArray(origIsDocBy) ? origIsDocBy : [origIsDocBy], + ), + ), + ); + + //Remove the id of this object so metadata can not be "isDocumentedBy" itself + isDocBy = _.without(isDocBy, id); + origIsDocBy = _.without(origIsDocBy, id); + + //Simply check if they are the same + if (origIsDocBy === isDocBy) { + i++; + continue; + } + //Are the number of relationships different? + else if (isDocBy.length != origIsDocBy.length) isDifferent = true; + //Are the arrays the same? + else if ( + _.intersection(isDocBy, origIsDocBy).length != origIsDocBy.length + ) + isDifferent = true; + + i++; + } - // Append to or create a new documents list - if (metadataModel) { - if (!Array.isArray(metadataModel.get("documents"))) { - metadataModel.set("documents", [dataONEObject.id]); - } else { - if ( - !_.contains( - metadataModel.get("documents"), - dataONEObject.id - ) - ) - metadataModel - .get("documents") - .push(dataONEObject.id); - } + return isDifferent; + }, - // Create an EML Entity for this DataONE Object if there isn't one already - if ( - metadataModel.type == "EML" && - !dataONEObject.get("metadataEntity") && - dataONEObject.type != "EML" - ) { - metadataModel.createEntity(dataONEObject); - metadataModel.set("uploadStatus", "q"); - } - } + /* + * Returns an array of the models that are in the queue or in progress of uploading + */ + getQueue: function () { + return this.filter(function (m) { + return m.get("uploadStatus") == "q" || m.get("uploadStatus") == "p"; + }); + }, + + /* + * Adds a DataONEObject model to this DataPackage collection + */ + addNewModel: function (model) { + //Check that this collection doesn't already contain this model + if (!this.contains(model)) { + this.add(model); + + //Mark this data package as changed + this.packageModel.set("changed", true); + this.packageModel.trigger("change:changed"); + } + }, + + handleAdd: function (dataONEObject) { + var metadataModel = this.find(function (m) { + return m.get("type") == "Metadata"; + }); + + // Append to or create a new documents list + if (metadataModel) { + if (!Array.isArray(metadataModel.get("documents"))) { + metadataModel.set("documents", [dataONEObject.id]); + } else { + if (!_.contains(metadataModel.get("documents"), dataONEObject.id)) + metadataModel.get("documents").push(dataONEObject.id); + } + + // Create an EML Entity for this DataONE Object if there isn't one already + if ( + metadataModel.type == "EML" && + !dataONEObject.get("metadataEntity") && + dataONEObject.type != "EML" + ) { + metadataModel.createEntity(dataONEObject); + metadataModel.set("uploadStatus", "q"); + } + } - this.saveReference(dataONEObject); + this.saveReference(dataONEObject); - this.setLoadingFiles(dataONEObject); + this.setLoadingFiles(dataONEObject); - //Save a reference to this DataPackage - // If the collections attribute is an array - /* if( Array.isArray(dataONEObject.get("collections")) ){ + //Save a reference to this DataPackage + // If the collections attribute is an array + /* if( Array.isArray(dataONEObject.get("collections")) ){ //Add this DataPackage to the collections list if it's not already in the array if( !_.contains(dataONEObject.get("collections"), this) ){ dataONEObject.get("collections").push(this); @@ -3877,271 +3548,250 @@

Source: src/js/collections/DataPackage.js

dataONEObject.set("collections", [this]); } */ - }, + }, - /** - * Fetches this DataPackage from the Solr index by using a SolrResults collection - * and merging the models in. - */ - fetchFromIndex: function () { - if ( - typeof this.solrResults == "undefined" || - !this.solrResults - ) { - this.solrResults = new SolrResults(); - } - - //If no query is set yet, use the FilterModel associated with this DataPackage - if (!this.solrResults.currentquery.length) { - this.solrResults.currentquery = this.filterModel.getQuery(); - } - - this.listenToOnce( - this.solrResults, - "reset", - function (solrResults) { - //Merge the SolrResults into this collection - this.mergeModels(solrResults.models); - - //Trigger the fetch as complete - this.trigger("complete"); - } - ); + /** + * Fetches this DataPackage from the Solr index by using a SolrResults collection + * and merging the models in. + */ + fetchFromIndex: function () { + if (typeof this.solrResults == "undefined" || !this.solrResults) { + this.solrResults = new SolrResults(); + } - //Query the index for this data package - this.solrResults.query(); - }, + //If no query is set yet, use the FilterModel associated with this DataPackage + if (!this.solrResults.currentquery.length) { + this.solrResults.currentquery = this.filterModel.getQuery(); + } - /** - * Merge the attributes of other models into the corresponding models in this collection. - * This should be used when merging models of other types (e.g. SolrResult) that represent the same - * object that the DataONEObject models in the collection represent. - * - * @param {Backbone.Model[]} otherModels - the other models to merge with the models in this collection - * @param {string[]} [fieldsToMerge] - If specified, only these fields will be extracted from the otherModels - */ - mergeModels: function (otherModels, fieldsToMerge) { - //If no otherModels are given, exit the function since there is nothing to merge - if ( - typeof otherModels == "undefined" || - !otherModels || - !otherModels.length - ) { - return false; - } + this.listenToOnce(this.solrResults, "reset", function (solrResults) { + //Merge the SolrResults into this collection + this.mergeModels(solrResults.models); + + //Trigger the fetch as complete + this.trigger("complete"); + }); + + //Query the index for this data package + this.solrResults.query(); + }, + + /** + * Merge the attributes of other models into the corresponding models in this collection. + * This should be used when merging models of other types (e.g. SolrResult) that represent the same + * object that the DataONEObject models in the collection represent. + * + * @param {Backbone.Model[]} otherModels - the other models to merge with the models in this collection + * @param {string[]} [fieldsToMerge] - If specified, only these fields will be extracted from the otherModels + */ + mergeModels: function (otherModels, fieldsToMerge) { + //If no otherModels are given, exit the function since there is nothing to merge + if ( + typeof otherModels == "undefined" || + !otherModels || + !otherModels.length + ) { + return false; + } - _.each( - otherModels, - function (otherModel) { - //Get the model from this collection that matches ids with the other model - var modelInDataPackage = this.findWhere({ - id: otherModel.get("id"), - }); - - //If a match is found, - if (modelInDataPackage) { - var valuesFromOtherModel; - - //If specific fields to merge are given, get the values for those from the other model - if (fieldsToMerge && fieldsToMerge.length) { - valuesFromOtherModel = _.pick( - otherModel.toJSON(), - fieldsToMerge - ); - } - //If no specific fields are given, merge (almost) all others - else { - //Get the default values for this model type - var otherModelDefaults = otherModel.defaults, - //Get a JSON object of all the attributes on this model - otherModelAttr = otherModel.toJSON(), - //Start an array of attributes to omit during the merge - omitKeys = []; - - _.each(otherModelAttr, function (val, key) { - //If this model's attribute is the default, don't set it on our DataONEObject model - // because whatever value is in the DataONEObject model is better information than the default - // value of the other model. - if (otherModelDefaults[key] === val) - omitKeys.push(key); - }); - - //Remove the properties that are still the default value - valuesFromOtherModel = _.omit( - otherModelAttr, - omitKeys - ); - } - - //Set the values from the other model on the model in this collection - modelInDataPackage.set(valuesFromOtherModel); - } - }, - this + _.each( + otherModels, + function (otherModel) { + //Get the model from this collection that matches ids with the other model + var modelInDataPackage = this.findWhere({ + id: otherModel.get("id"), + }); + + //If a match is found, + if (modelInDataPackage) { + var valuesFromOtherModel; + + //If specific fields to merge are given, get the values for those from the other model + if (fieldsToMerge && fieldsToMerge.length) { + valuesFromOtherModel = _.pick( + otherModel.toJSON(), + fieldsToMerge, ); - }, - - /** - * Update the relationships in this resource map when its been udpated - */ - updateRelationships: function () { - //Get the old id - var oldId = this.packageModel.get("oldPid"); - - if (!oldId) return; - - //Update the resource map list - this.each(function (m) { - var updateRMaps = _.without(m.get("resourceMap"), oldId); - updateRMaps.push(this.packageModel.get("id")); - - m.set("resourceMap", updateRMaps); - }, this); - }, - - saveReference: function (model) { - //Save a reference to this collection in the model - var currentCollections = model.get("collections"); - if (currentCollections.length > 0) { - currentCollections.push(this); - model.set("collections", _.uniq(currentCollections)); - } else model.set("collections", [this]); - }, + } + //If no specific fields are given, merge (almost) all others + else { + //Get the default values for this model type + var otherModelDefaults = otherModel.defaults, + //Get a JSON object of all the attributes on this model + otherModelAttr = otherModel.toJSON(), + //Start an array of attributes to omit during the merge + omitKeys = []; + + _.each(otherModelAttr, function (val, key) { + //If this model's attribute is the default, don't set it on our DataONEObject model + // because whatever value is in the DataONEObject model is better information than the default + // value of the other model. + if (otherModelDefaults[key] === val) omitKeys.push(key); + }); - /** - * Broadcast an accessPolicy across members of this package - * - * Note: Currently just sets the incoming accessPolicy on this - * object and doesn't broadcast to other members (such as data). - * How this works is likely to change in the future. - * - * Closely tied to the AccessPolicyView.broadcast property. - * - * @param {AccessPolicy} accessPolicy - The accessPolicy to - * broadcast - */ - broadcastAccessPolicy: function (accessPolicy) { - if (!accessPolicy) { - return; - } + //Remove the properties that are still the default value + valuesFromOtherModel = _.omit(otherModelAttr, omitKeys); + } - var policy = _.clone(accessPolicy); - this.packageModel.set("accessPolicy", policy); + //Set the values from the other model on the model in this collection + modelInDataPackage.set(valuesFromOtherModel); + } + }, + this, + ); + }, - // Stop now if the package is new because we don't want force - // a save just yet - if (this.packageModel.isNew()) { - return; - } + /** + * Update the relationships in this resource map when its been udpated + */ + updateRelationships: function () { + //Get the old id + var oldId = this.packageModel.get("oldPid"); + + if (!oldId) return; + + //Update the resource map list + this.each(function (m) { + var updateRMaps = _.without(m.get("resourceMap"), oldId); + updateRMaps.push(this.packageModel.get("id")); + + m.set("resourceMap", updateRMaps); + }, this); + }, + + saveReference: function (model) { + //Save a reference to this collection in the model + var currentCollections = model.get("collections"); + if (currentCollections.length > 0) { + currentCollections.push(this); + model.set("collections", _.uniq(currentCollections)); + } else model.set("collections", [this]); + }, + + /** + * Broadcast an accessPolicy across members of this package + * + * Note: Currently just sets the incoming accessPolicy on this + * object and doesn't broadcast to other members (such as data). + * How this works is likely to change in the future. + * + * Closely tied to the AccessPolicyView.broadcast property. + * + * @param {AccessPolicy} accessPolicy - The accessPolicy to + * broadcast + */ + broadcastAccessPolicy: function (accessPolicy) { + if (!accessPolicy) { + return; + } - this.packageModel.on("sysMetaUpdateError", function (e) { - // Show a generic error. Any errors at this point are things the - // user can't really recover from. i.e., we've already checked - // that the user has changePermission perms and we've already - // re-tried the request a few times - var message = - "There was an error sharing your dataset. Not all of your changes were applied."; - - // TODO: Is this really the right way to hook into the editor's - // error notification mechanism? - MetacatUI.appView.eml211EditorView.saveError(message); - }); + var policy = _.clone(accessPolicy); + this.packageModel.set("accessPolicy", policy); - this.packageModel.updateSysMeta(); - }, + // Stop now if the package is new because we don't want force + // a save just yet + if (this.packageModel.isNew()) { + return; + } - /** - * Tracks the upload status of DataONEObject models in this collection. If they are - * `loading` into the DOM or `in progress` of an upload to the server, they will be considered as "loading" files. - * @param {DataONEObject} [dataONEObject] - A model to begin tracking. Optional. If no DataONEObject is given, then only - * the number of loading files will be calcualted and set on the packageModel. - * @since 2.17.1 - */ - setLoadingFiles: function (dataONEObject) { - //Set the number of loading files and the isLoadingFiles flag - let numLoadingFiles = - this.where({ uploadStatus: "l" }).length + - this.where({ uploadStatus: "p" }).length; + this.packageModel.on("sysMetaUpdateError", function (e) { + // Show a generic error. Any errors at this point are things the + // user can't really recover from. i.e., we've already checked + // that the user has changePermission perms and we've already + // re-tried the request a few times + var message = + "There was an error sharing your dataset. Not all of your changes were applied."; + + // TODO: Is this really the right way to hook into the editor's + // error notification mechanism? + MetacatUI.appView.eml211EditorView.saveError(message); + }); + + this.packageModel.updateSysMeta(); + }, + + /** + * Tracks the upload status of DataONEObject models in this collection. If they are + * `loading` into the DOM or `in progress` of an upload to the server, they will be considered as "loading" files. + * @param {DataONEObject} [dataONEObject] - A model to begin tracking. Optional. If no DataONEObject is given, then only + * the number of loading files will be calcualted and set on the packageModel. + * @since 2.17.1 + */ + setLoadingFiles: function (dataONEObject) { + //Set the number of loading files and the isLoadingFiles flag + let numLoadingFiles = + this.where({ uploadStatus: "l" }).length + + this.where({ uploadStatus: "p" }).length; + this.packageModel.set({ + isLoadingFiles: numLoadingFiles > 0, + numLoadingFiles: numLoadingFiles, + }); + + if (dataONEObject) { + //Listen to the upload status to update the flag + this.listenTo(dataONEObject, "change:uploadStatus", function () { + //If the object is done being successfully saved + if (dataONEObject.get("uploadStatus") == "c") { + let numLoadingFiles = + this.where({ uploadStatus: "l" }).length + + this.where({ uploadStatus: "p" }).length; + + //If all models in this DataPackage have finished loading, then mark the loading as complete + if (!numLoadingFiles) { this.packageModel.set({ - isLoadingFiles: numLoadingFiles > 0, - numLoadingFiles: numLoadingFiles, + isLoadingFiles: false, + numLoadingFiles: numLoadingFiles, }); - - if (dataONEObject) { - //Listen to the upload status to update the flag - this.listenTo( - dataONEObject, - "change:uploadStatus", - function () { - //If the object is done being successfully saved - if (dataONEObject.get("uploadStatus") == "c") { - let numLoadingFiles = - this.where({ uploadStatus: "l" }).length + - this.where({ uploadStatus: "p" }).length; - - //If all models in this DataPackage have finished loading, then mark the loading as complete - if (!numLoadingFiles) { - this.packageModel.set({ - isLoadingFiles: false, - numLoadingFiles: numLoadingFiles, - }); - } else { - this.packageModel.set( - "numLoadingFiles", - numLoadingFiles - ); - } - } - } - ); - } - }, - - /** - * Returns atLocation information found in this resourceMap - * for all the PIDs in this resourceMap - * @returns object with PIDs as key and atLocation paths as values - * @since 2.28.0 - */ - getAtLocation: function () { - return this.atLocationObject; - }, - - /** - * Get the absolute path from a relative path, handling '~', '..', and '.'. - * - * @param {string} relativePath - The relative path to be converted to an absolute path. - * @returns {string} - The absolute path after processing '~', '..', and '.'. - * If the result is empty, returns '/'. - * @since 2.28.0 - */ - getAbsolutePath(relativePath) { - // Replace ~ with an empty space - const fullPath = relativePath.replace(/^~(?=$|\/|\\)/, ""); - - // Process '..' and '.' - const components = fullPath.split("/"); - const resolvedPath = components.reduce( - (accumulator, component) => { - if (component === "..") { - accumulator.pop(); - } else if (component !== "." && component !== "") { - accumulator.push(component); - } - return accumulator; - }, - [] - ); - - // Join the resolved path components with '/' - const result = resolvedPath.join("/"); - - return result || "/"; - }, + } else { + this.packageModel.set("numLoadingFiles", numLoadingFiles); + } + } + }); } - ); + }, - return DataPackage; + /** + * Returns atLocation information found in this resourceMap + * for all the PIDs in this resourceMap + * @returns object with PIDs as key and atLocation paths as values + * @since 2.28.0 + */ + getAtLocation: function () { + return this.atLocationObject; + }, + + /** + * Get the absolute path from a relative path, handling '~', '..', and '.'. + * + * @param {string} relativePath - The relative path to be converted to an absolute path. + * @returns {string} - The absolute path after processing '~', '..', and '.'. + * If the result is empty, returns '/'. + * @since 2.28.0 + */ + getAbsolutePath(relativePath) { + // Replace ~ with an empty space + const fullPath = relativePath.replace(/^~(?=$|\/|\\)/, ""); + + // Process '..' and '.' + const components = fullPath.split("/"); + const resolvedPath = components.reduce((accumulator, component) => { + if (component === "..") { + accumulator.pop(); + } else if (component !== "." && component !== "") { + accumulator.push(component); + } + return accumulator; + }, []); + + // Join the resolved path components with '/' + const result = resolvedPath.join("/"); + + return result || "/"; + }, + }, + ); + + return DataPackage; });
diff --git a/docs/docs/src_js_collections_Filters.js.html b/docs/docs/src_js_collections_Filters.js.html index bcb04776a..92f9161d8 100644 --- a/docs/docs/src_js_collections_Filters.js.html +++ b/docs/docs/src_js_collections_Filters.js.html @@ -65,7 +65,7 @@

Source: src/js/collections/Filters.js

DateFilter, NumericFilter, ToggleFilter, - SpatialFilter + SpatialFilter, ) { "use strict"; @@ -79,7 +79,6 @@

Source: src/js/collections/Filters.js

*/ var Filters = Backbone.Collection.extend( /** @lends Filters.prototype */ { - /** * The name of this type of collection * @type {string} @@ -129,7 +128,7 @@

Source: src/js/collections/Filters.js

} } catch (error) { console.log( - "Error initializing a Filters collection. Error details: " + error + "Error initializing a Filters collection. Error details: " + error, ); } }, @@ -325,7 +324,7 @@

Source: src/js/collections/Filters.js

}); } catch (error) { console.log( - "Error trying to find ID Filters, error details: " + error + "Error trying to find ID Filters, error details: " + error, ); } }, @@ -342,7 +341,7 @@

Source: src/js/collections/Filters.js

return this.difference(this.getIdFilters()); } catch (error) { console.log( - "Error trying to find non-ID Filters, error details: " + error + "Error trying to find non-ID Filters, error details: " + error, ); } }, @@ -382,13 +381,13 @@

Source: src/js/collections/Filters.js

groupQueryFragments.push(filterQuery); } }, - this + this, ); //Join this group's query fragments with an OR operator if (groupQueryFragments.length) { var queryString = groupQueryFragments.join( - "%20" + operator + "%20" + "%20" + operator + "%20", ); if (groupQueryFragments.length > 1) { queryString = "(" + queryString + ")"; @@ -401,7 +400,8 @@

Source: src/js/collections/Filters.js

} } catch (e) { console.log( - "Error creating a group query, returning a blank string. ", e + "Error creating a group query, returning a blank string. ", + e, ); return ""; } @@ -443,7 +443,7 @@

Source: src/js/collections/Filters.js

filterModel.get("values").length && _.difference( filterModel.get("values"), - filterModel.defaults().values + filterModel.defaults().values, ).length) || (!Array.isArray(filterModel.get("values")) && filterModel.get("values") !== filterModel.defaults().values)) @@ -491,8 +491,8 @@

Source: src/js/collections/Filters.js

return field.indexOf("geohash") > -1; }); } - }) - ) + }), + ), ); }, @@ -539,7 +539,7 @@

Source: src/js/collections/Filters.js

exclude: true, matchSubstring: true, operator: "OR", - } + }, ]); var query = catalogFilters.getGroupQuery(catalogFilters.models, "AND"); return query; @@ -625,7 +625,7 @@

Source: src/js/collections/Filters.js

} catch (e) { console.log( "Failed to remove empty Filter models from the Filters collection, error message: " + - e + e, ); } }, @@ -644,7 +644,7 @@

Source: src/js/collections/Filters.js

} catch (e) { console.log( "Failed to remove empty Filter models from the Filters collection, error message: " + - e + e, ); } }, @@ -671,7 +671,7 @@

Source: src/js/collections/Filters.js

return newModel; } catch (e) { console.log( - "Failed to replace a Filter model in a Filters collection, " + e + "Failed to replace a Filter model in a Filters collection, " + e, ); } }, @@ -696,11 +696,11 @@

Source: src/js/collections/Filters.js

} catch (e) { console.log( "Failed to get the index of a Filter within the collection of visible Filters, error message: " + - e + e, ); } }, - } + }, ); return Filters; }); diff --git a/docs/docs/src_js_collections_ObjectFormats.js.html b/docs/docs/src_js_collections_ObjectFormats.js.html index 80e6338c4..96b2808f8 100644 --- a/docs/docs/src_js_collections_ObjectFormats.js.html +++ b/docs/docs/src_js_collections_ObjectFormats.js.html @@ -44,70 +44,68 @@

Source: src/js/collections/ObjectFormats.js

-
/* global define */
-"use strict";
-
-define(['jquery', 'underscore', 'backbone', 'x2js', 'models/formats/ObjectFormat'],
-    function($, _, Backbone, X2JS, ObjectFormat) {
-
-    /**
-     * @class ObjectFormats
-     * @classdesc ObjectFormats represents the DataONE object format list
-     * found at https://cn.dataone.org/cn/v2/formats, or
-     * the Coordinating Node environment configured `AppModel.d1CNBaseUrl`
-     * This collection is intended to be used as a formats cache -
-     * retrieved once, and only refreshed later if an object format
-     * isn't present when needed.
-     * @classcategory Collections
-     * @extends Backbone.Collection
-     * @constructor
-     */
-    var ObjectFormats = Backbone.Collection.extend(
-      /** @lends ObjectFormats.prototype */{
-
-        model: ObjectFormat,
-
-        /**
-         * The constructed URL of the collection
-         * (/cn/v2/formats)
-         * @returns {string} - The URL to use during fetch
-         */
-        url: function() {
-
-            // no need for authentication token, just the URL
-            return MetacatUI.appModel.get("formatsServiceUrl");
-
-        },
-
-        /**
-        * Retrieve the formats from the Coordinating Node
-        * @extends Backbone.Collection#fetch
-        */
-        fetch: function(options) {
-            var fetchOptions = _.extend({dataType: "text"}, options);
-
-            return Backbone.Model.prototype.fetch.call(this, fetchOptions);
-
-        },
-
-        /**
-        * Parse the XML response from the CN
-        */
-        parse: function(response) {
-
-            // If the collection is already parsed, just return it
-            if ( typeof response === "object" ) return response;
-
-            // Otherwise, parse it
-            var x2js = new X2JS();
-            var formats = x2js.xml_str2json(response);
-
-            return formats.objectFormatList.objectFormat;
-        }
-
-    });
-
-    return ObjectFormats;
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "x2js",
+  "models/formats/ObjectFormat",
+], function ($, _, Backbone, X2JS, ObjectFormat) {
+  /**
+   * @class ObjectFormats
+   * @classdesc ObjectFormats represents the DataONE object format list
+   * found at https://cn.dataone.org/cn/v2/formats, or
+   * the Coordinating Node environment configured `AppModel.d1CNBaseUrl`
+   * This collection is intended to be used as a formats cache -
+   * retrieved once, and only refreshed later if an object format
+   * isn't present when needed.
+   * @classcategory Collections
+   * @extends Backbone.Collection
+   * @constructor
+   */
+  var ObjectFormats = Backbone.Collection.extend(
+    /** @lends ObjectFormats.prototype */ {
+      model: ObjectFormat,
+
+      /**
+       * The constructed URL of the collection
+       * (/cn/v2/formats)
+       * @returns {string} - The URL to use during fetch
+       */
+      url: function () {
+        // no need for authentication token, just the URL
+        return MetacatUI.appModel.get("formatsServiceUrl");
+      },
+
+      /**
+       * Retrieve the formats from the Coordinating Node
+       * @extends Backbone.Collection#fetch
+       */
+      fetch: function (options) {
+        var fetchOptions = _.extend({ dataType: "text" }, options);
+
+        return Backbone.Model.prototype.fetch.call(this, fetchOptions);
+      },
+
+      /**
+       * Parse the XML response from the CN
+       */
+      parse: function (response) {
+        // If the collection is already parsed, just return it
+        if (typeof response === "object") return response;
+
+        // Otherwise, parse it
+        var x2js = new X2JS();
+        var formats = x2js.xml_str2json(response);
+
+        return formats.objectFormatList.objectFormat;
+      },
+    },
+  );
+
+  return ObjectFormats;
 });
 
diff --git a/docs/docs/src_js_collections_ProjectList.js.html b/docs/docs/src_js_collections_ProjectList.js.html index a43dd5e14..5411921dc 100644 --- a/docs/docs/src_js_collections_ProjectList.js.html +++ b/docs/docs/src_js_collections_ProjectList.js.html @@ -44,12 +44,14 @@

Source: src/js/collections/ProjectList.js

-
/* global define */
-"use strict";
-define(['jquery', 'backbone', 'models/projects/Project'],
-    function ($, Backbone, Project) {
-        'use strict';
-        /**
+            
"use strict";
+define(["jquery", "backbone", "models/projects/Project"], function (
+  $,
+  Backbone,
+  Project,
+) {
+  "use strict";
+  /**
          @class ProjectList
          @classdesc A ProjectList represents a collection of projects. This can be
          used for a projects list view populating EML projects. It also supports loading
@@ -61,62 +63,67 @@ 

Source: src/js/collections/ProjectList.js

@since 2.22.0 */ - var ProjectList = Backbone.Collection.extend( - /** @lends ProjectList.prototype */{ - model: Project, - type: "ProjectList", //The name of this type of collection - authToken: undefined, - urlBase: undefined, - urlEndpoint: "project/", - - /** Builds the url from the urlBase **/ - url: function(){ - if(this.urlBase) - { - return new URL(new URL(this.urlBase).pathname + this.urlEndpoint, this.urlBase).href - } - else { - return undefined - } - }, - /** - * Override backbone's parse to set the data after the request returns from the server - */ - parse: function (response, options) { - // Add any custom data structure code here. - return response - }, - /** - * Override backbone's sync to set the auth token - */ - sync: function(method, model, options) { - - if (this.authToken) { - if (options.headers === undefined){ - options.headers = {} - } - options.headers["Authorization"] = "Bearer " + this.authToken; - } - if (this.urlBase) - return Backbone.Model.prototype.sync.apply(this, [method, model, options]); - }, - - /** - * Initializing the Model objects project variables. - */ - initialize: function (options) { - if (MetacatUI && MetacatUI.appModel) this.urlBase = MetacatUI.appModel.get("projectsApiUrl"); - if (options) { - if (options.authToken) this.authToken = options.authToken; - if (options.urlBase) this.urlBase = options.urlBase; - } - Backbone.Collection.prototype.initialize.apply(this, options); - } - }); - - return ProjectList - - }); + var ProjectList = Backbone.Collection.extend( + /** @lends ProjectList.prototype */ { + model: Project, + type: "ProjectList", //The name of this type of collection + authToken: undefined, + urlBase: undefined, + urlEndpoint: "project/", + + /** Builds the url from the urlBase **/ + url: function () { + if (this.urlBase) { + return new URL( + new URL(this.urlBase).pathname + this.urlEndpoint, + this.urlBase, + ).href; + } else { + return undefined; + } + }, + /** + * Override backbone's parse to set the data after the request returns from the server + */ + parse: function (response, options) { + // Add any custom data structure code here. + return response; + }, + /** + * Override backbone's sync to set the auth token + */ + sync: function (method, model, options) { + if (this.authToken) { + if (options.headers === undefined) { + options.headers = {}; + } + options.headers["Authorization"] = "Bearer " + this.authToken; + } + if (this.urlBase) + return Backbone.Model.prototype.sync.apply(this, [ + method, + model, + options, + ]); + }, + + /** + * Initializing the Model objects project variables. + */ + initialize: function (options) { + if (MetacatUI && MetacatUI.appModel) + this.urlBase = MetacatUI.appModel.get("projectsApiUrl"); + if (options) { + if (options.authToken) this.authToken = options.authToken; + if (options.urlBase) this.urlBase = options.urlBase; + } + Backbone.Collection.prototype.initialize.apply(this, options); + }, + }, + ); + + return ProjectList; +});
diff --git a/docs/docs/src_js_collections_QualityReport.js.html b/docs/docs/src_js_collections_QualityReport.js.html index 099bb8bf9..92f9c8483 100644 --- a/docs/docs/src_js_collections_QualityReport.js.html +++ b/docs/docs/src_js_collections_QualityReport.js.html @@ -44,13 +44,16 @@

Source: src/js/collections/QualityReport.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5",
-    'models/QualityCheckModel'
-  ],
-  function ($, _, Backbone, rdf, uuid, md5, QualityCheck) {
-
-    /**
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "rdflib",
+  "uuid",
+  "md5",
+  "models/QualityCheckModel",
+], function ($, _, Backbone, rdf, uuid, md5, QualityCheck) {
+  /**
      @class QualityReport
      @classdesc A DataPackage represents a hierarchical collection of
      packages, metadata, and data objects, modeling an OAI-ORE RDF graph.
@@ -59,9 +62,8 @@ 

Source: src/js/collections/QualityReport.js

@extends Backbone.Collection @constructor */ - var QualityReport = Backbone.Collection.extend( - /** @lends QualityReport.prototype */{ - + var QualityReport = Backbone.Collection.extend( + /** @lends QualityReport.prototype */ { //The name of this type of collection type: "QualityReport", runStatus: null, @@ -69,8 +71,7 @@

Source: src/js/collections/QualityReport.js

timestamp: null, initialize: function (models, options) { - if (typeof options == "undefined") - var options = {}; + if (typeof options == "undefined") var options = {}; //Set the id or create a new one this.id = options.pid || "urn:uuid:" + uuid.v4(); @@ -87,7 +88,7 @@

Source: src/js/collections/QualityReport.js

*/ model: QualityCheck, - parse: function(response, options) { + parse: function (response, options) { // runStatus can be one of "success", "failure", "queued" this.runStatus = response.runStatus; this.errorDescription = response.errorDescription; @@ -95,32 +96,35 @@

Source: src/js/collections/QualityReport.js

return response.result; }, - fetch: function(options) { + fetch: function (options) { var collectionRef = this; var fetchOptions = {}; - if((typeof options != "undefined")) { + if (typeof options != "undefined") { fetchOptions = _.extend(options, { - url: options.url, - cache: false, - contentType: false, //"multipart/form-data", - processData: false, - type: 'GET', - //headers: { 'Access-Control-Allow-Origin': 'http://localhost:8081' }, - headers: { - 'Accept': 'application/json' - }, - success: function (collection, jqXhr, options) { - //collectionRef.run = data; - collectionRef.trigger("fetchComplete"); - }, - error: function (collection, jqXhr, options) { - console.debug("error fetching quality report."); - collectionRef.fetchResponse = jqXhr; - collectionRef.trigger("fetchError"); - } - }); + url: options.url, + cache: false, + contentType: false, //"multipart/form-data", + processData: false, + type: "GET", + //headers: { 'Access-Control-Allow-Origin': 'http://localhost:8081' }, + headers: { + Accept: "application/json", + }, + success: function (collection, jqXhr, options) { + //collectionRef.run = data; + collectionRef.trigger("fetchComplete"); + }, + error: function (collection, jqXhr, options) { + console.debug("error fetching quality report."); + collectionRef.fetchResponse = jqXhr; + collectionRef.trigger("fetchError"); + }, + }); //fetchOptions = _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); - return Backbone.Collection.prototype.fetch.call(collectionRef, fetchOptions); + return Backbone.Collection.prototype.fetch.call( + collectionRef, + fetchOptions, + ); } }, @@ -132,31 +136,31 @@

Source: src/js/collections/QualityReport.js

var status = result.get("status"); // simple cases // always blue for info and skip - if (check.level == 'INFO') { - color = 'BLUE'; + if (check.level == "INFO") { + color = "BLUE"; return color; } - if (status == 'SKIP') { - color = 'BLUE'; + if (status == "SKIP") { + color = "BLUE"; return color; } // always green for success - if (status == 'SUCCESS') { - color = 'GREEN'; + if (status == "SUCCESS") { + color = "GREEN"; return color; } // handle failures and warnings - if (status == 'FAILURE') { - color = 'RED'; - if (check.level == 'OPTIONAL') { - color = 'ORANGE'; + if (status == "FAILURE") { + color = "RED"; + if (check.level == "OPTIONAL") { + color = "ORANGE"; } } - if (status == 'ERROR') { - color = 'ORANGE'; - if (check.level == 'REQUIRED') { - color = 'RED'; + if (status == "ERROR") { + color = "ORANGE"; + if (check.level == "REQUIRED") { + color = "RED"; } } return color; @@ -185,7 +189,6 @@

Source: src/js/collections/QualityReport.js

groupByType: function (results) { var groupedResults = _.groupBy(results, function (result) { - var check = result.get("check"); var status = result.get("status"); @@ -198,12 +201,12 @@

Source: src/js/collections/QualityReport.js

return "removeMe"; } - var type = "" + var type = ""; // Convert check type to lower case, so that the checks will be // grouped correctly, even if one check type has an incorrect capitalization. - if(check.type != null) { + if (check.type != null) { // Normalize check type by converting entire string to lowercase - type = check.type.toLowerCase() + type = check.type.toLowerCase(); // Now convert to title case type = type.charAt(0).toUpperCase() + type.slice(1); } @@ -215,8 +218,9 @@

Source: src/js/collections/QualityReport.js

delete groupedResults["removeMe"]; return groupedResults; - } - }); + }, + }, + ); return QualityReport; });
diff --git a/docs/docs/src_js_collections_SolrResults.js.html b/docs/docs/src_js_collections_SolrResults.js.html index 4a6d031e4..71bb23fb2 100644 --- a/docs/docs/src_js_collections_SolrResults.js.html +++ b/docs/docs/src_js_collections_SolrResults.js.html @@ -44,10 +44,14 @@

Source: src/js/collections/SolrResults.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'models/SolrHeader', 'models/SolrResult'],
-  function($, _, Backbone, SolrHeader, SolrResult) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/SolrHeader",
+  "models/SolrResult",
+], function ($, _, Backbone, SolrHeader, SolrResult) {
+  "use strict";
 
   /**
    @class SolrResults
@@ -57,179 +61,185 @@ 

Source: src/js/collections/SolrResults.js

@constructor */ var SolrResults = Backbone.Collection.extend( - /** @lends SolrResults.prototype */{ - - // Reference to this collection's model. - model: SolrResult, - - /** - * The name of this type of collection. - * @type {string} - * @default "SolrResults" - * @since 2.25.0 - */ - type: "SolrResults", - - initialize: function(models, options) { - - if( typeof options === "undefined" || !options ){ - var options = {}; - } - - this.docsCache = options.docsCache || null; - this.currentquery = options.query || '*:*'; - this.rows = options.rows || 25; - this.start = options.start || 0; - this.sort = options.sort || 'dateUploaded desc'; - this.facet = options.facet || []; - this.facetCounts = "nothing"; - this.stats = options.stats || false; - this.minYear = options.minYear || 1900; - this.maxYear = options.maxYear || new Date().getFullYear(); - this.queryServiceUrl = options.queryServiceUrl || MetacatUI.appModel.get('queryServiceUrl'); - - if( MetacatUI.appModel.get("defaultSearchFields")?.length ) - this.fields = MetacatUI.appModel.get("defaultSearchFields").join(","); - else - this.fields = options.fields || "id,title"; - - - //If POST queries are disabled in the whole app, don't use POSTs here - if( MetacatUI.appModel.get("disableQueryPOSTs") ){ - this.usePOST = false; - } - //If this collection was initialized with the usePOST option, use POSTs here - else if( options.usePOST ){ - this.usePOST = true; - } - //Otherwise default to using GET - else{ - this.usePOST = false; - } - }, - - url: function() { - //Convert facet keywords to a string - var facetFields = ""; - - this.facet = _.uniq(this.facet); - - for (var i=0; i<this.facet.length; i++){ - facetFields += "&facet.field=" + this.facet[i]; - } - // limit to matches - if (this.facet.length > 0) { - facetFields += "&facet.mincount=1"; // only facets meeting the current search - facetFields += "&facet.limit=-1"; // CAREFUL: -1 means no limit on the number of facets - } - - //Do we need stats? - if (!this.stats){ - var stats = ""; - } - else{ - var stats = "&stats=true"; - for(var i=0; i<this.stats.length; i++){ - stats += "&stats.field=" + this.stats[i]; + /** @lends SolrResults.prototype */ { + // Reference to this collection's model. + model: SolrResult, + + /** + * The name of this type of collection. + * @type {string} + * @default "SolrResults" + * @since 2.25.0 + */ + type: "SolrResults", + + initialize: function (models, options) { + if (typeof options === "undefined" || !options) { + var options = {}; } - } - //create the query url - var endpoint = (this.queryServiceUrl || MetatcatUI.appModel.get("queryServiceUrl")) + "q=" + this.currentquery; + this.docsCache = options.docsCache || null; + this.currentquery = options.query || "*:*"; + this.rows = options.rows || 25; + this.start = options.start || 0; + this.sort = options.sort || "dateUploaded desc"; + this.facet = options.facet || []; + this.facetCounts = "nothing"; + this.stats = options.stats || false; + this.minYear = options.minYear || 1900; + this.maxYear = options.maxYear || new Date().getFullYear(); + this.queryServiceUrl = + options.queryServiceUrl || MetacatUI.appModel.get("queryServiceUrl"); + + if (MetacatUI.appModel.get("defaultSearchFields")?.length) + this.fields = MetacatUI.appModel.get("defaultSearchFields").join(","); + else this.fields = options.fields || "id,title"; + + //If POST queries are disabled in the whole app, don't use POSTs here + if (MetacatUI.appModel.get("disableQueryPOSTs")) { + this.usePOST = false; + } + //If this collection was initialized with the usePOST option, use POSTs here + else if (options.usePOST) { + this.usePOST = true; + } + //Otherwise default to using GET + else { + this.usePOST = false; + } + }, - if(this.fields) - endpoint += "&fl=" + this.fields; - if(this.sort) - endpoint += "&sort=" + this.sort; - if( typeof this.rows == "number" || (typeof this.rows == "string" && this.rows.length)) - endpoint += "&rows=" + this.rows; - if( typeof this.start == "number" || (typeof this.start == "string" && this.start.length)) - endpoint += "&start=" + this.start; - if( this.facet.length > 0 ) - endpoint += "&facet=true&facet.sort=index" + facetFields; + url: function () { + //Convert facet keywords to a string + var facetFields = ""; - endpoint += stats + "&wt=json"; + this.facet = _.uniq(this.facet); - return endpoint; - }, + for (var i = 0; i < this.facet.length; i++) { + facetFields += "&facet.field=" + this.facet[i]; + } + // limit to matches + if (this.facet.length > 0) { + facetFields += "&facet.mincount=1"; // only facets meeting the current search + facetFields += "&facet.limit=-1"; // CAREFUL: -1 means no limit on the number of facets + } - parse: function(solr) { + //Do we need stats? + if (!this.stats) { + var stats = ""; + } else { + var stats = "&stats=true"; + for (var i = 0; i < this.stats.length; i++) { + stats += "&stats.field=" + this.stats[i]; + } + } - //Is this our latest query? If not, use our last set of docs from the latest query - if((decodeURIComponent(this.currentquery).replace(/\+/g, " ") != solr.responseHeader.params.q) && this.docsCache) - return this.docsCache; + //create the query url + var endpoint = + (this.queryServiceUrl || MetatcatUI.appModel.get("queryServiceUrl")) + + "q=" + + this.currentquery; + + if (this.fields) endpoint += "&fl=" + this.fields; + if (this.sort) endpoint += "&sort=" + this.sort; + if ( + typeof this.rows == "number" || + (typeof this.rows == "string" && this.rows.length) + ) + endpoint += "&rows=" + this.rows; + if ( + typeof this.start == "number" || + (typeof this.start == "string" && this.start.length) + ) + endpoint += "&start=" + this.start; + if (this.facet.length > 0) + endpoint += "&facet=true&facet.sort=index" + facetFields; + + endpoint += stats + "&wt=json"; + + return endpoint; + }, + + parse: function (solr) { + //Is this our latest query? If not, use our last set of docs from the latest query + if ( + decodeURIComponent(this.currentquery).replace(/\+/g, " ") != + solr.responseHeader.params.q && + this.docsCache + ) + return this.docsCache; + + if (!solr.response) { + if (solr.error && solr.error.msg) { + console.log("Solr error: " + solr.error.msg); + } + return; + } - if(!solr.response){ - if(solr.error && solr.error.msg){ - console.log("Solr error: " + solr.error.msg); + //Save some stats + this.header = new SolrHeader(solr.responseHeader); + this.header.set({ numFound: solr.response.numFound }); + this.header.set({ start: solr.response.start }); + this.header.set({ rows: solr.responseHeader.params.rows }); + + //Get the facet counts and store them in this model + if (solr.facet_counts) { + this.facetCounts = solr.facet_counts.facet_fields; + } else { + this.facetCounts = "nothing"; } - return - } - - //Save some stats - this.header = new SolrHeader(solr.responseHeader); - this.header.set({"numFound" : solr.response.numFound}); - this.header.set({"start" : solr.response.start}); - this.header.set({"rows" : solr.responseHeader.params.rows}); - - //Get the facet counts and store them in this model - if (solr.facet_counts) { - this.facetCounts = solr.facet_counts.facet_fields; - } else { - this.facetCounts = "nothing"; - } - //Cache this set of results - this.docsCache = solr.response.docs; + //Cache this set of results + this.docsCache = solr.response.docs; - return solr.response.docs; - }, + return solr.response.docs; + }, - /** - * Fetches the next page of results - */ - nextpage: function() { - // Only increment the page if the current page is not the last page - if (this.start + this.rows < this.header.get("numFound")) { - this.start += this.rows; - } - if (this.header != null) { - this.header.set({"start" : this.start}); - } + /** + * Fetches the next page of results + */ + nextpage: function () { + // Only increment the page if the current page is not the last page + if (this.start + this.rows < this.header.get("numFound")) { + this.start += this.rows; + } + if (this.header != null) { + this.header.set({ start: this.start }); + } - this.lastUrl = this.url(); + this.lastUrl = this.url(); - var fetchOptions = this.createFetchOptions(); - this.fetch(fetchOptions); - }, + var fetchOptions = this.createFetchOptions(); + this.fetch(fetchOptions); + }, - /** - * Fetches the previous page of results - */ - prevpage: function() { - this.start -= this.rows; - if (this.start < 0) { - this.start = 0; - } - if (this.header != null) { - this.header.set({"start" : this.start}); - } + /** + * Fetches the previous page of results + */ + prevpage: function () { + this.start -= this.rows; + if (this.start < 0) { + this.start = 0; + } + if (this.header != null) { + this.header.set({ start: this.start }); + } - this.lastUrl = this.url(); + this.lastUrl = this.url(); - var fetchOptions = this.createFetchOptions(); - this.fetch(fetchOptions); - }, + var fetchOptions = this.createFetchOptions(); + this.fetch(fetchOptions); + }, - /** - * Fetches the given page of results - * @param {number} page - */ - toPage: function(page) { - // go to the requested page - var requestedStart = this.rows * page; + /** + * Fetches the given page of results + * @param {number} page + */ + toPage: function (page) { + // go to the requested page + var requestedStart = this.rows * page; - /* + /* if (this.header != null) { if (requestedStart < this.header.get("numFound")) { this.start = requestedStart; @@ -237,229 +247,222 @@

Source: src/js/collections/SolrResults.js

this.header.set({"start" : this.start}); }*/ - this.start = requestedStart; - - this.lastUrl = this.url(); - - var fetchOptions = this.createFetchOptions(); - this.fetch(fetchOptions); - }, - - setrows: function(numrows) { - this.rows = numrows; - }, - - query: function(newquery) { - - if(typeof newquery != "undefined" && this.currentquery != newquery){ - this.currentquery = newquery; - this.start = 0; - } - - this.lastUrl = this.url(); - - var fetchOptions = this.createFetchOptions(); - this.fetch(fetchOptions); - }, - - setQuery: function(newquery) { - if (this.currentquery != newquery) { - this.currentquery = newquery; - this.start = 0; - this.lastQuery = newquery; - } - }, - - /** - * Returns the last query that was fetched. - * @returns {string} - */ - getLastQuery: function(){ - return this.lastQuery; - }, - - setfields: function(newfields) { - this.fields = newfields; - }, - - setSort: function(newsort) { - this.sort = newsort; - this.trigger("change:sort"); - }, - - setFacet: function (fields) { - if (!Array.isArray(fields)) { - fields = [fields]; - } - this.facet = fields; - this.trigger("change:facet"); - }, - - setStats: function(fields){ - this.stats = fields; - }, - - createFetchOptions: function(){ - var options = { - start : this.start, - reset: true - } - - let usePOST = this.usePOST || (this.currentquery.length > 1500 && !MetacatUI.appModel.get("disableQueryPOSTs")); + this.start = requestedStart; - if( usePOST ){ - options.type = "POST"; + this.lastUrl = this.url(); - var queryData = new FormData(); - queryData.append("q", decodeURIComponent(this.currentquery)); - queryData.append("rows", this.rows); - queryData.append("sort", this.sort.replace("+", " ")); - queryData.append("fl", this.fields); - queryData.append("start", this.start); - queryData.append("wt", "json"); + var fetchOptions = this.createFetchOptions(); + this.fetch(fetchOptions); + }, - //Add the facet fields to the FormData - if( this.facet.length ){ + setrows: function (numrows) { + this.rows = numrows; + }, - queryData.append("facet", "true"); + query: function (newquery) { + if (typeof newquery != "undefined" && this.currentquery != newquery) { + this.currentquery = newquery; + this.start = 0; + } - for (var i=0; i<this.facet.length; i++){ - queryData.append("facet.field", this.facet[i]); - } + this.lastUrl = this.url(); - queryData.append("facet.mincount", "1"); - queryData.append("facet.limit", "-1"); - queryData.append("facet.sort", "index"); + var fetchOptions = this.createFetchOptions(); + this.fetch(fetchOptions); + }, + setQuery: function (newquery) { + if (this.currentquery != newquery) { + this.currentquery = newquery; + this.start = 0; + this.lastQuery = newquery; } + }, + + /** + * Returns the last query that was fetched. + * @returns {string} + */ + getLastQuery: function () { + return this.lastQuery; + }, + + setfields: function (newfields) { + this.fields = newfields; + }, + + setSort: function (newsort) { + this.sort = newsort; + this.trigger("change:sort"); + }, + + setFacet: function (fields) { + if (!Array.isArray(fields)) { + fields = [fields]; + } + this.facet = fields; + this.trigger("change:facet"); + }, + + setStats: function (fields) { + this.stats = fields; + }, + + createFetchOptions: function () { + var options = { + start: this.start, + reset: true, + }; + + let usePOST = + this.usePOST || + (this.currentquery.length > 1500 && + !MetacatUI.appModel.get("disableQueryPOSTs")); + + if (usePOST) { + options.type = "POST"; + + var queryData = new FormData(); + queryData.append("q", decodeURIComponent(this.currentquery)); + queryData.append("rows", this.rows); + queryData.append("sort", this.sort.replace("+", " ")); + queryData.append("fl", this.fields); + queryData.append("start", this.start); + queryData.append("wt", "json"); + + //Add the facet fields to the FormData + if (this.facet.length) { + queryData.append("facet", "true"); + + for (var i = 0; i < this.facet.length; i++) { + queryData.append("facet.field", this.facet[i]); + } + + queryData.append("facet.mincount", "1"); + queryData.append("facet.limit", "-1"); + queryData.append("facet.sort", "index"); + } - //Add stats to the FormData - if( this.stats.length ){ - - queryData.append("stats", "true"); + //Add stats to the FormData + if (this.stats.length) { + queryData.append("stats", "true"); - for(var i=0; i<this.stats.length; i++){ - queryData.append("stats.field", this.stats[i]); + for (var i = 0; i < this.stats.length; i++) { + queryData.append("stats.field", this.stats[i]); + } } + options.data = queryData; + options.contentType = false; + options.processData = false; + options.dataType = "json"; + options.url = MetacatUI.appModel.get("queryServiceUrl"); } - options.data = queryData; - options.contentType = false; - options.processData = false; - options.dataType = "json"; - options.url = MetacatUI.appModel.get("queryServiceUrl"); - - } - - return _.extend(options, MetacatUI.appUserModel.createAjaxSettings()); - }, - - /** - * Returns the total number of results that were just fetched, or undefined if nothing has been fetched yet - * @since 2.22.0 - * @returns {number|undefined} - */ - getNumFound: function(){ - return this.header?.get("numFound"); - }, - - /** - * Calculates and returns the total pages of results that was just fetched - * @since 2.22.0 - * @returns {number} - */ - getNumPages: function(){ - let total = this.getNumFound(); - - if(total){ - return Math.ceil(total/this.header.get("rows"))-1; //-1 because our pages are zero-based numbered (where page 0 gets the first n results) - } - else{ - return 0; - } - }, - - /** - * Calculates and returns the current page of results that was just fetched - * @since 2.22.0 - * @returns {number} - */ - getCurrentPage: function(){ - - if(this.header?.get("start") && this.header?.get("rows")){ - return Math.ceil(this.header.get("start")/this.header.get("rows")); - } - else{ - return 0; + return _.extend(options, MetacatUI.appUserModel.createAjaxSettings()); + }, + + /** + * Returns the total number of results that were just fetched, or undefined if nothing has been fetched yet + * @since 2.22.0 + * @returns {number|undefined} + */ + getNumFound: function () { + return this.header?.get("numFound"); + }, + + /** + * Calculates and returns the total pages of results that was just fetched + * @since 2.22.0 + * @returns {number} + */ + getNumPages: function () { + let total = this.getNumFound(); + + if (total) { + return Math.ceil(total / this.header.get("rows")) - 1; //-1 because our pages are zero-based numbered (where page 0 gets the first n results) + } else { + return 0; } - }, - - /** - * Returns the index number of the first search result E.g. the first page of results may be 0-24, where 0 is the start. - * @since 2.22.0 - * @returns {number} - */ - getStart: function(){ - if(this.header){ - return this.header.get("start"); + }, + + /** + * Calculates and returns the current page of results that was just fetched + * @since 2.22.0 + * @returns {number} + */ + getCurrentPage: function () { + if (this.header?.get("start") && this.header?.get("rows")) { + return Math.ceil(this.header.get("start") / this.header.get("rows")); + } else { + return 0; } - else{ - return this.start; + }, + + /** + * Returns the index number of the first search result E.g. the first page of results may be 0-24, where 0 is the start. + * @since 2.22.0 + * @returns {number} + */ + getStart: function () { + if (this.header) { + return this.header.get("start"); + } else { + return this.start; } - }, - - /** - * Calculates the index number of the last search result. E.g. the first page of results may be 0-24, where 24 is the end. - * @since 2.22.0 - * @returns {number} - */ - getEnd: function(){ + }, + + /** + * Calculates the index number of the last search result. E.g. the first page of results may be 0-24, where 24 is the end. + * @since 2.22.0 + * @returns {number} + */ + getEnd: function () { return parseInt(this.getStart()) + parseInt(this.getRows()) - 1; // -1 since it is zero-based numbering - }, - - /** - * Returns the number of search result rows - * @since 2.22.0 - * @returns {number} - */ - getRows: function(){ - if(this.header){ - return this.header.get("rows"); - } - else{ - return this.rows; + }, + + /** + * Returns the number of search result rows + * @since 2.22.0 + * @returns {number} + */ + getRows: function () { + if (this.header) { + return this.header.get("rows"); + } else { + return this.rows; } - }, - - /** - * Gets and returns the URL string that was sent during the last fetch. - * @since 2.22.0 - * @returns {string} - */ - getLastUrl: function(){ + }, + + /** + * Gets and returns the URL string that was sent during the last fetch. + * @since 2.22.0 + * @returns {string} + */ + getLastUrl: function () { return this.lastUrl || ""; - }, - - /** - * Get the list of PIDs for the search results - * @returns {string[]} - The list of PID strings for the search results - * @since 2.25.0 - */ - getPIDs: function () { - return this.pluck("id"); - }, - - /** - * Determines whether the search parameters have changed since the last fetch. Returns true the next URL - * to be sent in a fetch() is different at all from the last url that was fetched. - * @since 2.22.0 - * @returns {boolean} - */ - hasChanged: function(){ + }, + + /** + * Get the list of PIDs for the search results + * @returns {string[]} - The list of PID strings for the search results + * @since 2.25.0 + */ + getPIDs: function () { + return this.pluck("id"); + }, + + /** + * Determines whether the search parameters have changed since the last fetch. Returns true the next URL + * to be sent in a fetch() is different at all from the last url that was fetched. + * @since 2.22.0 + * @returns {boolean} + */ + hasChanged: function () { return this.url() != this.getLastUrl(); - } - }); + }, + }, + ); return SolrResults; }); diff --git a/docs/docs/src_js_collections_Units.js.html b/docs/docs/src_js_collections_Units.js.html index a042b4fef..5fb19ca14 100644 --- a/docs/docs/src_js_collections_Units.js.html +++ b/docs/docs/src_js_collections_Units.js.html @@ -46,56 +46,58 @@

Source: src/js/collections/Units.js

"use strict";
 
-define(["jquery", "underscore", "backbone", "x2js", "models/metadata/eml211/EMLUnit"],
-    function($, _, Backbone, X2JS, EMLUnit) {
-
-    /**
-     * @class Units
-     * @classdesc Units represents the Ecological Metadata Language units list
-     * @classcategory Collections
-     * @extends Backbone.Collection
-     */
-    var Units = Backbone.Collection.extend(
-      /** @lends Units.prototype */{
-
-        model: EMLUnit,
-
-        comparator: function(unit){
-        	return unit.get("_name").charAt(0).toUpperCase() + unit.get("_name").slice(1);
-        },
-
-        /*
-         * The URL of the EML unit Dictionary
-         */
-        url: "https://raw.githubusercontent.com/NCEAS/eml/RELEASE_EML_2_2_0/eml-unitDictionary.xml",
-
-        /* Retrieve the units from the tagged EML Github Repository */
-        fetch: function(options) {
-        	if(typeof options != {})
-        		var options = {};
-
-            var fetchOptions = _.extend({dataType: "text"}, options);
-
-            return Backbone.Model.prototype.fetch.call(this, fetchOptions);
-
-        },
-
-        /* Parse the XML response */
-        parse: function(response) {
-
-            // If the collection is already parsed, just return it
-            if ( typeof response === "object" ) return response;
-
-            // Otherwise, parse it
-            var x2js = new X2JS();
-            var units = x2js.xml_str2json(response);
-
-            return units.unitList.unit;
-        }
-
-    });
-
-    return Units;
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "x2js",
+  "models/metadata/eml211/EMLUnit",
+], function ($, _, Backbone, X2JS, EMLUnit) {
+  /**
+   * @class Units
+   * @classdesc Units represents the Ecological Metadata Language units list
+   * @classcategory Collections
+   * @extends Backbone.Collection
+   */
+  var Units = Backbone.Collection.extend(
+    /** @lends Units.prototype */ {
+      model: EMLUnit,
+
+      comparator: function (unit) {
+        return (
+          unit.get("_name").charAt(0).toUpperCase() + unit.get("_name").slice(1)
+        );
+      },
+
+      /*
+       * The URL of the EML unit Dictionary
+       */
+      url: "https://raw.githubusercontent.com/NCEAS/eml/RELEASE_EML_2_2_0/eml-unitDictionary.xml",
+
+      /* Retrieve the units from the tagged EML Github Repository */
+      fetch: function (options) {
+        if (typeof options != {}) var options = {};
+
+        var fetchOptions = _.extend({ dataType: "text" }, options);
+
+        return Backbone.Model.prototype.fetch.call(this, fetchOptions);
+      },
+
+      /* Parse the XML response */
+      parse: function (response) {
+        // If the collection is already parsed, just return it
+        if (typeof response === "object") return response;
+
+        // Otherwise, parse it
+        var x2js = new X2JS();
+        var units = x2js.xml_str2json(response);
+
+        return units.unitList.unit;
+      },
+    },
+  );
+
+  return Units;
 });
 
diff --git a/docs/docs/src_js_collections_UserGroup.js.html b/docs/docs/src_js_collections_UserGroup.js.html index bc2ec3f7b..4c507b94e 100644 --- a/docs/docs/src_js_collections_UserGroup.js.html +++ b/docs/docs/src_js_collections_UserGroup.js.html @@ -44,280 +44,299 @@

Source: src/js/collections/UserGroup.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'models/UserModel'],
-	function($, _, Backbone, UserModel) {
-	'use strict';
-
-	/**
-	 * @class UserGroup
-	 * @classdesc The collection of Users that represent a DataONE group
-     * @classcategory Collections
-     * @extends Backbone.Collection
-	 */
-	var UserGroup = Backbone.Collection.extend(
-    /** @lends UserGroup.prototype */{
-		// Reference to this collection's model.
-		model: UserModel,
-
-		//Custom attributes of groups
-		groupId: "",
-		name: "",
-		nameAvailable: null,
-
-		url: function(){
-			return MetacatUI.appModel.get("accountsUrl") + encodeURIComponent(this.groupId);
-		},
-
-		comparator: "lastName", //Sort by last name
-
-		initialize: function(models, options) {
-			if((typeof models == "undefined") || !models)
-				var models = [];
-
-			if(typeof options !== "undefined"){
-				//Save our options
-				$.extend(this, options);
-				this.groupId = options.groupId || "";
-				this.name    = options.name    || "";
-				this.pending = (typeof options.pending === "undefined") ? false : options.pending;
-
-				//If raw data is passed, parse it to get a list of users to be added to this group
-				if(options.rawData){
-
-					//Get a list of UserModel attributes to add to this collection
-					var toAdd = this.parse(options.rawData);
-
-					//Create a UserModel for each user
-					_.each(toAdd, function(modelAttributes){
-						//Don't pass the raw data to the UserModel creation because it is redundant-
-						//We already parsed the raw data when we called add() above
-						var rawDataSave = modelAttributes.rawData;
-						modelAttributes.rawData = null;
-
-						//Create the model then add the raw data back
-						var member = new UserModel(modelAttributes);
-						member.set("rawData", rawDataSave);
-
-						models.push(member);
-					});
-				}
-			}
-
-			//Add all our models to this collection
-			this.add(models);
-		},
-
-		/*
-		 * Gets the group from the server. Options object uses the BackboneJS options API
-		 */
-		getGroup: function(options){
-			if(!this.groupId && this.name){
-				this.groupId = "CN=" + this.name + ",DC=dataone,DC=org";
-			}
-
-			this.fetch(options);
-
-			return this;
-		},
-
-		/*
-		 * Fetches the group info from the server. Should not be called directly - use getGroup() instead
-		 */
-		fetch: function (options) {
-	        options = options || { silent: false, reset: false, remove: false };
-	        options.dataType = "xml";
-	        options.error = function(collection, response, options){
-	        	//If this group is not found, then the name is available
-	        	if((response.status == 404) && (response.responseText.indexOf("No Such Object") > -1)){
-	        		collection.nameAvailable = true;
-	        		collection.trigger("nameChecked", collection);
-	        	}
-	        }
-	        return Backbone.Collection.prototype.fetch.call(this, options);
-	    },
-
-	    /*
-	     * Backbone.js override - parses the XML reponse from DataONE and creates a JSON representation that will
-	     * be used to create UserModels
-	     */
-		parse: function(response, options){
-			if(!response) return;
-
-			//This group name is not available/already taken
-			this.nameAvailable = false;
-			this.trigger("nameChecked", this);
-
-			var group = $(response).find("group subject:contains('" + this.groupId + "')").parent("group"),
-				people = $(response).find("person"),
-				collection = this,
-				toAdd = new Array(),
-				existing = this.pluck("username");
-
-			if(!people.length)
-				people = $(group).find("hasMember");
-
-			//Make all existing usernames lowercase for string matching
-			if(existing.length) existing = _.invoke(existing, "toLowerCase");
-
-			this.name = $(group).children("groupName").text();
-
-			_.each(people, function(person){
-
-				//The tag name is "hasMember" if we retrieved info about this group from the group nodes only
-				if(person.tagName == "hasMember"){
-					var username = $(person).text();
-
-					//If this user is already in the group, skip adding it
-					if(_.contains(existing, username.toLowerCase())) return;
-
-					var user = new UserModel({ username: username }),
-						userAttr = user.toJSON();
-
-					toAdd.push(userAttr);
-				}
-				//The tag name is "person" if we retrieved info about this group through the /accounts service, which includes all nodes about all members
-				else{
-					//If this user is not listed as a member of this group, skip it
-					if($(person).children("isMemberOf:contains('" + collection.groupId + "')").length < 1)
-						return;
-
-					//Username of this person
-					var username = $(person).children("subject").text();
-
-					//If this user is already in the group, skip adding it
-					if(_.contains(existing, username.toLowerCase())) return;
-
-					//User attributes - pass the full response for the UserModel to parse
-					var userAttr = new UserModel({username: username}).parseXML(response);
-
-					//Add to collection
-					toAdd.push(userAttr);
-				}
-			});
-
-			return toAdd;
-		},
-
-		/*
-		 * An alternative to Backbone sync
-		 * - will send a POST request to DataONE CNIdentity.createGroup() to create this collection as a new DataONE group
-		 * or
-		 * - will send a PUT request to DataONE CNIdentity.updateGroup() to update this existing DataONE group
-		 *
-		 *  If this group is marked as pending, then the group is created, otherwise it's updated
-		 */
-		save: function(onSuccess, onError){
-			if(this.pending && (this.nameAvailable == false)) return false;
-
-			var memberXML = "",
-				ownerXML = "",
-				collection = this;
-
-			//Create the member and owner XML
-			this.forEach(function(member){
-				//Don't list yourself as an owner or member (implied)
-				if(MetacatUI.appUserModel == member) return;
-
-				var username = member.get("username") ? member.get("username").trim() : null;
-				if(!username) return;
-
-				memberXML += "<hasMember>" + username + "</hasMember>";
-
-				if(collection.isOwner(member))
-					ownerXML += "<rightsHolder>" + username + "</rightsHolder>";
-			});
-
-			//Create the group XML
-			var groupXML =
-				'<?xml version="1.0" encoding="UTF-8"?>'
-				+ '<d1:group xmlns:d1="http://ns.dataone.org/service/types/v1">'
-					+ '<subject>'   + this.groupId + '</subject>'
-					+ '<groupName>' + this.name    + '</groupName>'
-					+ memberXML
-					+ ownerXML
-				+ '</d1:group>';
-
-			var xmlBlob = new Blob([groupXML], {type : 'application/xml'});
-			var formData = new FormData();
-			formData.append("group", xmlBlob, "group");
-
-			// AJAX call to update
-			$.ajax({
-				type: this.pending? "POST" : "PUT",
-				cache: false,
-			    contentType: false,
-			    processData: false,
-				xhrFields: {
-					withCredentials: true
-				},
-				headers: {
-			        "Authorization": "Bearer " + MetacatUI.appUserModel.get("token")
-			    },
-				url: MetacatUI.appModel.get("groupsUrl"),
-				data: formData,
-				success: function(data, textStatus, xhr) {
-					if(typeof onSuccess != "undefined")
-						onSuccess(data);
-
-					collection.pending = false;
-					collection.nameAvailable = null;
-					collection.getGroup();
-				},
-				error: function(xhr, textStatus, error) {
-					if(typeof onError != "undefined")
-						onError(xhr);
-				}
-			});
-
-			return true;
-		},
-
-		/*
-		 * For pending groups only (those in the creation stage)
-		 * Will check if the given name/id is available
-		 */
-		checkName: function(name){
-			//Only check the name for pending groups
-			if(!this.pending) return;
-
-			//Reset the name and ID
-			this.name = name || this.name;
-			this.groupId = null;
-			this.nameAvailable = null;
-
-			//Get group info/check name availablity
-			this.getGroup({ add: false });
-		},
-
-		/*
-		 * Retrieves the UserModels that are rightsHolders of this group
-		 */
-		getOwners: function(){
-			var groupId = this.groupId;
-			return _.filter(this.models, function(user){
-				return _.contains(user.get("isOwnerOf"), groupId);
-			});
-		},
-
-		/*
-		 * Shortcut function - will check if a specified User is an owner of this group
-		 */
-		isOwner: function(model){
-			if(typeof model === "undefined") return false;
-
-			if(this.pending && (model == MetacatUI.appUserModel)) return true;
-
-			var usernames = [];
-			_.each(this.getOwners(), function(user){ usernames.push(user.get("username")); });
-
-			return _.contains(usernames, model.get("username"));
-		}
-
-	});
-
-	return UserGroup;
-
+            
define(["jquery", "underscore", "backbone", "models/UserModel"], function (
+  $,
+  _,
+  Backbone,
+  UserModel,
+) {
+  "use strict";
+
+  /**
+   * @class UserGroup
+   * @classdesc The collection of Users that represent a DataONE group
+   * @classcategory Collections
+   * @extends Backbone.Collection
+   */
+  var UserGroup = Backbone.Collection.extend(
+    /** @lends UserGroup.prototype */ {
+      // Reference to this collection's model.
+      model: UserModel,
+
+      //Custom attributes of groups
+      groupId: "",
+      name: "",
+      nameAvailable: null,
+
+      url: function () {
+        return (
+          MetacatUI.appModel.get("accountsUrl") +
+          encodeURIComponent(this.groupId)
+        );
+      },
+
+      comparator: "lastName", //Sort by last name
+
+      initialize: function (models, options) {
+        if (typeof models == "undefined" || !models) var models = [];
+
+        if (typeof options !== "undefined") {
+          //Save our options
+          $.extend(this, options);
+          this.groupId = options.groupId || "";
+          this.name = options.name || "";
+          this.pending =
+            typeof options.pending === "undefined" ? false : options.pending;
+
+          //If raw data is passed, parse it to get a list of users to be added to this group
+          if (options.rawData) {
+            //Get a list of UserModel attributes to add to this collection
+            var toAdd = this.parse(options.rawData);
+
+            //Create a UserModel for each user
+            _.each(toAdd, function (modelAttributes) {
+              //Don't pass the raw data to the UserModel creation because it is redundant-
+              //We already parsed the raw data when we called add() above
+              var rawDataSave = modelAttributes.rawData;
+              modelAttributes.rawData = null;
+
+              //Create the model then add the raw data back
+              var member = new UserModel(modelAttributes);
+              member.set("rawData", rawDataSave);
+
+              models.push(member);
+            });
+          }
+        }
+
+        //Add all our models to this collection
+        this.add(models);
+      },
+
+      /*
+       * Gets the group from the server. Options object uses the BackboneJS options API
+       */
+      getGroup: function (options) {
+        if (!this.groupId && this.name) {
+          this.groupId = "CN=" + this.name + ",DC=dataone,DC=org";
+        }
+
+        this.fetch(options);
+
+        return this;
+      },
+
+      /*
+       * Fetches the group info from the server. Should not be called directly - use getGroup() instead
+       */
+      fetch: function (options) {
+        options = options || { silent: false, reset: false, remove: false };
+        options.dataType = "xml";
+        options.error = function (collection, response, options) {
+          //If this group is not found, then the name is available
+          if (
+            response.status == 404 &&
+            response.responseText.indexOf("No Such Object") > -1
+          ) {
+            collection.nameAvailable = true;
+            collection.trigger("nameChecked", collection);
+          }
+        };
+        return Backbone.Collection.prototype.fetch.call(this, options);
+      },
+
+      /*
+       * Backbone.js override - parses the XML reponse from DataONE and creates a JSON representation that will
+       * be used to create UserModels
+       */
+      parse: function (response, options) {
+        if (!response) return;
+
+        //This group name is not available/already taken
+        this.nameAvailable = false;
+        this.trigger("nameChecked", this);
+
+        var group = $(response)
+            .find("group subject:contains('" + this.groupId + "')")
+            .parent("group"),
+          people = $(response).find("person"),
+          collection = this,
+          toAdd = new Array(),
+          existing = this.pluck("username");
+
+        if (!people.length) people = $(group).find("hasMember");
+
+        //Make all existing usernames lowercase for string matching
+        if (existing.length) existing = _.invoke(existing, "toLowerCase");
+
+        this.name = $(group).children("groupName").text();
+
+        _.each(people, function (person) {
+          //The tag name is "hasMember" if we retrieved info about this group from the group nodes only
+          if (person.tagName == "hasMember") {
+            var username = $(person).text();
+
+            //If this user is already in the group, skip adding it
+            if (_.contains(existing, username.toLowerCase())) return;
+
+            var user = new UserModel({ username: username }),
+              userAttr = user.toJSON();
+
+            toAdd.push(userAttr);
+          }
+          //The tag name is "person" if we retrieved info about this group through the /accounts service, which includes all nodes about all members
+          else {
+            //If this user is not listed as a member of this group, skip it
+            if (
+              $(person).children(
+                "isMemberOf:contains('" + collection.groupId + "')",
+              ).length < 1
+            )
+              return;
+
+            //Username of this person
+            var username = $(person).children("subject").text();
+
+            //If this user is already in the group, skip adding it
+            if (_.contains(existing, username.toLowerCase())) return;
+
+            //User attributes - pass the full response for the UserModel to parse
+            var userAttr = new UserModel({ username: username }).parseXML(
+              response,
+            );
+
+            //Add to collection
+            toAdd.push(userAttr);
+          }
+        });
+
+        return toAdd;
+      },
+
+      /*
+       * An alternative to Backbone sync
+       * - will send a POST request to DataONE CNIdentity.createGroup() to create this collection as a new DataONE group
+       * or
+       * - will send a PUT request to DataONE CNIdentity.updateGroup() to update this existing DataONE group
+       *
+       *  If this group is marked as pending, then the group is created, otherwise it's updated
+       */
+      save: function (onSuccess, onError) {
+        if (this.pending && this.nameAvailable == false) return false;
+
+        var memberXML = "",
+          ownerXML = "",
+          collection = this;
+
+        //Create the member and owner XML
+        this.forEach(function (member) {
+          //Don't list yourself as an owner or member (implied)
+          if (MetacatUI.appUserModel == member) return;
+
+          var username = member.get("username")
+            ? member.get("username").trim()
+            : null;
+          if (!username) return;
+
+          memberXML += "<hasMember>" + username + "</hasMember>";
+
+          if (collection.isOwner(member))
+            ownerXML += "<rightsHolder>" + username + "</rightsHolder>";
+        });
+
+        //Create the group XML
+        var groupXML =
+          '<?xml version="1.0" encoding="UTF-8"?>' +
+          '<d1:group xmlns:d1="http://ns.dataone.org/service/types/v1">' +
+          "<subject>" +
+          this.groupId +
+          "</subject>" +
+          "<groupName>" +
+          this.name +
+          "</groupName>" +
+          memberXML +
+          ownerXML +
+          "</d1:group>";
+
+        var xmlBlob = new Blob([groupXML], { type: "application/xml" });
+        var formData = new FormData();
+        formData.append("group", xmlBlob, "group");
+
+        // AJAX call to update
+        $.ajax({
+          type: this.pending ? "POST" : "PUT",
+          cache: false,
+          contentType: false,
+          processData: false,
+          xhrFields: {
+            withCredentials: true,
+          },
+          headers: {
+            Authorization: "Bearer " + MetacatUI.appUserModel.get("token"),
+          },
+          url: MetacatUI.appModel.get("groupsUrl"),
+          data: formData,
+          success: function (data, textStatus, xhr) {
+            if (typeof onSuccess != "undefined") onSuccess(data);
+
+            collection.pending = false;
+            collection.nameAvailable = null;
+            collection.getGroup();
+          },
+          error: function (xhr, textStatus, error) {
+            if (typeof onError != "undefined") onError(xhr);
+          },
+        });
+
+        return true;
+      },
+
+      /*
+       * For pending groups only (those in the creation stage)
+       * Will check if the given name/id is available
+       */
+      checkName: function (name) {
+        //Only check the name for pending groups
+        if (!this.pending) return;
+
+        //Reset the name and ID
+        this.name = name || this.name;
+        this.groupId = null;
+        this.nameAvailable = null;
+
+        //Get group info/check name availablity
+        this.getGroup({ add: false });
+      },
+
+      /*
+       * Retrieves the UserModels that are rightsHolders of this group
+       */
+      getOwners: function () {
+        var groupId = this.groupId;
+        return _.filter(this.models, function (user) {
+          return _.contains(user.get("isOwnerOf"), groupId);
+        });
+      },
+
+      /*
+       * Shortcut function - will check if a specified User is an owner of this group
+       */
+      isOwner: function (model) {
+        if (typeof model === "undefined") return false;
+
+        if (this.pending && model == MetacatUI.appUserModel) return true;
+
+        var usernames = [];
+        _.each(this.getOwners(), function (user) {
+          usernames.push(user.get("username"));
+        });
+
+        return _.contains(usernames, model.get("username"));
+      },
+    },
+  );
+
+  return UserGroup;
 });
 
diff --git a/docs/docs/src_js_collections_bookkeeper_Quotas.js.html b/docs/docs/src_js_collections_bookkeeper_Quotas.js.html index 44b2de270..c1c56dcce 100644 --- a/docs/docs/src_js_collections_bookkeeper_Quotas.js.html +++ b/docs/docs/src_js_collections_bookkeeper_Quotas.js.html @@ -46,9 +46,12 @@

Source: src/js/collections/bookkeeper/Quotas.js

"use strict";
 
-define(["jquery", "underscore", "backbone", "models/bookkeeper/Quota"],
-  function($, _, Backbone, Quota) {
-
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/bookkeeper/Quota",
+], function ($, _, Backbone, Quota) {
   /**
    * @class Quotas
    * @classdesc Quotas are limits set for a particular DataONE Product, such as the number
@@ -60,88 +63,82 @@ 

Source: src/js/collections/bookkeeper/Quotas.js

*/ var Quotas = Backbone.Collection.extend( /** @lends Quotas.prototype */ { - - /** - * The class/model that is contained in this collection. - * @type {Backbone.Model} - */ - model: Quota, - - /** - * A list of query parameters that are supported by the Bookkeeper Quotas API. These - * query parameters can be passed to {@link Quotas#fetch} in the `options` object, and they - * will be used during the fetch. - * @type {string[]} - */ - queryParams: ["quotaType", "subscriber"], - - /** - * Constructs a URL string for fetching this collection and returns it - * @param {Object} [options] - * @property {string} options.quotaType The Usage quotaType to fetch - * @property {string} options.subscriber The user or group subject associated with these Quotas - * @returns {string} The URL string - */ - url: function(options){ - - var url = ""; - - //Use the attributes from the options object for the URL, if it is passed to this function - if( typeof options == "object" ){ - - _.each( this.queryParams, function(name){ - if( typeof options[name] !== "undefined"){ - if( url.length == 0 ){ - url += "?"; - } - else{ - url += "&"; + /** + * The class/model that is contained in this collection. + * @type {Backbone.Model} + */ + model: Quota, + + /** + * A list of query parameters that are supported by the Bookkeeper Quotas API. These + * query parameters can be passed to {@link Quotas#fetch} in the `options` object, and they + * will be used during the fetch. + * @type {string[]} + */ + queryParams: ["quotaType", "subscriber"], + + /** + * Constructs a URL string for fetching this collection and returns it + * @param {Object} [options] + * @property {string} options.quotaType The Usage quotaType to fetch + * @property {string} options.subscriber The user or group subject associated with these Quotas + * @returns {string} The URL string + */ + url: function (options) { + var url = ""; + + //Use the attributes from the options object for the URL, if it is passed to this function + if (typeof options == "object") { + _.each(this.queryParams, function (name) { + if (typeof options[name] !== "undefined") { + if (url.length == 0) { + url += "?"; + } else { + url += "&"; + } + + url += name + "=" + encodeURIComponent(options[name]); } - - url += name + "=" + encodeURIComponent(options[name]); - } - }); - - } - - //Prepend the Bookkeeper Usages URL to the url query parameters string - url = MetacatUI.appModel.get("bookkeeperQuotasUrl") + url; - - return url; - + }); + } + + //Prepend the Bookkeeper Usages URL to the url query parameters string + url = MetacatUI.appModel.get("bookkeeperQuotasUrl") + url; + + return url; + }, + + /** + * Fetches a list of Quotas from the DataONE Bookkeeper service, parses them, and + * stores them on this collection. + * @param {Object} [options] + * @property {string} options.quotaType The quotaType to fetch + * @property {string} options.subscriber The user or group subject associated with these Quotas + */ + fetch: function (options) { + var fetchOptions = { + url: this.url(options), + }; + + fetchOptions = Object.assign( + fetchOptions, + MetacatUI.appUserModel.createAjaxSettings(), + ); + + //Call Backbone.Collection.fetch to retrieve the info + return Backbone.Collection.prototype.fetch.call(this, fetchOptions); + }, + + /** + * Parses the fetch() of this collection. Bookkeeper returns JSON already, so there + * isn't much parsing to do. + * @returns {JSON} The collection data in JSON form + */ + parse: function (response) { + return response.quotas; + }, }, - - /** - * Fetches a list of Quotas from the DataONE Bookkeeper service, parses them, and - * stores them on this collection. - * @param {Object} [options] - * @property {string} options.quotaType The quotaType to fetch - * @property {string} options.subscriber The user or group subject associated with these Quotas - */ - fetch: function(options){ - - var fetchOptions = { - url: this.url(options) - } - - fetchOptions = Object.assign(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); - - //Call Backbone.Collection.fetch to retrieve the info - return Backbone.Collection.prototype.fetch.call(this, fetchOptions); - - }, - - /** - * Parses the fetch() of this collection. Bookkeeper returns JSON already, so there - * isn't much parsing to do. - * @returns {JSON} The collection data in JSON form - */ - parse: function(response){ - - return response.quotas; - } - - }); + ); return Quotas; }); diff --git a/docs/docs/src_js_collections_bookkeeper_Usages.js.html b/docs/docs/src_js_collections_bookkeeper_Usages.js.html index 997fd52e3..6b956d640 100644 --- a/docs/docs/src_js_collections_bookkeeper_Usages.js.html +++ b/docs/docs/src_js_collections_bookkeeper_Usages.js.html @@ -46,9 +46,13 @@

Source: src/js/collections/bookkeeper/Usages.js

"use strict";
 
-define(["jquery", "underscore", "backbone", "models/bookkeeper/Usage", "models/bookkeeper/Quota"],
-  function($, _, Backbone, Usage, Quota) {
-
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/bookkeeper/Usage",
+  "models/bookkeeper/Quota",
+], function ($, _, Backbone, Usage, Quota) {
   /**
    * @class Usages
    * @classdesc A Usages collection is a collection of Usage Models which track
@@ -61,128 +65,122 @@ 

Source: src/js/collections/bookkeeper/Usages.js

*/ var Usages = Backbone.Collection.extend( /** @lends Usages.prototype */ { + /** + * The class/model that is contained in this collection. + * @type {Backbone.Model} + */ + model: Usage, + + /** + * A reference to a Quota model that this collection of Usages is associated with. + * @type {Quota} + */ + quota: null, + + /** + * A list of query parameters that are supported by the Bookkeeper Usages API. These + * query parameters can be passed to {@link Usages#fetch} in the `options` object, and they + * will be used during the fetch. + * @type {string[]} + */ + queryParams: ["quotaType", "subscriber"], + + /** + * Constructs a URL string for fetching this collection and returns it + * @param {Object} [options] + * @property {string} options.quotaType The Usage quotaType to fetch + * @property {string} options.subscriber The user or group subject associated with these Usages + * @returns {string} The URL string + */ + url: function (options) { + var url = ""; + + //Use the attributes from the options object for the URL, if it is passed to this function + if (typeof options == "object") { + _.each(this.queryParams, function (name) { + if (typeof options[name] !== "undefined") { + if (url.length == 0) { + url += "?"; + } else { + url += "&"; + } + + url += name + "=" + encodeURIComponent(options[name]); + } + }); + } - /** - * The class/model that is contained in this collection. - * @type {Backbone.Model} - */ - model: Usage, - - /** - * A reference to a Quota model that this collection of Usages is associated with. - * @type {Quota} - */ - quota: null, - - /** - * A list of query parameters that are supported by the Bookkeeper Usages API. These - * query parameters can be passed to {@link Usages#fetch} in the `options` object, and they - * will be used during the fetch. - * @type {string[]} - */ - queryParams: ["quotaType", "subscriber"], - - /** - * Constructs a URL string for fetching this collection and returns it - * @param {Object} [options] - * @property {string} options.quotaType The Usage quotaType to fetch - * @property {string} options.subscriber The user or group subject associated with these Usages - * @returns {string} The URL string - */ - url: function(options){ - - var url = ""; - - //Use the attributes from the options object for the URL, if it is passed to this function - if( typeof options == "object" ){ - - _.each( this.queryParams, function(name){ - if( typeof options[name] !== "undefined"){ - if( url.length == 0 ){ - url += "?"; + //Prepend the Bookkeeper Usages URL to the url query parameters string + url = MetacatUI.appModel.get("bookkeeperUsagesUrl") + url; + + return url; + }, + + /** + * Fetches a list of Usages from the DataONE Bookkeeper service, parses them, and + * stores them on this collection. + * @param {Object} [options] + * @property {string} options.quotaType The Usage quotaType to fetch + * @property {string} options.subscriber The user or group subject associated with these Usages + */ + fetch: function (options) { + var fetchOptions = { + url: this.url(options), + }; + + fetchOptions = Object.assign( + fetchOptions, + MetacatUI.appUserModel.createAjaxSettings(), + ); + + //Call Backbone.Collection.fetch to retrieve the info + return Backbone.Collection.prototype.fetch.call(this, fetchOptions); + }, + + /** + * Parses the fetch() of this collection. Bookkeeper returns JSON already, so there + * isn't much parsing to do. + * @returns {JSON} The collection data in JSON form + */ + parse: function (response) { + return response.usages; + }, + + /** + * Merges another collection of models with this collection by matching instanceId to seriesId/id. + * A reference to the model from the otherCollection is stored in the corresponding Usage model. + * @type {DataPackage|SolrResults} + */ + mergeCollections: function (otherCollection) { + //Iterate over each Usage in this collection + this.each(function (usage) { + //Find the other model that matches this Usage + var match = otherCollection.find(function (otherModel) { + //Make a match on the seriesId + if ( + _.contains(otherModel.get("seriesId"), usage.get("instanceId")) + ) { + return true; } - else{ - url += "&"; + //Make a match on the id + else if ( + _.contains(otherModel.get("id"), usage.get("instanceId")) + ) { + return true; + } else { + return false; } + }); - url += name + "=" + encodeURIComponent(options[name]); + //If a match is found, store a reference in each model + if (match) { + usage.set(match.type, match); + match.set("usageModel", this); } - }); - - } - - //Prepend the Bookkeeper Usages URL to the url query parameters string - url = MetacatUI.appModel.get("bookkeeperUsagesUrl") + url; - - return url; - - }, - - /** - * Fetches a list of Usages from the DataONE Bookkeeper service, parses them, and - * stores them on this collection. - * @param {Object} [options] - * @property {string} options.quotaType The Usage quotaType to fetch - * @property {string} options.subscriber The user or group subject associated with these Usages - */ - fetch: function(options){ - - var fetchOptions = { - url: this.url(options) - } - - fetchOptions = Object.assign(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); - - //Call Backbone.Collection.fetch to retrieve the info - return Backbone.Collection.prototype.fetch.call(this, fetchOptions); - - }, - - /** - * Parses the fetch() of this collection. Bookkeeper returns JSON already, so there - * isn't much parsing to do. - * @returns {JSON} The collection data in JSON form - */ - parse: function(response){ - return response.usages; + }, this); + }, }, - - /** - * Merges another collection of models with this collection by matching instanceId to seriesId/id. - * A reference to the model from the otherCollection is stored in the corresponding Usage model. - * @type {DataPackage|SolrResults} - */ - mergeCollections: function(otherCollection){ - - //Iterate over each Usage in this collection - this.each(function(usage){ - - //Find the other model that matches this Usage - var match = otherCollection.find(function(otherModel){ - //Make a match on the seriesId - if( _.contains(otherModel.get("seriesId"), usage.get("instanceId")) ){ - return true; - } - //Make a match on the id - else if( _.contains(otherModel.get("id"), usage.get("instanceId")) ){ - return true; - } - else{ - return false; - } - }); - - //If a match is found, store a reference in each model - if( match ){ - usage.set(match.type, match); - match.set("usageModel", this); - } - - }, this); - - } - - }); + ); return Usages; }); diff --git a/docs/docs/src_js_collections_maps_AssetCategories.js.html b/docs/docs/src_js_collections_maps_AssetCategories.js.html index 6d648b895..bccbdb5d7 100644 --- a/docs/docs/src_js_collections_maps_AssetCategories.js.html +++ b/docs/docs/src_js_collections_maps_AssetCategories.js.html @@ -51,12 +51,7 @@

Source: src/js/collections/maps/AssetCategories.js

"models/maps/AssetCategory", "models/maps/Map", "collections/maps/MapAssets", -], function ( - Backbone, - AssetCategory, - MapModel, - MapAssets, -) { +], function (Backbone, AssetCategory, MapModel, MapAssets) { /** * @classdesc AssetCategories collection is a group of AssetCategory models - models * that provide the information required to render geo-spatial data in categories, @@ -69,7 +64,6 @@

Source: src/js/collections/maps/AssetCategories.js

*/ const AssetCategories = Backbone.Collection.extend( /** @lends AssetCategories.prototype */ { - /** @inheritdoc */ model: AssetCategory, @@ -81,19 +75,21 @@

Source: src/js/collections/maps/AssetCategories.js

* models */ setMapModel(mapModel) { - this.each(assetCategoryModel => assetCategoryModel.setMapModel(mapModel)); + this.each((assetCategoryModel) => + assetCategoryModel.setMapModel(mapModel), + ); }, - /** + /** * Gets an array of MapAssets, one from each AssetCategory model. * @returns {MapAssets[]} */ getMapAssets() { - return this.map(assetCategory => { + return this.map((assetCategory) => { return assetCategory.get("mapAssets"); }); - } - } + }, + }, ); return AssetCategories; diff --git a/docs/docs/src_js_collections_maps_AssetColors.js.html b/docs/docs/src_js_collections_maps_AssetColors.js.html index 13f21272c..ceacdbefe 100644 --- a/docs/docs/src_js_collections_maps_AssetColors.js.html +++ b/docs/docs/src_js_collections_maps_AssetColors.js.html @@ -44,92 +44,81 @@

Source: src/js/collections/maps/AssetColors.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/AssetColor'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    AssetColor
-  ) {
-
-    /**
-     * @class AssetColors
-     * @classdesc An AssetColors collection represents the colors used to create a color
-     * scale for an asset in a map. The last color in the collection is treated as a
-     * default.
-     * @class AssetColors
-     * @classcategory Collections/Maps
-     * @extends Backbone.Collection
-     * @since 2.18.0
-     * @constructor
-     */
-    var AssetColors = Backbone.Collection.extend(
-      /** @lends AssetColors.prototype */ {
-
-        /**
-        * The class/model that this collection contains.
-        * @type {Backbone.Model}
-        */
-        model: AssetColor,
-
-        /**
-         * Add custom sort functionality such that values are sorted
-         * numerically, but keep the special value key words "min" and "max" at
-         * the beginning or end of the collection, respectively.
-         * @since 2.25.0
-         */
-        comparator: function (color) {
-          let value = color.get('value');
-          if (value === 'min') {
-            return -Infinity;
-          } else if (value === 'max') {
-            return Infinity;
-          } else {
-            return value
-          }
-        },
-
-        /**
-         * Finds the last color model in the collection. If there are no colors in the
-         * collection, returns the default color set in a new Asset Color model.
-         * @return {AssetColor}
-         */
-        getDefaultColor: function () {
-          let defaultColor = this.at(-1)
-          if (!defaultColor) {
-            defaultColor = new AssetColor();
-          }
-          return defaultColor
-        },
-
-        /**
-         * For any attribute that exists in the models in this collection, return an
-         * array of the values for that attribute.
-         * @param {string} attr - The attribute to get the values for.
-         * @return {Array}
-         * @since 2.25.0
-         */
-        getAttr: function(attr) {
-          return this.map(function (model) {
-            return model.get(attr);
-          });
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/AssetColor",
+], function ($, _, Backbone, AssetColor) {
+  /**
+   * @class AssetColors
+   * @classdesc An AssetColors collection represents the colors used to create a color
+   * scale for an asset in a map. The last color in the collection is treated as a
+   * default.
+   * @class AssetColors
+   * @classcategory Collections/Maps
+   * @extends Backbone.Collection
+   * @since 2.18.0
+   * @constructor
+   */
+  var AssetColors = Backbone.Collection.extend(
+    /** @lends AssetColors.prototype */ {
+      /**
+       * The class/model that this collection contains.
+       * @type {Backbone.Model}
+       */
+      model: AssetColor,
+
+      /**
+       * Add custom sort functionality such that values are sorted
+       * numerically, but keep the special value key words "min" and "max" at
+       * the beginning or end of the collection, respectively.
+       * @since 2.25.0
+       */
+      comparator: function (color) {
+        let value = color.get("value");
+        if (value === "min") {
+          return -Infinity;
+        } else if (value === "max") {
+          return Infinity;
+        } else {
+          return value;
         }
-
-      }
-    );
-
-    return AssetColors;
-
-  }
-);
+ }, + + /** + * Finds the last color model in the collection. If there are no colors in the + * collection, returns the default color set in a new Asset Color model. + * @return {AssetColor} + */ + getDefaultColor: function () { + let defaultColor = this.at(-1); + if (!defaultColor) { + defaultColor = new AssetColor(); + } + return defaultColor; + }, + + /** + * For any attribute that exists in the models in this collection, return an + * array of the values for that attribute. + * @param {string} attr - The attribute to get the values for. + * @return {Array} + * @since 2.25.0 + */ + getAttr: function (attr) { + return this.map(function (model) { + return model.get(attr); + }); + }, + }, + ); + + return AssetColors; +}); +
diff --git a/docs/docs/src_js_collections_maps_Features.js.html b/docs/docs/src_js_collections_maps_Features.js.html index 99b14191d..625ec7898 100644 --- a/docs/docs/src_js_collections_maps_Features.js.html +++ b/docs/docs/src_js_collections_maps_Features.js.html @@ -50,7 +50,7 @@

Source: src/js/collections/maps/Features.js

$, _, Backbone, - Feature + Feature, ) { /** * @class Features @@ -119,7 +119,7 @@

Source: src/js/collections/maps/Features.js

} catch (error) { console.log( `Failed to get unique attributes for "${attrName}".`, - error + error, ); } }, @@ -139,7 +139,7 @@

Source: src/js/collections/maps/Features.js

featureObject instanceof Feature ? featureObject.get("featureObject") : featureObject; - return this.findWhere({ 'featureObject': featureObject }) ? true : false; + return this.findWhere({ featureObject: featureObject }) ? true : false; }, /** @@ -153,11 +153,10 @@

Source: src/js/collections/maps/Features.js

containsFeatures: function (featureObjects) { if (!featureObjects || !featureObjects.length) return false; return featureObjects.every((featureObject) => - this.containsFeature(featureObject) + this.containsFeature(featureObject), ); }, - - } + }, ); return Features; diff --git a/docs/docs/src_js_collections_maps_GeoPoints.js.html b/docs/docs/src_js_collections_maps_GeoPoints.js.html index 42125b4b7..3dd7d17da 100644 --- a/docs/docs/src_js_collections_maps_GeoPoints.js.html +++ b/docs/docs/src_js_collections_maps_GeoPoints.js.html @@ -230,7 +230,7 @@

Source: src/js/collections/maps/GeoPoints.js

toCZMLPoints: function () { return this.models.map((model) => { return model.toCZML(); - }) + }); }, /** @@ -369,7 +369,7 @@

Source: src/js/collections/maps/GeoPoints.js

return model.get("mapWidgetCoords"); }); }, - } + }, ); return GeoPoints; diff --git a/docs/docs/src_js_collections_maps_Geohashes.js.html b/docs/docs/src_js_collections_maps_Geohashes.js.html index cf7e7b0c7..814d24acc 100644 --- a/docs/docs/src_js_collections_maps_Geohashes.js.html +++ b/docs/docs/src_js_collections_maps_Geohashes.js.html @@ -129,7 +129,7 @@

Source: src/js/collections/maps/Geohashes.js

if (fix) return p < min ? min : max; throw new Error( `Precision must be a number between ${min} and ${max}` + - ` (inclusive), but got ${p}` + ` (inclusive), but got ${p}`, ); }, @@ -186,7 +186,7 @@

Source: src/js/collections/maps/Geohashes.js

bounds.forEach(function (b) { const c = b.getCoords(); hashStrings = hashStrings.concat( - nGeohash.bboxes(c.south, c.west, c.north, c.east, precision) + nGeohash.bboxes(c.south, c.west, c.north, c.east, precision), ); }); return hashStrings; @@ -218,7 +218,6 @@

Source: src/js/collections/maps/Geohashes.js

return this.models.map((geohash) => geohash.get(attr)); }, - /** * Add geohashes to the collection based on a bounding box. * @param {GeoBoundingBox} bounds - Bounding box with north, south, east, and west @@ -248,7 +247,7 @@

Source: src/js/collections/maps/Geohashes.js

maxGeohashes = Infinity, overwrite = false, minPrecision = this.MIN_PRECISION, - maxPrecision = this.MAX_PRECISION + maxPrecision = this.MAX_PRECISION, ) { let hashStrings = []; if (consolidate) { @@ -256,7 +255,7 @@

Source: src/js/collections/maps/Geohashes.js

bounds, minPrecision, maxPrecision, - maxGeohashes + maxGeohashes, ); } else { const area = bounds.getArea(); @@ -264,7 +263,7 @@

Source: src/js/collections/maps/Geohashes.js

area, maxGeohashes, minPrecision, - maxPrecision + maxPrecision, ); hashStrings = this.getHashStringsForBounds(bounds, precision); } @@ -304,7 +303,7 @@

Source: src/js/collections/maps/Geohashes.js

*/ getGeohashAreas: function ( minPrecision = this.MIN_PRECISION, - maxPrecision = this.MAX_PRECISION + maxPrecision = this.MAX_PRECISION, ) { minPrecision = this.validatePrecision(minPrecision); maxPrecision = this.validatePrecision(maxPrecision); @@ -336,7 +335,7 @@

Source: src/js/collections/maps/Geohashes.js

area, maxGeohashes, absMin = this.MIN_PRECISION, - absMax = this.MAX_PRECISION + absMax = this.MAX_PRECISION, ) { absMin = this.validatePrecision(absMin); absMax = this.validatePrecision(absMax); @@ -361,7 +360,7 @@

Source: src/js/collections/maps/Geohashes.js

console.warn( `The area is too large to cover with fewer than ${maxGeohashes} ` + `geohashes at the min precision level (${absMin}). Returning ` + - `the min precision level, which may result in too many geohashes.` + `the min precision level, which may result in too many geohashes.`, ); } @@ -382,7 +381,7 @@

Source: src/js/collections/maps/Geohashes.js

getMinPrecision: function ( area, absMin = this.MIN_PRECISION, - absMax = this.MAX_PRECISION + absMax = this.MAX_PRECISION, ) { absMin = this.validatePrecision(absMin); absMax = this.validatePrecision(absMax); @@ -422,12 +421,12 @@

Source: src/js/collections/maps/Geohashes.js

bounds, maxGeohashes = Infinity, absMin = this.MIN_PRECISION, - absMax = this.MAX_PRECISION + absMax = this.MAX_PRECISION, ) { - if (!bounds.isValid()){ + if (!bounds.isValid()) { console.warn( `Bounds are invalid: ${JSON.stringify(bounds)}. ` + - `Returning the min and max allowable precision levels.` + `Returning the min and max allowable precision levels.`, ); return [absMin, absMax]; } @@ -463,7 +462,7 @@

Source: src/js/collections/maps/Geohashes.js

bounds, minPrecision = this.MIN_PRECISION, maxPrecision = this.MAX_PRECISION, - maxGeohashes = Infinity + maxGeohashes = Infinity, ) { // Check the inputs if (!bounds.isValid()) return []; @@ -477,7 +476,7 @@

Source: src/js/collections/maps/Geohashes.js

bounds, maxGeohashes, minPrecision, - maxPrecision + maxPrecision, ); // Base32 is the set of characters used to encode geohashes @@ -498,8 +497,7 @@

Source: src/js/collections/maps/Geohashes.js

for (const b of allBounds) { if (bounds.boundsAreFullyContained(n, e, s, w)) { return "inside"; - } else if ( - bounds.boundsAreFullyOutside(n, e, s, w)) { + } else if (bounds.boundsAreFullyOutside(n, e, s, w)) { outside.push(true); } } @@ -573,7 +571,7 @@

Source: src/js/collections/maps/Geohashes.js

if (!bounds || !bounds.isValid()) { console.warn( `Bounds are invalid: ${JSON.stringify(bounds)}. ` + - `Returning an empty Geohashes collection.` + `Returning an empty Geohashes collection.`, ); return new Geohashes(); } @@ -581,7 +579,7 @@

Source: src/js/collections/maps/Geohashes.js

let hashes = []; precisions.forEach((precision) => { hashes = hashes.concat( - this.getHashStringsForBounds(bounds, precision) + this.getHashStringsForBounds(bounds, precision), ); }); const subsetModels = this.filter((geohash) => { @@ -763,7 +761,7 @@

Source: src/js/collections/maps/Geohashes.js

}); return geohash; }, - } + }, ); return Geohashes; diff --git a/docs/docs/src_js_collections_maps_MapAssets.js.html b/docs/docs/src_js_collections_maps_MapAssets.js.html index fd88d20ec..c9e398029 100644 --- a/docs/docs/src_js_collections_maps_MapAssets.js.html +++ b/docs/docs/src_js_collections_maps_MapAssets.js.html @@ -65,7 +65,7 @@

Source: src/js/collections/maps/MapAssets.js

CesiumVectorData, CesiumImagery, CesiumTerrain, - CesiumGeohash + CesiumGeohash, ) { /** * @class MapAssets @@ -101,7 +101,11 @@

Source: src/js/collections/maps/MapAssets.js

model: Cesium3DTileset, }, { - types: ["GeoJsonDataSource", "CzmlDataSource", "CustomDataSource"], + types: [ + "GeoJsonDataSource", + "CzmlDataSource", + "CustomDataSource", + ], model: CesiumVectorData, }, { @@ -165,13 +169,13 @@

Source: src/js/collections/maps/MapAssets.js

otherModel.set("selected", false); }); } - } + }, ); } catch (error) { console.log( "There was an error initializing a MapAssets collection" + ". Error details: " + - error + error, ); } }, @@ -224,7 +228,7 @@

Source: src/js/collections/maps/MapAssets.js

console.log( "Failed to get all of the MapAssets in a MapAssets collection." + " Returning all models in the asset collection." + - e + e, ); return this.models; } @@ -284,7 +288,7 @@

Source: src/js/collections/maps/MapAssets.js

return asset?.getFeatureAttributes(feature); }); }, - } + }, ); return MapAssets; diff --git a/docs/docs/src_js_collections_maps_VectorFilters.js.html b/docs/docs/src_js_collections_maps_VectorFilters.js.html index 7e8d08045..f7b0d004f 100644 --- a/docs/docs/src_js_collections_maps_VectorFilters.js.html +++ b/docs/docs/src_js_collections_maps_VectorFilters.js.html @@ -44,75 +44,65 @@

Source: src/js/collections/maps/VectorFilters.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/VectorFilter'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    VectorFilter
-  ) {
-
-    /**
-     * @class VectorFilters
-     * @classdesc A VectorFilters collection is a set of conditions used to show or hide
-     * features of a vector layer on a map.
-     * @class VectorFilters
-     * @classcategory Collections/Maps
-     * @extends Backbone.Collection
-     * @since 2.18.0
-     * @constructor
-     */
-    var VectorFilters = Backbone.Collection.extend(
-      /** @lends VectorFilters.prototype */ {
-
-        /**
-        * The class/model that this collection contains.
-        * @type {Backbone.Model}
-        */
-        model: VectorFilter,
-
-        /**
-         * This function is used to determine if a feature should be shown or hidden based
-         * on the current filters.
-         * @param {Object} properties The properties of the feature to be filtered.
-         * (See the 'properties' attribute of {@link Feature#defaults}.)
-         * @returns {boolean} Returns true if the feature passes all of the filters.
-         */
-        featureIsVisible: function (properties) {
-          try {
-            if (!properties) {
-              properties = {};
-            }
-            let visible = true;
-            this.each(function (filter) {
-              visible = visible && filter.featureIsVisible(properties);
-            });
-            return visible;
-          }
-          catch (error) {
-            console.log(
-              'There was an error filtering a feature in a VectorFilters collection' +
-              '. Error details: ' + error + '. The feature will be visible.'
-            );
-            return true;
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/VectorFilter",
+], function ($, _, Backbone, VectorFilter) {
+  /**
+   * @class VectorFilters
+   * @classdesc A VectorFilters collection is a set of conditions used to show or hide
+   * features of a vector layer on a map.
+   * @class VectorFilters
+   * @classcategory Collections/Maps
+   * @extends Backbone.Collection
+   * @since 2.18.0
+   * @constructor
+   */
+  var VectorFilters = Backbone.Collection.extend(
+    /** @lends VectorFilters.prototype */ {
+      /**
+       * The class/model that this collection contains.
+       * @type {Backbone.Model}
+       */
+      model: VectorFilter,
+
+      /**
+       * This function is used to determine if a feature should be shown or hidden based
+       * on the current filters.
+       * @param {Object} properties The properties of the feature to be filtered.
+       * (See the 'properties' attribute of {@link Feature#defaults}.)
+       * @returns {boolean} Returns true if the feature passes all of the filters.
+       */
+      featureIsVisible: function (properties) {
+        try {
+          if (!properties) {
+            properties = {};
           }
+          let visible = true;
+          this.each(function (filter) {
+            visible = visible && filter.featureIsVisible(properties);
+          });
+          return visible;
+        } catch (error) {
+          console.log(
+            "There was an error filtering a feature in a VectorFilters collection" +
+              ". Error details: " +
+              error +
+              ". The feature will be visible.",
+          );
+          return true;
         }
+      },
+    },
+  );
 
-      }
-    );
-
-    return VectorFilters;
-
-  }
-);
+ return VectorFilters; +}); +
diff --git a/docs/docs/src_js_collections_maps_viewfinder_ZoomPresets.js.html b/docs/docs/src_js_collections_maps_viewfinder_ZoomPresets.js.html index 5df786d0f..58dab7c1d 100644 --- a/docs/docs/src_js_collections_maps_viewfinder_ZoomPresets.js.html +++ b/docs/docs/src_js_collections_maps_viewfinder_ZoomPresets.js.html @@ -44,76 +44,82 @@

Source: src/js/collections/maps/viewfinder/ZoomPresets.js
-
'use strict';
-
-define(
-  [
-    'underscore',
-    'backbone',
-    'models/maps/viewfinder/ZoomPresetModel',
-  ],
-  function (_, Backbone, ZoomPresetModel) {
-    /**
-     * @class ZoomPresets
-     * @classdesc A ZoomPresets collection is a group of ZoomPresetModel models
-     * that provide a location and list of layers to make visible when the user
-     * selects.
-     * @class ZoomPresets
-     * @classcategory Collections/Maps
-     * @extends Backbone.Collection
-     * @since 2.29.0
-     * @constructor
-     */
-    const ZoomPresets = Backbone.Collection.extend(
+            
"use strict";
+
+define([
+  "underscore",
+  "backbone",
+  "models/maps/viewfinder/ZoomPresetModel",
+], function (_, Backbone, ZoomPresetModel) {
+  /**
+   * @class ZoomPresets
+   * @classdesc A ZoomPresets collection is a group of ZoomPresetModel models
+   * that provide a location and list of layers to make visible when the user
+   * selects.
+   * @class ZoomPresets
+   * @classcategory Collections/Maps
+   * @extends Backbone.Collection
+   * @since 2.29.0
+   * @constructor
+   */
+  const ZoomPresets = Backbone.Collection.extend(
     /** @lends ZoomPresets.prototype */ {
-        /** @inheritdoc */
-        model: ZoomPresetModel,
-
-        /**
-         * @param {Object[]} zoomPresets The raw list of objects that represent
-         * the zoom presets, to be converted into ZoomPresetModels.
-         * @param {MapAsset[]} allLayers All of the layers available for display
-         * in the map.
-         */
-        parse({ zoomPresetObjects, allLayers }) {
-          if (isNonEmptyArray(zoomPresetObjects)) {
-            const zoomPresets = zoomPresetObjects.map(zoomPresetObj => {
-              const enabledLayerIds = [];
-              const enabledLayerLabels = [];
-              for (const layer of allLayers) {
-                if (zoomPresetObj.layerIds?.find(id => id === layer.get('layerId'))) {
-                  enabledLayerIds.push(layer.get('layerId'));
-                  enabledLayerLabels.push(layer.get('label'));
-                }
+      /** @inheritdoc */
+      model: ZoomPresetModel,
+
+      /**
+       * @param {Object[]} zoomPresets The raw list of objects that represent
+       * the zoom presets, to be converted into ZoomPresetModels.
+       * @param {MapAsset[]} allLayers All of the layers available for display
+       * in the map.
+       */
+      parse({ zoomPresetObjects, allLayers }) {
+        if (isNonEmptyArray(zoomPresetObjects)) {
+          const zoomPresets = zoomPresetObjects.map((zoomPresetObj) => {
+            const enabledLayerIds = [];
+            const enabledLayerLabels = [];
+            for (const layer of allLayers) {
+              if (
+                zoomPresetObj.layerIds?.find(
+                  (id) => id === layer.get("layerId"),
+                )
+              ) {
+                enabledLayerIds.push(layer.get("layerId"));
+                enabledLayerLabels.push(layer.get("label"));
               }
+            }
 
-              return new ZoomPresetModel({
+            return new ZoomPresetModel(
+              {
                 description: zoomPresetObj.description,
                 enabledLayerLabels,
                 enabledLayerIds,
                 position: {
                   latitude: zoomPresetObj.latitude,
                   longitude: zoomPresetObj.longitude,
-                  height: zoomPresetObj.height
+                  height: zoomPresetObj.height,
                 },
                 title: zoomPresetObj.title,
-              }, { parse: true });
-            });
-
-            return zoomPresets;
-          }
-
-          return [];
-        },
-      }
-    );
-
-    function isNonEmptyArray(a) {
-      return a && a.length && Array.isArray(a);
-    }
-
-    return ZoomPresets;
-  });
+ }, + { parse: true }, + ); + }); + + return zoomPresets; + } + + return []; + }, + }, + ); + + function isNonEmptyArray(a) { + return a && a.length && Array.isArray(a); + } + + return ZoomPresets; +}); +
diff --git a/docs/docs/src_js_collections_metadata_eml_EMLAnnotations.js.html b/docs/docs/src_js_collections_metadata_eml_EMLAnnotations.js.html index b13bf5d2c..0d741fe70 100644 --- a/docs/docs/src_js_collections_metadata_eml_EMLAnnotations.js.html +++ b/docs/docs/src_js_collections_metadata_eml_EMLAnnotations.js.html @@ -46,8 +46,12 @@

Source: src/js/collections/metadata/eml/EMLAnnotations.js
"use strict";
 
-define(["jquery", "underscore", "backbone", "models/metadata/eml211/EMLAnnotation"],
-function($, _, Backbone, EMLAnnotation){
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/metadata/eml211/EMLAnnotation",
+], function ($, _, Backbone, EMLAnnotation) {
   /**
    * @class EMLAnnotations
    * @classdesc A collection of EMLAnnotations.
@@ -58,69 +62,63 @@ 

Source: src/js/collections/metadata/eml/EMLAnnotations.js var EMLAnnotations = Backbone.Collection.extend( /** @lends EMLAnnotations.prototype */ { - /** - * The reference to the model class that this collection is made of. - * @type EMLAnnotation - * @since 2.19.0 - */ + * The reference to the model class that this collection is made of. + * @type EMLAnnotation + * @since 2.19.0 + */ model: EMLAnnotation, /** - * Checks if this collection already has an annotation for the same property URI. - * @param {EMLAnnotation} annotation The EMLAnnotation to compare against the annotations already in this collection. - * @returns {Boolean} Returns true is this collection already has an annotation for this property. - * @since 2.19.0 - */ - hasDuplicateOf: function(annotation){ - - try{ - + * Checks if this collection already has an annotation for the same property URI. + * @param {EMLAnnotation} annotation The EMLAnnotation to compare against the annotations already in this collection. + * @returns {Boolean} Returns true is this collection already has an annotation for this property. + * @since 2.19.0 + */ + hasDuplicateOf: function (annotation) { + try { //If there is at least one model in this collection and there is a propertyURI set on the given model, - if( this.length && annotation.get("propertyURI") ){ + if (this.length && annotation.get("propertyURI")) { //Return whether or not there is a duplicate let properties = this.pluck("propertyURI"); return properties.includes(annotation.get("propertyURI")); } //If this collection is empty or the propertyURI is falsey, return false - else{ + else { return false; } - - } - catch(e){ + } catch (e) { console.error("Could not check for a duplicate annotation: ", e); return false; } - }, /** - * Removes the EMLAnnotation from this collection that has the same propertyURI as the given annotation. - * Then adds the given annotation to the collection. If no duplicate is found, the given annotation is still added - * to the collection. - * @param {EMLAnnotation} annotation - * @since 2.19.0 - */ - replaceDuplicateWith: function(annotation){ - - try{ - - if( this.length && annotation.get("propertyURI") ){ - let duplicates = this.findWhere({ "propertyURI": annotation.get("propertyURI") }); + * Removes the EMLAnnotation from this collection that has the same propertyURI as the given annotation. + * Then adds the given annotation to the collection. If no duplicate is found, the given annotation is still added + * to the collection. + * @param {EMLAnnotation} annotation + * @since 2.19.0 + */ + replaceDuplicateWith: function (annotation) { + try { + if (this.length && annotation.get("propertyURI")) { + let duplicates = this.findWhere({ + propertyURI: annotation.get("propertyURI"), + }); this.remove(duplicates); } this.add(annotation); - - } - catch(e){ - console.error("Could not replace the EMLAnnotation in the collection: ", e); + } catch (e) { + console.error( + "Could not replace the EMLAnnotation in the collection: ", + e, + ); } - - } - - }); + }, + }, + ); return EMLAnnotations; }); diff --git a/docs/docs/src_js_collections_metadata_eml_EMLMissingValueCodes.js.html b/docs/docs/src_js_collections_metadata_eml_EMLMissingValueCodes.js.html index 336e703b7..de7997790 100644 --- a/docs/docs/src_js_collections_metadata_eml_EMLMissingValueCodes.js.html +++ b/docs/docs/src_js_collections_metadata_eml_EMLMissingValueCodes.js.html @@ -48,7 +48,7 @@

Source: src/js/collections/metadata/eml/EMLMissingValueCo define(["backbone", "models/metadata/eml211/EMLMissingValueCode"], function ( Backbone, - EMLMissingValueCode + EMLMissingValueCode, ) { /** * @class EMLMissingValueCodes @@ -125,7 +125,7 @@

Source: src/js/collections/metadata/eml/EMLMissingValueCo // For now, if there is at least one error, just return the first one return errors.length ? errors[0] : null; }, - } + }, ); return EMLMissingValueCodes; diff --git a/docs/docs/src_js_collections_queryFields_QueryFields.js.html b/docs/docs/src_js_collections_queryFields_QueryFields.js.html index 2b3f0ef65..f83ff67ab 100644 --- a/docs/docs/src_js_collections_queryFields_QueryFields.js.html +++ b/docs/docs/src_js_collections_queryFields_QueryFields.js.html @@ -44,161 +44,175 @@

Source: src/js/collections/queryFields/QueryFields.js

-
/* global define */
-define(
-  ["jquery", "underscore", "backbone", "x2js", "models/queryFields/QueryField"],
-  function($, _, Backbone, X2JS, QueryField) {
-    "use strict";
-
-    /**
-     * @class QueryFields
-     * @classdesc The collection of queryable fields supported by the the
-     * DataONE Solr index, as provided by the DataONE API
-     * CNRead.getQueryEngineDescription() function. For more information, see:
-     * https://indexer-documentation.readthedocs.io/en/latest/generated/solr_schema.html
-     * https://dataone-architecture-documentation.readthedocs.io/en/latest/design/SearchMetadata.html
-     * @classcategory Collections/QueryFields
-     * @name QueryFields
-     * @extends Backbone.Collection
-     * @since 2.14.0
-     * @constructor
-     */
-    var QueryFields = Backbone.Collection.extend(
-      /** @lends QueryFields.prototype */
-      {
-
-        /**
-         * The type of Backbone model that this collection comprises
-         */
-        model: QueryField,
-
-        /**
-         * initialize - Creates a new QueryFields collection
-         */
-        initialize: function(models, options) {
-          try {
-            if (typeof options === "undefined") {
-              var options = {};
-            }
-          } catch (e) {
-            console.log("Failed to initialize a Query Fields collection, error message: " + e);
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "x2js",
+  "models/queryFields/QueryField",
+], function ($, _, Backbone, X2JS, QueryField) {
+  "use strict";
+
+  /**
+   * @class QueryFields
+   * @classdesc The collection of queryable fields supported by the the
+   * DataONE Solr index, as provided by the DataONE API
+   * CNRead.getQueryEngineDescription() function. For more information, see:
+   * https://indexer-documentation.readthedocs.io/en/latest/generated/solr_schema.html
+   * https://dataone-architecture-documentation.readthedocs.io/en/latest/design/SearchMetadata.html
+   * @classcategory Collections/QueryFields
+   * @name QueryFields
+   * @extends Backbone.Collection
+   * @since 2.14.0
+   * @constructor
+   */
+  var QueryFields = Backbone.Collection.extend(
+    /** @lends QueryFields.prototype */
+    {
+      /**
+       * The type of Backbone model that this collection comprises
+       */
+      model: QueryField,
+
+      /**
+       * initialize - Creates a new QueryFields collection
+       */
+      initialize: function (models, options) {
+        try {
+          if (typeof options === "undefined") {
+            var options = {};
           }
-        },
-
-        /**
-         * comparator - A sortBy function that returns the order of each Query
-         * Filter model based on its position in the categoriesMap object.
-         *
-         * @param  {QueryFilter} model The individual Query Filter model
-         * @return {number}      A numeric value by which the model should be ordered relative to others.
-         */
-        comparator: function(model){
-          try {
-            var categoriesMap = model.categoriesMap();
-            var order = _(categoriesMap)
-              .chain()
-              .pluck("queryFields")
-              .flatten()
-              .value();
-            return order.indexOf(model.get("name"));
-          } catch (e) {
-            console.log("Failed to sort the Query Fields Collection, error message: " + e);
-            return 0
+        } catch (e) {
+          console.log(
+            "Failed to initialize a Query Fields collection, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * comparator - A sortBy function that returns the order of each Query
+       * Filter model based on its position in the categoriesMap object.
+       *
+       * @param  {QueryFilter} model The individual Query Filter model
+       * @return {number}      A numeric value by which the model should be ordered relative to others.
+       */
+      comparator: function (model) {
+        try {
+          var categoriesMap = model.categoriesMap();
+          var order = _(categoriesMap)
+            .chain()
+            .pluck("queryFields")
+            .flatten()
+            .value();
+          return order.indexOf(model.get("name"));
+        } catch (e) {
+          console.log(
+            "Failed to sort the Query Fields Collection, error message: " + e,
+          );
+          return 0;
+        }
+      },
+
+      /**
+       * The constructed URL of the collection
+       *
+       * @returns {string} - The URL to use during fetch
+       */
+      url: function () {
+        try {
+          return MetacatUI.appModel.get("queryServiceUrl").replace(/\/\?$/, "");
+        } catch (e) {
+          return "https://cn.dataone.org/cn/v2/query/solr";
+        }
+      },
+
+      /**
+       * Retrieve the fields from the Coordinating Node
+       * @extends Backbone.Collection#fetch
+       */
+      fetch: function (options) {
+        try {
+          var fetchOptions = _.extend({ dataType: "text" }, options);
+          return Backbone.Model.prototype.fetch.call(this, fetchOptions);
+        } catch (e) {
+          console.log("Failed to fetch Query Fields, error message: " + e);
+        }
+      },
+
+      /**
+       * parse - Parse the XML response from the CN
+       *
+       * @param  {string} response The queryEngineDescription XML as a string
+       * @return {Array}  the Array of Query Field attributes to be added to the collection.
+       */
+      parse: function (response) {
+        try {
+          // If the collection is already parsed, just return it
+          if (typeof response === "object") {
+            return response;
           }
-        },
-
-        /**
-         * The constructed URL of the collection
-         *
-         * @returns {string} - The URL to use during fetch
-         */
-        url: function() {
-          try {
-            return MetacatUI.appModel.get("queryServiceUrl").replace(/\/\?$/, "");
-          } catch (e) {
-            return "https://cn.dataone.org/cn/v2/query/solr"
+          var x2js = new X2JS();
+          var responseJSON = x2js.xml_str2json(response);
+          if (responseJSON && responseJSON.queryEngineDescription) {
+            return responseJSON.queryEngineDescription.queryField;
           }
-        },
-
-        /**
-        * Retrieve the fields from the Coordinating Node
-        * @extends Backbone.Collection#fetch
-        */
-        fetch: function(options) {
-          try {
-            var fetchOptions = _.extend({dataType: "text"}, options);
-            return Backbone.Model.prototype.fetch.call(this, fetchOptions);
-          } catch (e) {
-            console.log("Failed to fetch Query Fields, error message: " + e);
+        } catch (e) {
+          console.log(
+            "Failed to parse Query Fields response, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * getRequiredFilterType - Based on an array of query (Solr) field names, get the
+       * type of filter model to use with these fields. For example, if the fields are
+       * type text, use a regular filter model. If the fields are tdate, use a
+       * dateFilter. If the field types are mixed, then returns the filterType default
+       * value in QueryField models.
+       *
+       * @param  {string[]} fields The list of Query Field names
+       * @return {string} The nodeName of the filter model to use (one of the four types
+       * of fields that are set in {@link QueryField#filterTypesMap})
+       */
+      getRequiredFilterType: function (fields) {
+        try {
+          var types = [],
+            // When fields is empty or are different types
+            defaultFilterType =
+              MetacatUI.queryFields.models[0].defaults().filterType;
+
+          if (!fields || fields.length === 0 || fields[0] === "") {
+            return defaultFilterType;
           }
-        },
-
-        /**
-         * parse - Parse the XML response from the CN
-         *
-         * @param  {string} response The queryEngineDescription XML as a string
-         * @return {Array}  the Array of Query Field attributes to be added to the collection.
-         */
-        parse: function(response) {
-          try {
-            // If the collection is already parsed, just return it
-            if ( typeof response === "object" ){
-              return response;
-            }
-            var x2js = new X2JS();
-            var responseJSON = x2js.xml_str2json(response);
-            if(responseJSON && responseJSON.queryEngineDescription){
-              return responseJSON.queryEngineDescription.queryField;
-            }
-          } catch (e) {
-            console.log("Failed to parse Query Fields response, error message: " + e);
-          }
-        },
-
-        /**
-         * getRequiredFilterType - Based on an array of query (Solr) field names, get the
-         * type of filter model to use with these fields. For example, if the fields are
-         * type text, use a regular filter model. If the fields are tdate, use a
-         * dateFilter. If the field types are mixed, then returns the filterType default
-         * value in QueryField models.
-         *
-         * @param  {string[]} fields The list of Query Field names
-         * @return {string} The nodeName of the filter model to use (one of the four types
-         * of fields that are set in {@link QueryField#filterTypesMap})
-         */
-         getRequiredFilterType: function (fields) {
-          try {
-            var types = [],
-              // When fields is empty or are different types
-              defaultFilterType = MetacatUI.queryFields.models[0].defaults().filterType;
-
-            if (!fields || fields.length === 0 || fields[0] === "") {
-              return defaultFilterType
-            }
-
-            fields.forEach((newField, i) => {
-              var fieldModel = MetacatUI.queryFields.findWhere({ name: newField });
-                types.push(fieldModel.get("filterType"));
-            });
 
-            // Test of all the fields are of the same type
-            var allEqual = types.every((val, i, arr) => val === arr[0]);
+          fields.forEach((newField, i) => {
+            var fieldModel = MetacatUI.queryFields.findWhere({
+              name: newField,
+            });
+            types.push(fieldModel.get("filterType"));
+          });
 
-            if (allEqual) {
-              return types[0]
-            } else {
-              return defaultFilterType
-            }
+          // Test of all the fields are of the same type
+          var allEqual = types.every((val, i, arr) => val === arr[0]);
 
-          } catch (e) {
-            console.log("Failed to detect the required filter type in a Query Fields" +
-              " Collection, error message: " + e);
+          if (allEqual) {
+            return types[0];
+          } else {
+            return defaultFilterType;
           }
-        },
-
-      });
-    return QueryFields;
-  });
+        } catch (e) {
+          console.log(
+            "Failed to detect the required filter type in a Query Fields" +
+              " Collection, error message: " +
+              e,
+          );
+        }
+      },
+    },
+  );
+  return QueryFields;
+});
 

diff --git a/docs/docs/src_js_common_IconUtilities.js.html b/docs/docs/src_js_common_IconUtilities.js.html index 13e10495d..24c526335 100644 --- a/docs/docs/src_js_common_IconUtilities.js.html +++ b/docs/docs/src_js_common_IconUtilities.js.html @@ -44,28 +44,25 @@

Source: src/js/common/IconUtilities.js

-
/*global define */
-define([
+            
define([
   "models/portals/PortalImage",
   "showdown",
-  "showdownXssFilter"
-],
-	function(PortalImage, showdown, showdownXss) {
-	'use strict';
+  "showdownXssFilter",
+], function (PortalImage, showdown, showdownXss) {
+  "use strict";
 
   // The start of a base64 encoded SVG string
-  const B64_START = 'data:image/svg+xml;base64,';
-
-	/**
-  * @namespace IconUtilities
-  * @description A generic utility object that contains functions used throughout
-  * MetacatUI to perform useful functions related to icons, but not used to store or
-  * manipulate any state about the application.
-  * @type {object}
-  * @since 2.28.0
-  */
-	const IconUtilities = /** @lends IconUtilities.prototype */ {
-
+  const B64_START = "data:image/svg+xml;base64,";
+
+  /**
+   * @namespace IconUtilities
+   * @description A generic utility object that contains functions used throughout
+   * MetacatUI to perform useful functions related to icons, but not used to store or
+   * manipulate any state about the application.
+   * @type {object}
+   * @since 2.28.0
+   */
+  const IconUtilities = /** @lends IconUtilities.prototype */ {
     /**
      * Simple test to see if a string is an SVG
      * @param {string} str The string to check
@@ -90,8 +87,8 @@ 

Source: src/js/common/IconUtilities.js

}).get("imageURL"); return fetch(imageURL) - .then(response => response.text()) - .then(data => { + .then((response) => response.text()) + .then((data) => { if (this.isSVG(data)) { return data; } @@ -127,20 +124,26 @@

Source: src/js/common/IconUtilities.js

* @returns {SVGElement|null} - The modified SVG element or null if an error occurs. * @since 2.29.0 */ - formatSvgForCesiumBillboard(svgString, strokeWidth = 0, strokeColor = "white") { + formatSvgForCesiumBillboard( + svgString, + strokeWidth = 0, + strokeColor = "white", + ) { const svgElement = this.parseSvg(svgString); if (!svgElement) { - console.error("No SVG element found in the SVG string or failed to parse."); + console.error( + "No SVG element found in the SVG string or failed to parse.", + ); return null; } - + this.removeCommentNodes(svgElement); this.setStrokeProperties(svgElement, strokeWidth, strokeColor); this.adjustViewBox(svgElement, strokeWidth); - + return svgElement; }, - + /** * Parses an SVG string and returns the SVG element. * @param {string} svgString - The SVG markup as a string. @@ -153,18 +156,21 @@

Source: src/js/common/IconUtilities.js

const svgElement = doc.querySelector("svg"); return svgElement; }, - + /** * Removes comment nodes from an SVG element. * @param {SVGElement} svgElement - The SVG element. * @since 2.29.0 */ removeCommentNodes(svgElement) { - while (svgElement.firstChild && svgElement.firstChild.nodeType === Node.COMMENT_NODE) { + while ( + svgElement.firstChild && + svgElement.firstChild.nodeType === Node.COMMENT_NODE + ) { svgElement.removeChild(svgElement.firstChild); } }, - + /** * Sets stroke properties on an SVG element. * @param {SVGElement} svgElement - The SVG element. @@ -176,7 +182,7 @@

Source: src/js/common/IconUtilities.js

svgElement.setAttribute("stroke-width", strokeWidth); svgElement.setAttribute("stroke", strokeColor); }, - + /** * Adjusts the viewBox of an SVG element to accommodate a stroke width. * @param {SVGElement} svgElement - The SVG element. @@ -191,9 +197,14 @@

Source: src/js/common/IconUtilities.js

const newY = y - strokeWidth; const newWidth = width + 2 * strokeWidth; const newHeight = height + 2 * strokeWidth; - svgElement.setAttribute("viewBox", `${newX} ${newY} ${newWidth} ${newHeight}`); + svgElement.setAttribute( + "viewBox", + `${newX} ${newY} ${newWidth} ${newHeight}`, + ); } else { - console.warn("SVG element does not have a 'viewBox' attribute; viewBox adjustment skipped."); + console.warn( + "SVG element does not have a 'viewBox' attribute; viewBox adjustment skipped.", + ); } }, @@ -207,9 +218,8 @@

Source: src/js/common/IconUtilities.js

svgToBase64(svgElement) { const base64 = btoa(svgElement.outerHTML); return B64_START + base64; - } - - } + }, + }; return IconUtilities; }); diff --git a/docs/docs/src_js_common_Utilities.js.html b/docs/docs/src_js_common_Utilities.js.html index cfd261e6f..28e95e54f 100644 --- a/docs/docs/src_js_common_Utilities.js.html +++ b/docs/docs/src_js_common_Utilities.js.html @@ -44,55 +44,53 @@

Source: src/js/common/Utilities.js

-
/*global define */
-define(['jquery', 'underscore'],
-	function($, _) {
-	'use strict';
-
-	/**
-  * @namespace Utilities
-  * @description A generic utility object that contains functions used throughout MetacatUI to perform useful functions,
-  * but not used to store or manipulate any state about the application.
-  * @type {object}
-  * @since 2.14.0
-  */
-	var Utilities = /** @lends Utilities.prototype */ {
-
+            
define(["jquery", "underscore"], function ($, _) {
+  "use strict";
+
+  /**
+   * @namespace Utilities
+   * @description A generic utility object that contains functions used throughout MetacatUI to perform useful functions,
+   * but not used to store or manipulate any state about the application.
+   * @type {object}
+   * @since 2.14.0
+   */
+  var Utilities = /** @lends Utilities.prototype */ {
     /**
-    * HTML-encodes the given string so it can be inserted into an HTML page without running
-    * any embedded Javascript.
-    * @param {string} s
-    * @returns {string}
-    */
-    encodeHTML: function(s) {
-
-      try{
-        if( !s || typeof s !== "string" ){
+     * HTML-encodes the given string so it can be inserted into an HTML page without running
+     * any embedded Javascript.
+     * @param {string} s
+     * @returns {string}
+     */
+    encodeHTML: function (s) {
+      try {
+        if (!s || typeof s !== "string") {
           return "";
         }
 
-        return s.replace(/&/g, '&amp;')
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;')
-                .replace(/'/g, "&apos;")
-                .replace(/\//g, "/")
-                .replace(/"/g, '&quot;');
-      }
-      catch(e){
+        return s
+          .replace(/&/g, "&amp;")
+          .replace(/</g, "&lt;")
+          .replace(/>/g, "&gt;")
+          .replace(/'/g, "&apos;")
+          .replace(/\//g, "/")
+          .replace(/"/g, "&quot;");
+      } catch (e) {
         console.error("Could not encode HTML: ", e);
         return "";
       }
     },
 
     /**
-    * Validates that the given string is a valid DOI
-    * @param {string} identifier
-    * @returns {boolean}
-    * @since 2.15.0
-    */
-    isValidDOI: function(identifier) {
+     * Validates that the given string is a valid DOI
+     * @param {string} identifier
+     * @returns {boolean}
+     * @since 2.15.0
+     */
+    isValidDOI: function (identifier) {
       // generate doi regex
-      var doiRGEX = new RegExp(/^\s*(http:\/\/|https:\/\/)?(doi.org\/|dx.doi.org\/)?(doi: ?|DOI: ?)?(10\.\d{4,}(\.\d)*)\/(\w+).*$/ig)
+      var doiRGEX = new RegExp(
+        /^\s*(http:\/\/|https:\/\/)?(doi.org\/|dx.doi.org\/)?(doi: ?|DOI: ?)?(10\.\d{4,}(\.\d)*)\/(\w+).*$/gi,
+      );
 
       return doiRGEX.test(identifier);
     },
@@ -154,19 +152,18 @@ 

Source: src/js/common/Utilities.js

var names = header_line.split(","); // Remove surrounding parens and double-quotes - names = names.map(function(name) { + names = names.map(function (name) { return name.replaceAll(/^["']|["']$/gm, ""); }); // Filter out zero-length values (headers like a,b,c,,,,,) - names = names.filter(function(name) { + names = names.filter(function (name) { return name.length > 0; }); return names; - } - - } + }, + }; return Utilities; }); diff --git a/docs/docs/src_js_models_AccessRule.js.html b/docs/docs/src_js_models_AccessRule.js.html index 918512bfd..f9f4d8556 100644 --- a/docs/docs/src_js_models_AccessRule.js.html +++ b/docs/docs/src_js_models_AccessRule.js.html @@ -44,10 +44,8 @@

Source: src/js/models/AccessRule.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone'],
-	function($, _, Backbone) {
-	'use strict';
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  "use strict";
 
   /**
    * @class AccessRule
@@ -55,243 +53,253 @@ 

Source: src/js/models/AccessRule.js

* @classcategory Models * @extends Backbone.Model */ - var AccessRule = Backbone.Model.extend( + var AccessRule = Backbone.Model.extend( /** @lends AccessRule */ { - - defaults: function(){ - return{ - subject: null, - read: null, - write: null, - changePermission: null, - name: null, - dataONEObject: null - } - }, - - initialize: function(){ - - }, - - /** - * Translates the access rule XML DOM into a JSON object to be set on the model. - * @param {Element} accessRuleXML An <allow> DOM element that contains a single access rule - * @return {JSON} The Access Rule values to be set on this model - */ - parse: function(accessRuleXML) { - // If there is no access policy, do not attempt to parse anything - if (typeof accessRuleXML === "undefined" || !accessRuleXML) { - return {}; - } - - var accessRuleXMLObj = $(accessRuleXML); - - // Start an access rule object with the given subject - var parsedAccessRule = { - subject: accessRuleXMLObj.find("subject").text() - }; - - _.each(accessRuleXMLObj.find("permission"), function(permissionNode, idx) { - let permissionText = $(permissionNode).text().trim(); - - // Check if the permission text is not empty - if (permissionText.length) { - // Save the parsed permission - parsedAccessRule[permissionText] = true; - } else { - // This is added as a workaround for malformed permission XML - // introduced by Chromium 120.X - // See https://github.com/NCEAS/metacatui/issues/2235 - - // Define the regular expression - let globalPermRegex = /<permission><\/permission>(.*)/g; - // Define the regular expression - let permRegex = /<permission><\/permission>(.*)/; - - let accessRoleStr = accessRuleXMLObj.html(); - - let matches = accessRoleStr.match(globalPermRegex); - - // Check if matches exist and have a length - if (matches && matches.length && idx < matches.length) { - let permMatch = matches[idx].match(permRegex); - - // Check if permMatch exists and has a length - if (permMatch && permMatch.length) { - parsedAccessRule[permMatch[1]] = true; - } - } + defaults: function () { + return { + subject: null, + read: null, + write: null, + changePermission: null, + name: null, + dataONEObject: null, + }; + }, + + initialize: function () {}, + + /** + * Translates the access rule XML DOM into a JSON object to be set on the model. + * @param {Element} accessRuleXML An <allow> DOM element that contains a single access rule + * @return {JSON} The Access Rule values to be set on this model + */ + parse: function (accessRuleXML) { + // If there is no access policy, do not attempt to parse anything + if (typeof accessRuleXML === "undefined" || !accessRuleXML) { + return {}; } - }); - return parsedAccessRule; - }, + var accessRuleXMLObj = $(accessRuleXML); + + // Start an access rule object with the given subject + var parsedAccessRule = { + subject: accessRuleXMLObj.find("subject").text(), + }; + + _.each( + accessRuleXMLObj.find("permission"), + function (permissionNode, idx) { + let permissionText = $(permissionNode).text().trim(); + + // Check if the permission text is not empty + if (permissionText.length) { + // Save the parsed permission + parsedAccessRule[permissionText] = true; + } else { + // This is added as a workaround for malformed permission XML + // introduced by Chromium 120.X + // See https://github.com/NCEAS/metacatui/issues/2235 - /** - * Takes the values set on this model's attributes and creates an XML string - * to be inserted into a DataONEObject's system metadata access policy. - * @returns {object} The access rule XML object or null if not created - */ - serialize: function() { - // Serialize the allow rules - if (this.get("read") || this.get("write") || this.get("changePermission")) { + // Define the regular expression + let globalPermRegex = /<permission><\/permission>(.*)/g; + // Define the regular expression + let permRegex = /<permission><\/permission>(.*)/; + + let accessRoleStr = accessRuleXMLObj.html(); + + let matches = accessRoleStr.match(globalPermRegex); + + // Check if matches exist and have a length + if (matches && matches.length && idx < matches.length) { + let permMatch = matches[idx].match(permRegex); + + // Check if permMatch exists and has a length + if (permMatch && permMatch.length) { + parsedAccessRule[permMatch[1]] = true; + } + } + } + }, + ); + + return parsedAccessRule; + }, + + /** + * Takes the values set on this model's attributes and creates an XML string + * to be inserted into a DataONEObject's system metadata access policy. + * @returns {object} The access rule XML object or null if not created + */ + serialize: function () { + // Serialize the allow rules + if ( + this.get("read") || + this.get("write") || + this.get("changePermission") + ) { // Create the <allow> element - var allowElement = document.createElement('allow'); + var allowElement = document.createElement("allow"); // Create the <subject> element and set its text content - var subjectElement = document.createElement('subject'); + var subjectElement = document.createElement("subject"); subjectElement.textContent = this.get("subject"); // Append the <subject> and <permission> elements to <allow> allowElement.appendChild(subjectElement); // Create the <permission> elements and set their text content - var permissions = ['read', 'write', 'changePermission']; + var permissions = ["read", "write", "changePermission"]; for (var i = 0; i < permissions.length; i++) { - if (this.get(permissions[i])) { - var permissionElement = document.createElement('permission'); - permissionElement.textContent = permissions[i]; - allowElement.appendChild(permissionElement); - } + if (this.get(permissions[i])) { + var permissionElement = document.createElement("permission"); + permissionElement.textContent = permissions[i]; + allowElement.appendChild(permissionElement); + } } // Return the <allow> element return allowElement; - } - - // If no access rule is created, return null - return null; - }, - - - /** - * Gets and sets the subject info for the subjects in this access policy. - */ - getSubjectInfo: function(){ - - //If there is no subject, exit now since there is nothing to retrieve - if( !this.get("subject") ){ - return; - } + } - //If the subject is "public", there is no subject info to retrieve - if( this.get("subject") == "public" ){ - this.set("name", "Anyone"); - return; - } + // If no access rule is created, return null + return null; + }, - //If this is the current user, we can use the name we already have in the app. - if( this.get("subject") == MetacatUI.appUserModel.get("username") ){ - if( MetacatUI.appUserModel.get("fullName") ){ - this.set("name", MetacatUI.appUserModel.get("fullName")); + /** + * Gets and sets the subject info for the subjects in this access policy. + */ + getSubjectInfo: function () { + //If there is no subject, exit now since there is nothing to retrieve + if (!this.get("subject")) { return; } - } - var model = this; - - var ajaxOptions = { - url: MetacatUI.appModel.get("accountsUrl") + encodeURIComponent(this.get("subject")), - type: "GET", - dataType: "text", - processData: false, - parse: false, - success: function(response) { + //If the subject is "public", there is no subject info to retrieve + if (this.get("subject") == "public") { + this.set("name", "Anyone"); + return; + } - //If there was no response, exit now - if(!response){ + //If this is the current user, we can use the name we already have in the app. + if (this.get("subject") == MetacatUI.appUserModel.get("username")) { + if (MetacatUI.appUserModel.get("fullName")) { + this.set("name", MetacatUI.appUserModel.get("fullName")); return; } + } - var xmlDoc; - - try{ - xmlDoc = $.parseXML(response); - } - catch(e){ - //If the parsing XML failed, exit now - console.error("The accounts service did not return valid XML.", e); - return; - } + var model = this; + + var ajaxOptions = { + url: + MetacatUI.appModel.get("accountsUrl") + + encodeURIComponent(this.get("subject")), + type: "GET", + dataType: "text", + processData: false, + parse: false, + success: function (response) { + //If there was no response, exit now + if (!response) { + return; + } - //If the XML string was not parsed correctly, exit now - if( !XMLDocument.prototype.isPrototypeOf(xmlDoc) ){ - return; - } + var xmlDoc; + + try { + xmlDoc = $.parseXML(response); + } catch (e) { + //If the parsing XML failed, exit now + console.error( + "The accounts service did not return valid XML.", + e, + ); + return; + } - var subjectNode; + //If the XML string was not parsed correctly, exit now + if (!XMLDocument.prototype.isPrototypeOf(xmlDoc)) { + return; + } - if( model.isGroup() ){ - //Find the subject XML node for this person, by matching the text content with the subject - subjectNode = $(xmlDoc).find("group subject:contains(" + model.get("subject") + ")"); - } - else{ - //Find the subject XML node for this person, by matching the text content with the subject - subjectNode = $(xmlDoc).find("person subject:contains(" + model.get("subject") + ")"); - } + var subjectNode; + + if (model.isGroup()) { + //Find the subject XML node for this person, by matching the text content with the subject + subjectNode = $(xmlDoc).find( + "group subject:contains(" + model.get("subject") + ")", + ); + } else { + //Find the subject XML node for this person, by matching the text content with the subject + subjectNode = $(xmlDoc).find( + "person subject:contains(" + model.get("subject") + ")", + ); + } - //If no subject XML node was found, exit now - if( !subjectNode || !subjectNode.length ){ - return; - } + //If no subject XML node was found, exit now + if (!subjectNode || !subjectNode.length) { + return; + } - //If more than one subject was found (should be very unlikely), then find the one with the exact matching subject - if( subjectNode.length > 1 ){ - _.each(subjectNode, function(subjNode){ - if( $(subjNode).text() == model.get("subject") ){ - subjectNode = $(subjNode); - } - }); - } + //If more than one subject was found (should be very unlikely), then find the one with the exact matching subject + if (subjectNode.length > 1) { + _.each(subjectNode, function (subjNode) { + if ($(subjNode).text() == model.get("subject")) { + subjectNode = $(subjNode); + } + }); + } - var name; - if( model.isGroup() ){ - //Get the group name - name = $(subjectNode).siblings("groupName").text(); + var name; + if (model.isGroup()) { + //Get the group name + name = $(subjectNode).siblings("groupName").text(); - //If there is no group name, then just use the name parsed from the subject - if( !name ){ - name = model.get("subject").substring(3, model.get("subject").indexOf(",DC=dataone") ); + //If there is no group name, then just use the name parsed from the subject + if (!name) { + name = model + .get("subject") + .substring(3, model.get("subject").indexOf(",DC=dataone")); + } + } else { + //Get the first and last name for this person + name = + $(subjectNode).siblings("givenName").text() + + " " + + $(subjectNode).siblings("familyName").text(); } - } - else{ - //Get the first and last name for this person - name = $(subjectNode).siblings("givenName").text() + " " + $(subjectNode).siblings("familyName").text(); - } - - //Set the name on the model - model.set("name", name); + //Set the name on the model + model.set("name", name); + }, + }; + + //Send the XHR + $.ajax(ajaxOptions); + }, + + /** + * Returns true if the subbject set on this AccessRule is for a group of people. + * @returns {boolean} + */ + isGroup: function () { + try { + //Check if the subject is a group subject + var matches = this.get("subject").match(/CN=.+,DC=dataone,DC=org/); + return Array.isArray(matches) && matches.length; + } catch (e) { + console.error( + "Couldn't determine if the subject in this AccessRule is a group: ", + e, + ); + return false; } - } - - //Send the XHR - $.ajax(ajaxOptions); + }, }, + ); - /** - * Returns true if the subbject set on this AccessRule is for a group of people. - * @returns {boolean} - */ - isGroup: function(){ - - try{ - //Check if the subject is a group subject - var matches = this.get("subject").match(/CN=.+,DC=dataone,DC=org/); - return (Array.isArray(matches) && matches.length); - } - catch(e){ - console.error("Couldn't determine if the subject in this AccessRule is a group: ", e); - return false; - } - - } - - }); - - return AccessRule; - + return AccessRule; });
diff --git a/docs/docs/src_js_models_AppModel.js.html b/docs/docs/src_js_models_AppModel.js.html index 3d6e7a780..5ea0549c8 100644 --- a/docs/docs/src_js_models_AppModel.js.html +++ b/docs/docs/src_js_models_AppModel.js.html @@ -44,308 +44,318 @@

Source: src/js/models/AppModel.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone'],
-  function($, _, Backbone) {
-  'use strict';
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  "use strict";
 
   /**
-  * @class AppModel
-  * @classdesc A utility model that contains top-level configuration and storage for the application
-  * @name AppModel
-  * @extends Backbone.Model
-  * @constructor
-  * @classcategory Models
-  */
+   * @class AppModel
+   * @classdesc A utility model that contains top-level configuration and storage for the application
+   * @name AppModel
+   * @extends Backbone.Model
+   * @constructor
+   * @classcategory Models
+   */
   var AppModel = Backbone.Model.extend(
     /** @lends AppModel.prototype */ {
+      defaults: _.extend(
+        /** @lends AppConfig.prototype */ {
+          //TODO: These attributes are stored in the AppModel, but shouldn't be set in the AppConfig,
+          //so we need to add docs for them in a separate place
+          headerType: "default",
+          searchHistory: [],
+          page: 0,
+          previousPid: null,
+          lastPid: null,
+          anchorId: null,
+          profileUsername: null,
+
+          /**
+           * The theme name to use
+           * @type {string}
+           * @default "default"
+           */
+          theme: "default",
+
+          /**
+           * The default page title.
+           * @type {string}
+           */
+          title: MetacatUI.themeTitle || "Metacat Data Catalog",
+
+          /**
+           * The default page description.
+           * @type {string}
+           * @since 2.25.0
+           */
+          description:
+            "A research data catalog and repository that provides access to scientific data, metadata, and more.",
+
+          /**
+           * The name of this repository. This is used throughout the interface in different
+           * messages and page content.
+           * @type {string}
+           * @default "Metacat Data Catalog"
+           * @since 2.11.2
+           */
+          repositoryName: MetacatUI.themeTitle || "Metacat Data Catalog",
+
+          /**
+           * The e-mail address that people should contact when they need help with
+           * submitting datasets, resolving error messages, etc.
+           * @type {string}
+           * @default knb-help@nceas.ucsb.edu
+           */
+          emailContact: "knb-help@nceas.ucsb.edu",
+
+          /**
+           * Your Google Maps API key, which is used to display interactive maps on the search
+           * views and static maps on dataset landing pages.
+           * If a Google Maps API key is not specified, the maps will be omitted from the interface.
+           * The Google Maps API key also controls the showViewfinder feature on a Map
+           * and should have the Geocoding API and Places API enabled in order to
+           * function properly.
+           * Sign up for Google Maps services at https://console.developers.google.com/
+           * @type {string}
+           * @example "AIzaSyCYyHnbIokUEpMx5M61ButwgNGX8fIHUs"
+           * @default null
+           */
+          mapKey: null,
+
+          /**
+           * Your Google Analytics API key, which is used to send page view and custom events
+           * to the Google Analytics service.
+           * This service is optional in MetacatUI.
+           * Sign up for Google Analytics services at https://analytics.google.com/analytics/web/
+           * @type {string}
+           * @example "UA-74622301-1"
+           * @default null
+           */
+          googleAnalyticsKey: null,
+
+          /**
+           * The map to use in the catalog search (both DataCatalogViewWithFilters and
+           * DataCatalog). This can be set to either "google" (the default), or "cesium". To
+           * use Google maps, the {@link AppConfig#googleAnalyticsKey} must be set. To use
+           * Cesium maps, the {@link AppConfig#enableCesium} property must be set to true, and
+           * the {@link AppConfig#cesiumToken} must be set. DEPRECATION NOTE: This configuration
+           * is deprecated along with the {@link DataCatalogView} and {@link DataCatalogViewWithFilters}
+           * views and Google Maps. The {@link CatalogSearchView} will replace these as the primary search view and will only
+           * support Cesium, not Google Maps.
+           * @type {string}
+           * @example "cesium"
+           * @default "google"
+           * @deprecated
+           */
+          dataCatalogMap: "google",
+
+          /**
+           * Set this option to true to display the filtering button for data package table
+           * @type {boolean}
+           * @since 2.28.0
+           */
+          dataPackageFiltering: false,
+
+          /**
+           * Set this option to true to display the sorting button for data package table
+           * @type {boolean}
+           * @since 2.28.0
+           */
+          dataPackageSorting: false,
+
+          /**
+           * The default options for the Cesium map used in the {@link CatalogSearchView} for searching the data
+           * catalog. Add custom layers, a default home position (for example, zoom into your area of research),
+           * and enable/disable map widgets. See {@link MapConfig} for the full suite of options. Keep the `CesiumGeohash`
+           * layer here in order to show the search results in the map as geohash boxes. Use any satellite imagery
+           * layer of your choice, such as a self-hosted imagery layer or hosted on Cesium Ion.
+           * @type {MapConfig}
+           * @since 2.22.0
+           */
+          catalogSearchMapOptions: {
+            showLayerList: false,
+            clickFeatureAction: "zoom",
+          },
 
-    defaults: _.extend(
-      /** @lends AppConfig.prototype */{
-
-      //TODO: These attributes are stored in the AppModel, but shouldn't be set in the AppConfig,
-      //so we need to add docs for them in a separate place
-      headerType: 'default',
-      searchHistory: [],
-      page: 0,
-      previousPid: null,
-      lastPid: null,
-      anchorId: null,
-      profileUsername: null,
-
-      /**
-      * The theme name to use
-      * @type {string}
-      * @default "default"
-      */
-      theme: "default",
-
-      /**
-      * The default page title.
-      * @type {string}
-      */
-      title: MetacatUI.themeTitle || "Metacat Data Catalog",
-
-      /**
-       * The default page description.
-       * @type {string}
-       * @since 2.25.0
-       */
-      description: "A research data catalog and repository that provides access to scientific data, metadata, and more.",
-
-      /**
-      * The name of this repository. This is used throughout the interface in different
-      * messages and page content.
-      * @type {string}
-      * @default "Metacat Data Catalog"
-      * @since 2.11.2
-      */
-      repositoryName: MetacatUI.themeTitle || "Metacat Data Catalog",
-
-      /**
-      * The e-mail address that people should contact when they need help with
-      * submitting datasets, resolving error messages, etc.
-      * @type {string}
-      * @default knb-help@nceas.ucsb.edu
-      */
-      emailContact: "knb-help@nceas.ucsb.edu",
-
-      /**
-      * Your Google Maps API key, which is used to display interactive maps on the search
-      * views and static maps on dataset landing pages.
-      * If a Google Maps API key is not specified, the maps will be omitted from the interface.
-      * The Google Maps API key also controls the showViewfinder feature on a Map
-      * and should have the Geocoding API and Places API enabled in order to
-      * function properly.
-      * Sign up for Google Maps services at https://console.developers.google.com/
-      * @type {string}
-      * @example "AIzaSyCYyHnbIokUEpMx5M61ButwgNGX8fIHUs"
-      * @default null
-      */
-      mapKey: null,
-
-      /**
-      * Your Google Analytics API key, which is used to send page view and custom events
-      * to the Google Analytics service.
-      * This service is optional in MetacatUI.
-      * Sign up for Google Analytics services at https://analytics.google.com/analytics/web/
-      * @type {string}
-      * @example "UA-74622301-1"
-      * @default null
-      */
-      googleAnalyticsKey: null,
-
-      /**
-      * The map to use in the catalog search (both DataCatalogViewWithFilters and
-      * DataCatalog). This can be set to either "google" (the default), or "cesium". To
-      * use Google maps, the {@link AppConfig#googleAnalyticsKey} must be set. To use
-      * Cesium maps, the {@link AppConfig#enableCesium} property must be set to true, and
-      * the {@link AppConfig#cesiumToken} must be set. DEPRECATION NOTE: This configuration
-      * is deprecated along with the {@link DataCatalogView} and {@link DataCatalogViewWithFilters}
-      * views and Google Maps. The {@link CatalogSearchView} will replace these as the primary search view and will only
-      * support Cesium, not Google Maps.
-      * @type {string}
-      * @example "cesium"
-      * @default "google"
-      * @deprecated
-      */
-      dataCatalogMap: "google",
-
-      /**
-      * Set this option to true to display the filtering button for data package table
-      * @type {boolean}
-      * @since 2.28.0
-      */
-      dataPackageFiltering: false,
-
-      /**
-      * Set this option to true to display the sorting button for data package table
-      * @type {boolean}
-      * @since 2.28.0
-      */
-      dataPackageSorting: false,
-
-      /**
-       * The default options for the Cesium map used in the {@link CatalogSearchView} for searching the data
-       * catalog. Add custom layers, a default home position (for example, zoom into your area of research),
-       * and enable/disable map widgets. See {@link MapConfig} for the full suite of options. Keep the `CesiumGeohash`
-       * layer here in order to show the search results in the map as geohash boxes. Use any satellite imagery
-       * layer of your choice, such as a self-hosted imagery layer or hosted on Cesium Ion.
-       * @type {MapConfig}
-       * @since 2.22.0
-       */
-      catalogSearchMapOptions: {
-        showLayerList: false,
-        clickFeatureAction: "zoom"
-       },
-
-      /**
-      * The node identifier for this repository. This is set dynamically by retrieving the
-      * DataONE Coordinating Node document and finding this repository in the Node list.
-      * (see https://cn.dataone.org/cn/v2/node).
-      * If this repository is not registered with DataONE, then set this node id by copying
-      * the node id from your node info at https://your-repo-site.com/metacat/d1/mn/v2/node
-      * @type {string}
-      * @example "urn:node:METACAT"
-      * @default null
-      */
-      nodeId: null,
-
-      /**
-      * If true, this MetacatUI instance is pointing to a CN rather than a MN.
-      * This attribute is set during the AppModel initialization, based on the other configured attributes.
-      * @readonly
-      * @type {boolean}
-      */
-      isCN: false,
-
-      /**
-      * Enable or disable the user profiles. If enabled, users will see a "My profile" link
-      * and can view their datasets, metrics on those datasets, their groups, etc.
-      * @type {boolean}
-      * @default true
-      */
-      enableUserProfiles: true,
-
-      /**
-      * Enable or disable the user settings view. If enabled, users will see a list of
-      * changeable settings - name, email, groups, portals, etc.
-      * @type {boolean}
-      * @default true
-      */
-      enableUserProfileSettings: true,
-
-      /**
-      * The maximum dataset .zip file size, in bytes, that a user can download.
-      * Datasets whose total size are larger than this maximum will show a disabled
-      * "Download All" button, and users will be directed to download files individually.
-      * This is useful for preventing the Metacat package service from getting overloaded.
-      * @type {number}
-      * @default 100000000000
-      */
-      maxDownloadSize: 100000000000,
-
-      /**
-      * Add a message that will display during a certain time period. This is useful when
-      * displaying a warning message about planned outages/maintenance, or alert users to other
-      * important information.
-      * If this attribute is left blank, no message will display, even if there is a start and end time specified.
-      * If there are is no start or end time specified, this message will display until you remove it here.
-      *
-      * @type {string}
-      * @default null
-      * @since 2.11.4
-      */
-      temporaryMessage: null,
-
-      /**
-      * If there is a temporaryMessage specified, it will display after this start time.
-      * Remember that Dates are in GMT time!
-      * @type {Date}
-      * @example new Date(1594818000000)
-      * @default null
-      * @since 2.11.4
-      */
-      temporaryMessageStartTime: null,
-
-      /**
-      * If there is a temporaryMessage specified, it will display before this end time.
-      * Remember that Dates are in GMT time!
-      * @type {Date}
-      * @example new Date(1594818000000)
-      * @default null
-      * @since 2.11.4
-      */
-      temporaryMessageEndTime: null,
-
-      /**
-      * Additional HTML classes to give the temporary message element. Use these to style the message.
-      * @type {string}
-      * @default "warning"
-      * @since 2.11.4
-      */
-      temporaryMessageClasses: "warning",
-
-      /**
-      * A jQuery selector for the element that the temporary message will be displayed in.
-      * @type {string}
-      * @default "#Navbar"
-      * @since 2.11.4
-      */
-      temporaryMessageContainer: "#Navbar",
-
-      /**
-      * If true, the temporary message will include a "Need help? Email us at..." email link
-      * at the end of the message. The email address will be set to {@link AppConfig#emailContact}
-      * @type {boolean}
-      * @default true
-      * @since 2.13.3
-      */
-      temporaryMessageIncludeEmail: true,
-
-      /**
-      * Show or hide the source repository logo in the search result rows
-      * @type {boolean}
-      * @default false
-      */
-      displayRepoLogosInSearchResults: false,
-      /**
-      * Show or hide the Download button in the search result rows
-      * @type {boolean}
-      * @default false
-      */
-      displayDownloadButtonInSearchResults: false,
-
-      /**
-      * If set to false, some parts of the app will send POST HTTP requests to the
-      * Solr search index via the `/query/solr` DataONE API.
-      * Set this configuration to true if using Metacat 2.10.2 or earlier
-      * @type {boolean}
-      */
-      disableQueryPOSTs: false,
-
-      /** If set to true, some parts of the app will use the Solr Join Query syntax
-      * when sending queries to the `/query/solr` DataONE API.
-      * If this is not enabled, then some parts of the UI may not work if a query has too
-      * many characters or has too many boolean clauses. This impacts the "Metrics" tabs of portals/collections,
-      * at least.
-      * The Solr Join Query Parser as added in Solr 4.0.0-ALPHA (I believe!): https://archive.apache.org/dist/lucene/solr/4.0.0/changes/Changes.html#4.0.0-alpha.new_features
-      * About the Solr Join Query Parser: https://lucene.apache.org/solr/guide/8_5/other-parsers.html#join-query-parser
-      * WARNING: At some point, MetacatUI will deprecate this configuration and will REQUIRE Solr Join Queries
-      * @type {boolean}
-      */
-      enableSolrJoins: false,
-
-      /**
-      * The search filters that will be displayed in the search views. Add or remove
-      * filter names from this array to show or hide them. See "example" to see all the
-      * filter options.
-      * @type {string[]}
-      * @default ["all", "attribute", "documents", "creator", "dataYear", "pubYear", "id", "taxon", "spatial", "isPrivate"]
-      * @example ["all", "annotation", "attribute", "dataSource", "documents", "creator", "dataYear", "pubYear", "id", "taxon", "spatial"]
-      */
-      defaultSearchFilters: ["all", "attribute", "documents", "creator", "dataYear", "pubYear", "id", "taxon", "spatial", "isPrivate", "projectText"],
-
-      /**
-       * Enable to show Whole Tale features
-       * @type {Boolean}
-       * @default false
-       */
-      showWholeTaleFeatures: false,
-      /**
-       * The WholeTale environments that are exposed on the dataset landing pages
-       * @type {string[]}
-       * @default ["RStudio", "Jupyter Notebook"]
-       */
-      taleEnvironments: ["RStudio", "Jupyter Notebook"],
-      /**
-      * The Whole Tale endpoint
-      * @type {string}
-      * @default 'https://girder.wholetale.org/api/v1/integration/dataone'
-      */
-      dashboardUrl: 'https://girder.wholetale.org/api/v1/integration/dataone',
-
-      /**
+          /**
+           * The node identifier for this repository. This is set dynamically by retrieving the
+           * DataONE Coordinating Node document and finding this repository in the Node list.
+           * (see https://cn.dataone.org/cn/v2/node).
+           * If this repository is not registered with DataONE, then set this node id by copying
+           * the node id from your node info at https://your-repo-site.com/metacat/d1/mn/v2/node
+           * @type {string}
+           * @example "urn:node:METACAT"
+           * @default null
+           */
+          nodeId: null,
+
+          /**
+           * If true, this MetacatUI instance is pointing to a CN rather than a MN.
+           * This attribute is set during the AppModel initialization, based on the other configured attributes.
+           * @readonly
+           * @type {boolean}
+           */
+          isCN: false,
+
+          /**
+           * Enable or disable the user profiles. If enabled, users will see a "My profile" link
+           * and can view their datasets, metrics on those datasets, their groups, etc.
+           * @type {boolean}
+           * @default true
+           */
+          enableUserProfiles: true,
+
+          /**
+           * Enable or disable the user settings view. If enabled, users will see a list of
+           * changeable settings - name, email, groups, portals, etc.
+           * @type {boolean}
+           * @default true
+           */
+          enableUserProfileSettings: true,
+
+          /**
+           * The maximum dataset .zip file size, in bytes, that a user can download.
+           * Datasets whose total size are larger than this maximum will show a disabled
+           * "Download All" button, and users will be directed to download files individually.
+           * This is useful for preventing the Metacat package service from getting overloaded.
+           * @type {number}
+           * @default 100000000000
+           */
+          maxDownloadSize: 100000000000,
+
+          /**
+           * Add a message that will display during a certain time period. This is useful when
+           * displaying a warning message about planned outages/maintenance, or alert users to other
+           * important information.
+           * If this attribute is left blank, no message will display, even if there is a start and end time specified.
+           * If there are is no start or end time specified, this message will display until you remove it here.
+           *
+           * @type {string}
+           * @default null
+           * @since 2.11.4
+           */
+          temporaryMessage: null,
+
+          /**
+           * If there is a temporaryMessage specified, it will display after this start time.
+           * Remember that Dates are in GMT time!
+           * @type {Date}
+           * @example new Date(1594818000000)
+           * @default null
+           * @since 2.11.4
+           */
+          temporaryMessageStartTime: null,
+
+          /**
+           * If there is a temporaryMessage specified, it will display before this end time.
+           * Remember that Dates are in GMT time!
+           * @type {Date}
+           * @example new Date(1594818000000)
+           * @default null
+           * @since 2.11.4
+           */
+          temporaryMessageEndTime: null,
+
+          /**
+           * Additional HTML classes to give the temporary message element. Use these to style the message.
+           * @type {string}
+           * @default "warning"
+           * @since 2.11.4
+           */
+          temporaryMessageClasses: "warning",
+
+          /**
+           * A jQuery selector for the element that the temporary message will be displayed in.
+           * @type {string}
+           * @default "#Navbar"
+           * @since 2.11.4
+           */
+          temporaryMessageContainer: "#Navbar",
+
+          /**
+           * If true, the temporary message will include a "Need help? Email us at..." email link
+           * at the end of the message. The email address will be set to {@link AppConfig#emailContact}
+           * @type {boolean}
+           * @default true
+           * @since 2.13.3
+           */
+          temporaryMessageIncludeEmail: true,
+
+          /**
+           * Show or hide the source repository logo in the search result rows
+           * @type {boolean}
+           * @default false
+           */
+          displayRepoLogosInSearchResults: false,
+          /**
+           * Show or hide the Download button in the search result rows
+           * @type {boolean}
+           * @default false
+           */
+          displayDownloadButtonInSearchResults: false,
+
+          /**
+           * If set to false, some parts of the app will send POST HTTP requests to the
+           * Solr search index via the `/query/solr` DataONE API.
+           * Set this configuration to true if using Metacat 2.10.2 or earlier
+           * @type {boolean}
+           */
+          disableQueryPOSTs: false,
+
+          /** If set to true, some parts of the app will use the Solr Join Query syntax
+           * when sending queries to the `/query/solr` DataONE API.
+           * If this is not enabled, then some parts of the UI may not work if a query has too
+           * many characters or has too many boolean clauses. This impacts the "Metrics" tabs of portals/collections,
+           * at least.
+           * The Solr Join Query Parser as added in Solr 4.0.0-ALPHA (I believe!): https://archive.apache.org/dist/lucene/solr/4.0.0/changes/Changes.html#4.0.0-alpha.new_features
+           * About the Solr Join Query Parser: https://lucene.apache.org/solr/guide/8_5/other-parsers.html#join-query-parser
+           * WARNING: At some point, MetacatUI will deprecate this configuration and will REQUIRE Solr Join Queries
+           * @type {boolean}
+           */
+          enableSolrJoins: false,
+
+          /**
+           * The search filters that will be displayed in the search views. Add or remove
+           * filter names from this array to show or hide them. See "example" to see all the
+           * filter options.
+           * @type {string[]}
+           * @default ["all", "attribute", "documents", "creator", "dataYear", "pubYear", "id", "taxon", "spatial", "isPrivate"]
+           * @example ["all", "annotation", "attribute", "dataSource", "documents", "creator", "dataYear", "pubYear", "id", "taxon", "spatial"]
+           */
+          defaultSearchFilters: [
+            "all",
+            "attribute",
+            "documents",
+            "creator",
+            "dataYear",
+            "pubYear",
+            "id",
+            "taxon",
+            "spatial",
+            "isPrivate",
+            "projectText",
+          ],
+
+          /**
+           * Enable to show Whole Tale features
+           * @type {Boolean}
+           * @default false
+           */
+          showWholeTaleFeatures: false,
+          /**
+           * The WholeTale environments that are exposed on the dataset landing pages
+           * @type {string[]}
+           * @default ["RStudio", "Jupyter Notebook"]
+           */
+          taleEnvironments: ["RStudio", "Jupyter Notebook"],
+          /**
+           * The Whole Tale endpoint
+           * @type {string}
+           * @default 'https://girder.wholetale.org/api/v1/integration/dataone'
+           */
+          dashboardUrl:
+            "https://girder.wholetale.org/api/v1/integration/dataone",
+
+          /**
        * A list of all the required fields in the EML Editor.
        * Any field set to `true` will prevent the user from saving the Editor until a value has been given
        * Any EML field not supported in this list cannot be required.
@@ -389,1987 +399,2300 @@ 

Source: src/js/models/AppModel.js

* generalTaxonomicCoverage: false, * taxonCoverage: false, * geoCoverage: false, - * intellectualRights: true, - * keywordSets: false, - * methods: false, - * samplingDescription: false, - * studyExtentDescription: false, - * temporalCoverage: false, - * title: true, - * contact: true, - * principalInvestigator: true - * } - */ - emlEditorRequiredFields: { - abstract: true, - alternateIdentifier: false, - dataSensitivity: false, - funding: false, - generalTaxonomicCoverage: false, - taxonCoverage: false, - geoCoverage: false, - intellectualRights: true, - keywordSets: false, - methods: false, - samplingDescription: false, - studyExtentDescription: false, - temporalCoverage: false, - title: true, - creator: true, - contact: true - }, - - /** - * A list of required fields for each EMLParty (People) in the dataset editor. - * This is a literal object where the keys are the EML Party type (e.g. creator, principalInvestigator) {@link see EMLParty.partytypes} - * and the values are arrays of field names. - * By default, EMLPartys are *always* required to have an individual's name, position name, or organization name. - * @type {object} - * @since 2.21.0 - * @example - * { - * contact: ["email"], - * creator: ["email", "address", "phone"] - * principalInvestigator: ["organizationName"] - * } - * @default - * { - * } - */ - emlEditorRequiredFields_EMLParty: { - - }, - - /** - * An array of science metadata format IDs that are editable in MetacatUI. - * Metadata documents with these format IDs will have an Edit button and will be - * editable in the Editor Views. - * This should only be changed if you have extended MetacatUI to edit a new format, - * or if you want to disable editing of a specific format ID. - * @type {string[]} - * @default [ - "eml://ecoinformatics.org/eml-2.1.1", - "https://eml.ecoinformatics.org/eml-2.2.0" - ] - * @example - * [ - * "eml://ecoinformatics.org/eml-2.1.1", - * "https://eml.ecoinformatics.org/eml-2.2.0" - * ] - * @readonly - */ - editableFormats: [ - "eml://ecoinformatics.org/eml-2.1.1", - "https://eml.ecoinformatics.org/eml-2.2.0" - ], - - /** - * The format ID the dataset editor serializes new EML as - * @type {string} - * @default "https://eml.ecoinformatics.org/eml-2.2.0" - * @readonly - * @since 2.13.0 - */ - editorSerializationFormat: "https://eml.ecoinformatics.org/eml-2.2.0", - - /** - * The XML schema location the dataset editor will use when creating new EML. This should - * correspond with {@link AppConfig#editorSerializationFormat} - * @type {string} - * @default "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd" - * @readonly - * @since 2.13.0 - */ - editorSchemaLocation: "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd", - - /** - * The text to use for the eml system attribute. The system attribute - * indicates the data management system within which an identifier is in - * scope and therefore unique. This is typically a URL (Uniform Resource - * Locator) that indicates a data management system. All identifiers that - * share a system must be unique. In other words, if the same identifier - * is used in two locations with identical systems, then by definition the - * objects at which they point are in fact the same object. - * @type {string} - * @since 2.26.0 - * @link https://eml.ecoinformatics.org/schema/eml-resource_xsd#SystemType - * @link https://eml.ecoinformatics.org/schema/eml_xsd - */ - emlSystem: "knb", - - /** - * This error message is displayed when the Editor encounters an error saving - * @type {string} - */ - editorSaveErrorMsg: "Not all of your changes could be submitted.", - /** - * This error message is displayed when the Editor encounters an error saving, and a plain-text draft is saved instead - * @type {string} - */ - editorSaveErrorMsgWithDraft: "Not all of your changes could be submitted, but a draft " + - "has been saved which can be accessed by our support team. Please contact us.", - /** - * The text of the Save button in the dataset editor. - * @type {string} - * @default "Save dataset" - * @since 2.13.3 - */ - editorSaveButtonText: "Save dataset", - - /** - * A list of keyword thesauri options for the user to choose from in the EML Editor. - * A "None" option will also always display. - * @type {object[]} - * @property {string} label - A readable and short label for the keyword thesaurus that is displayed in the UI - * @property {string} thesaurus - The exact keyword thesaurus name that will be saved in the EML - * @since 2.10.0 - * @default [{ - label: "GCMD", - thesaurus: "NASA Global Change Master Directory (GCMD)" - }] - * @example - * [{ - * label: "GCMD", - * thesaurus: "NASA Global Change Master Directory (GCMD)" - * }] - */ - emlKeywordThesauri: [{ - label: "GCMD", - thesaurus: "NASA Global Change Master Directory (GCMD)" - }], - - - /** - * If true, questions related to Data Sensitivity will be shown in the EML Editor. - * @type {boolean} - * @default true - * @since 2.19.0 - */ - enableDataSensitivityInEditor: true, - - - /** - * The URL of a webpage that shows more information about Data Sensitivity and DataTags. This will be used - * for links in help text throughout the app, such as next to Data Sensitivity questions in the dataset editor. - * - * @type {string} - * @default "http://datatags.org" - * @since 2.19.0 - */ - dataSensitivityInfoURL: "http://datatags.org", - - /** - * In the editor, sometimes it is useful to have guided questions for the Methods section - * in addition to the generic numbered method steps. These custom methods are defined here - * as an array of literal objects that define each custom Methods question. Custom methods - * are serialized to the EML as regular method steps, but with an unchangeable title, defined here, - * in order to identify them. - * - * @typedef {object} CustomEMLMethod - * @property {string[]} titleOptions One or more titles that may exist in an EML Method Step that identify that Method Step as a custom method type. THe first title in the array is serialized to the EML XML. - * @property {string} id A unique identifier for this custom method type. - * @property {boolean} required If true, this custom method will be a required field for submission in the EML editor. - * @example [{ - "titleOptions": ["Ethical Research Procedures"], - "id": "ethical-research-procedures", - "required": false - }] - * @since 2.19.0 - */ - /** - * In the editor, sometimes it is useful to have guided questions for the Methods section - * in addition to the generic numbered method steps. These custom methods are defined here - * as an array of literal objects that define each custom Methods question. Custom methods - * are serialized to the EML as regular method steps, but with an unchangeable title, defined here, - * in order to identify them. - * @type {CustomEMLMethod} - * @since 2.19.0 - */ - customEMLMethods: [], - - /** - * Configuration options for a drop down list of taxa. - * @typedef {object} AppConfig#quickAddTaxaList - * @type {Object} - * @property {string} label - The label for the dropdown menu - * @property {string} placeholder - The placeholder text for the input field - * @property {EMLTaxonCoverage#taxonomicClassification[]} taxa - The list of taxa to show in the dropdown menu - * @example - * { - * label: "Primates", - * placeholder: "Select one or more primates", - * taxa: [ - * { - * commonName: "Bonobo", - * taxonRankName: "Species", - * taxonRankValue: "Pan paniscus", - * taxonId: { - * provider: "ncbi", - * value: "9597" - * } - * }, - * { - * commonName: "Chimpanzee", - * ... - * }, - * ... - * } - * @since 2.24.0 - */ - - /** - * A list of taxa to show in the Taxa Quick Add section of the EML editor. - * This can be used to expedite entry of taxa that are common in the - * repository's domain. The quickAddTaxa is a list of objects, each - * defining a separate dropdown interface. This way, common taxa can - * be grouped together. - * Alternative, provide a SID for a JSON data object that is stored in the - * repository. The JSON must be in the same format as required for this - * configuration option. - * @since 2.24.0 - * @type {AppConfig#quickAddTaxaList[] | string} - * @example - * [ - * { - * label: "Bats" - * placeholder: "Select one or more bats", - * taxa: [ ... ] - * }, - * { - * label: "Birds" - * placeholder: "Select one or more birds", - * taxa: [ ... ] - * } - * ] - */ - quickAddTaxa: [], - - /** - * The base URL for the repository. This only needs to be changed if the repository - * is hosted at a different origin than the MetacatUI origin. This URL is used to contruct all - * of the DataONE REST API URLs. If you are testing MetacatUI against a development repository - * at an external location, this is where you would set that external repository URL. - * @type {string} - * @default window.location.origin || (window.location.protocol + "//" + window.location.host) - */ - baseUrl: window.location.origin || (window.location.protocol + "//" + window.location.host), - - /** - * The directory that metacat is installed in at the `baseUrl`. For example, if you - * have metacat installed in the tomcat webapps directory as `metacat`, then this should be set - * to "/metacat". Or if you renamed the metacat webapp to `catalog`, then it should be `/catalog`. - * @type {string} - * @default "/metacat" - */ - context: MetacatUI.AppConfig.metacatContext || '/metacat', - - /** - * The URL fragment for the DataONE Member Node (MN) API. - * @type {string} - * @default '/d1/mn/v2' - */ - d1Service: '/d1/mn/v2', - /** - * The base URL of the DataONE Coordinating Node (CN). CHange this if you - * are testing a deployment in a development environment. - * @type {string} - * @default "https://cn.dataone.org" - * @example "https://cn-stage.test.dataone.org" - */ - d1CNBaseUrl: "https://cn.dataone.org", - /** - * The URL fragment for the DataONE Coordinating Node (CN) API. - * @type {string} - * @default '/cn/v2' - */ - d1CNService: "/cn/v2", - /** - * The URL for the DataONE Search MetacatUI. This only needs to be changed - * if you want to point to a development environment. - * @type {string} - * @default "https://search.dataone.org" - * @readonly - * @since 2.13.0 - */ - dataoneSearchUrl: "https://search.dataone.org", - /** - * The URL for the DataONE listNodes() API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNCore.listNodes) - * @type {string} - */ - nodeServiceUrl: null, - /** - * The URL for the DataONE View API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#module-MNView) - * @type {string} - */ - viewServiceUrl: null, - /** - * The URL for the DataONE getPackage() API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNPackage.getPackage) - * - * @type {string} - */ - packageServiceUrl: null, - /** - * The URL for the Metacat Publish service. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * @type {string} - */ - publishServiceUrl: null, - /** - * The URL for the DataONE isAuthorized() API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNAuthorization.isAuthorized) - * @type {string} - */ - authServiceUrl: null, - /** - * The URL for the DataONE query API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNQuery.query) - * @type {string} - */ - queryServiceUrl: null, - /** - * The URL for the DataONE reserveIdentifier() API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNCore.reserveIdentifier) - * @type {string} - */ - reserveServiceUrl: null, - /** - * The URL for the DataONE system metadata API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNRead.getSystemMetadata - * and https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNStorage.updateSystemMetadata) - * @type {string} - */ - metaServiceUrl: null, - /** - * The URL for the DataONE system metadata API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNRead.getSystemMetadata - * and https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNStorage.updateSystemMetadata) - * @type {string} - */ - objectServiceUrl: null, - /** - * The URL for the DataONE Formats API. This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNCore.listFormats) - * @type {string} - */ - formatsServiceUrl: null, - /** - * The URL fragment for the DataONE Formats API. This is combined with the AppConfig#formatsServiceUrl - * @type {string} - * @default "/formats" - */ - formatsUrl: "/formats", - - /** - * If true, parts of the UI (most notably, "funding" field in the dataset editor) - * may look up NSF Award information - * @type {boolean} - * @default false - */ - useNSFAwardAPI: false, - /** - * The URL for the NSF Award API, which can be used by the {@link LookupModel} - * to look up award information for the dataset editor or other views. The - * URL must point to a proxy that can make requests to the NSF Award API, - * since it does not support CORS. - * @type {string} - * @default "/research.gov/awardapi-service/v1/awards.json" - */ - grantsUrl: "/research.gov/awardapi-service/v1/awards.json", - - /** - * The base URL for the ORCID REST services - * @type {string} - * @default "https:/orcid.org" - */ - orcidBaseUrl: "https:/orcid.org", - - /** - * The URL for the ORCID search API, which can be used to search for information - * about people using their ORCID, email, name, etc. - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * @type {string} - */ - orcidSearchUrl: null, - - /** - * The URL for the Metacat API. The Metacat API has been deprecated and is kept here - * for compatability with Metacat repositories that are using the old x509 certificate - * authentication mechanism. This is deprecated since authentication is now done via - * the DataONE Portal service using auth tokens. (Using the {@link AppConfig#tokenUrl}) - * This URL is contructed dynamically when the AppModel is initialized. - * Only override this if you are an advanced user and have a reason to! - * @type {string} - */ - metacatServiceUrl: null, - - /** - * If false, the /monitor/status (the service that returns the status of various DataONE services) will not be used. - * @type {boolean} - * @default true - * @since 2.9.0 - */ - enableMonitorStatus: true, - - /** - * The URL for the service that returns the status of various DataONE services. - * The only supported status so far is the search index queue -- the number of - * objects that are waiting to be indexed in the Solr search index. - * This URL is contructed dynamically when the - * AppModel is initialized. Only override this if you are an advanced user and have a reason to! - * @type {string} - * @since 2.9.0 - */ - monitorStatusUrl: "", - - /** - * If true, users will see a page with sign-in troubleshooting tips - * @type {boolean} - * @default true - * @since 2.13.3 - */ - showSignInHelp: true, - /** - * If true, users can sign in using CILogon as the identity provider. - * ORCID is the only recommended identity provider. CILogon may be deprecated - * in the future. - * @type {boolean} - * @default false - */ - enableCILogonSignIn: false, - /** - * The URL for the DataONE Sign In API using CILogon as the identity provider - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * @type {string} - */ - signInUrl: null, - /** - * The URL for the DataONE Sign Out API - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * @type {string} - */ - signOutUrl: null, - /** - * The URL for the DataONE Sign In API using ORCID as the identity provider - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * @type {string} - */ - signInUrlOrcid: null, - - /** - * Enable DataONE LDAP authentication. If true, users can sign in from an LDAP account that is in the DataONE CN LDAP directory. - * This is not recommended, as DataONE is moving towards supporting only ORCID logins for users. - * This LDAP authentication is separate from the File-based authentication for the Metacat Admin interface. - * @type {boolean} - * @default false - * @since 2.11.0 - */ - enableLdapSignIn: false, - /** - * The URL for the DataONE Sign In API using LDAP as the identity provider - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * @type {string} - */ - signInUrlLdap: null, - - /** - * The URL for the DataONE Token API using ORCID as the identity provider - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * @type {string} - */ - tokenUrl: null, - /** - * The URL for the DataONE echoCredentials() API - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNDiagnostic.echoCredentials) - * @type {string} - */ - checkTokenUrl: null, - /** - * The URL for the DataONE Identity API - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#module-CNIdentity) - * @type {string} - */ - accountsUrl: null, - /** - * The URL for the DataONE Pending Maps API - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNIdentity.getPendingMapIdentity) - * @type {string} - */ - pendingMapsUrl: null, - /** - * The URL for the DataONE mapIdentity() API - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNIdentity.mapIdentity) - * @type {string} - */ - accountsMapsUrl: null, - /** - * The URL for the DataONE Groups API - * This URL is constructed dynamically once the {@link AppModel} is initialized. - * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNIdentity.createGroup) - * @type {string} - */ - groupsUrl: null, - /** - * The URL for the DataONE metadata assessment service - * @type {string} - * @default "https://api.dataone.org/quality" - */ - mdqBaseUrl: "https://api.dataone.org/quality", - /** - * Metadata Assessment Suite IDs for the dataset assessment reports. - * @type {string[]} - * @default ["FAIR-suite-0.4.0"] - */ - mdqSuiteIds: ["FAIR-suite-0.4.0"], - /** - * Metadata Assessment Suite labels for the dataset assessment reports - * @type {string[]} - * @default ["FAIR Suite v0.4.0"] - */ - mdqSuiteLabels: ["FAIR Suite v0.4.0"], - /** - * Metadata Assessment Suite IDs for the aggregated assessment charts - * @type {string[]} - * @default ["FAIR-suite-0.4.0"] - */ - mdqAggregatedSuiteIds: ["FAIR-suite-0.4.0"], - /** - * Metadata Assessment Suite labels for the aggregated assessment charts - * @type {string[]} - * @default ["FAIR Suite v0.4.0"] - */ - mdqAggregatedSuiteLabels: ["FAIR Suite v0.4.0"], - /** - * The metadata formats for which to display metadata assessment reports - * @type {string[]} - * @default ["eml*", "https://eml*", "*isotc211*"] - */ - mdqFormatIds:["eml*", "https://eml*", "*isotc211*"], - - /** - * Metrics endpoint url - * @type {string} - */ - metricsUrl: 'https://logproc-stage-ucsb-1.test.dataone.org/metrics', - - /** - * Forwards collection Query to Metrics Service if enabled - * @type {boolean} - * @default true - */ - metricsForwardCollectionQuery: true, - - /** - * DataONE Citation reporting endpoint url - * @type {string} - */ - dataoneCitationsUrl: 'https://logproc-stage-ucsb-1.test.dataone.org/citations', - - /** - * Hide or show the report Citation button in the dataset landing page. - * @type {boolean} - * @default true - */ - hideReportCitationButton: false, - - /** - * Hide or show the aggregated citations chart in the StatsView. - * These charts are only available for DataONE Plus members or Hosted Repositories. - * (see https://dataone.org) - * @type {boolean} - * @default true - * @since 2.9.0 - */ - hideSummaryCitationsChart: true, - /** - * Hide or show the aggregated downloads chart in the StatsView - * These charts are only available for DataONE Plus members or Hosted Repositories. - * (see https://dataone.org) - * @type {boolean} - * @default true - * @since 2.9.0 - */ - hideSummaryDownloadsChart: true, - /** - * Hide or show the aggregated metadata assessment chart in the StatsView - * These charts are only available for DataONE Plus members or Hosted Repositories. - * (see https://dataone.org) - * @type {boolean} - * @default true - * @since 2.9.0 - */ - hideSummaryMetadataAssessment: true, - /** - * Hide or show the aggregated views chart in the StatsView - * These charts are only available for DataONE Plus members or Hosted Repositories. - * (see https://dataone.org) - * @type {boolean} - * @default true - * @since 2.9.0 - */ - hideSummaryViewsChart: true, - - /** - * Metrics flag for the Dataset Landing Page - * Enable this flag to enable metrics display - * @type {boolean} - * @default true - */ - displayDatasetMetrics: true, - - /** - * If true, displays the dataset metrics tooltips on the metrics buttons. - * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} - * @type {boolean} - * @default true - */ - displayDatasetMetricsTooltip: true, - /** - * If true, displays the datasets metric modal windows on the dataset landing page - * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} - * @type {boolean} - * @default true - */ - displayMetricModals: true, - /** - * If true, displays the dataset citation metrics on the dataset landing page - * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} - * @type {boolean} - * @default true - */ - displayDatasetCitationMetric: true, - /** - * If true, displays the dataset download metrics on the dataset landing page - * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} - * @type {boolean} - * @default true - */ - displayDatasetDownloadMetric: true, - /** - * If true, displays the dataset view metrics on the dataset landing page - * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} - * @type {boolean} - * @default true - */ - displayDatasetViewMetric: true, - /** - * If true, displays the citation registration tool on the dataset landing page - * @type {boolean} - * @default true - * @since 2.15.0 - */ - displayRegisterCitationTool: true, - /** - * If true, displays the "Edit" button on the dataset landing page - * @type {boolean} - * @default true - */ - displayDatasetEditButton: true, - /** - * If true, displays the metadata assessment metrics on the dataset landing page - * @type {boolean} - * @default false - */ - displayDatasetQualityMetric: false, - /** - * If true, displays the WholeTale "Analyze" button on the dataset landing page - * @type {boolean} - * @default false - */ - displayDatasetAnalyzeButton: false, - /** - * If true, displays various buttons on the dataset landing page for dataset owners - * @type {boolean} - * @default false - */ - displayDatasetControls: true, - /** Hide metrics display for SolrResult models that match the given properties. - * Properties can be functions, which are given the SolrResult model value as a parameter. - * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} - * @type {object} - * @example - * { - * formatId: "eml://ecoinformatics.org/eml-2.1.1", - * isPublic: true, - * dateUploaded: function(date){ - * return new Date(date) < new Date('1995-12-17T03:24:00'); - * } - * } - * // This example would hide metrics for any objects that are: - * // EML 2.1.1 OR public OR were uploaded before 12/17/1995. - */ - hideMetricsWhen: null, - - /** - * The bounding box path color to use in the Google Static Map images on the dataset landing pages. - * Specify the color either as a 24-bit (example: color=0xFFFFCC) or 32-bit hexadecimal value - * (example: color=0xFFFFCCFF), or from the set: black, brown, green, purple, yellow, blue, gray, orange, red, white. - * For more information, see the Google Statis Maps API docs: https://developers.google.com/maps/documentation/maps-static/start#PathStyles - * @type {string} - * @default "0xDA4D3Aff" (red) - * @since 2.13.0 - */ - datasetMapPathColor: "0xDA4D3Aff", - - /** - * The bounding box fill color to use in the Google Static Map images on the dataset landing pages. - * If you don't want to fill in the bounding boxes with a color, set this to null or undefined. - * Specify the color either as a 24-bit (example: color=0xFFFFCC) or 32-bit hexadecimal value - * (example: color=0xFFFFCCFF), or from the set: black, brown, green, purple, yellow, blue, gray, orange, red, white. - * For more information, see the Google Statis Maps API docs: https://developers.google.com/maps/documentation/maps-static/start#PathStyles - * @type {string} - * @default "0xFFFF0033" (light yellow) - * @since 2.13.0 - */ - datasetMapFillColor: "0xFFFF0033", - - /** - * The hue/color of the tiles drawn on the map when searching for data. - * This should be a three-digit hue degree between 0 and 360. (Try https://hslpicker.com) - * This is set on the {@link Map} model when it is initialized. - * @type {string} - * @default "192" (blue) - * @since 2.13.3 - */ - searchMapTileHue: "192", - - /** - * If true, the dataset landing pages will generate Schema.org-compliant JSONLD - * and insert it into the page. - * @type {boolean} - * @default true - */ - isJSONLDEnabled: true, - - /** - * If true, users can see a "Publish" button in the MetadataView, which makes the metadata - * document public and gives it a DOI identifier. - * If false, the button will be hidden completely. - * @type {boolean} - * @default true - */ - enablePublishDOI: true, - - /** - * A list of users or groups who exclusively will be able to see and use the "Publish" button, - * which makes the metadata document public and gives it a DOI identifier. - * Anyone not in this list will not be able to see the Publish button. - * `enablePublishDOI` must be set to `true` for this to take effect. - * @type {string[]} - */ - enablePublishDOIForSubjects: [], - - /** - * If true, users can change the AccessPolicy for any of their objects. - * This is equivalent to setting {@link AppConfig#allowAccessPolicyChangesPortals} and - * {@link AppConfig#allowAccessPolicyChangesDatasets} to `true`. - * @type {boolean} - * @default true - * @since 2.9.0 - */ - allowAccessPolicyChanges: true, - - /** - * If true, users can change the AccessPolicy for their portals only. - * @type {boolean} - * @default true - * @since 2.15.0 - */ - allowAccessPolicyChangesPortals: true, - - /** - * Limit portal Access policy editing to only a defined list of people or groups. - * To let everyone edit access policies for their own objects, keep this as an empty array - * and make sure {@link AppConfig#allowAccessPolicyChangesPortals} is set to `true` - * @type {boolean} - * @default [] - * @since 2.15.0 - */ - allowAccessPolicyChangesPortalsForSubjects: [], - - /** - * If true, users can change the AccessPolicy for their datasets only. - * @type {boolean} - * @default true - * @since 2.15.0 - */ - allowAccessPolicyChangesDatasets: true, - - /** - * Limit dataset Access policy editing to only a defined list of people or groups. - * To let everyone edit access policies for their own objects, keep this as an empty array - * and make sure {@link AppConfig#allowAccessPolicyChangesDatasets} is set to `true` - * @type {boolean} - * @default true - * @since 2.15.0 - */ - allowAccessPolicyChangesDatasetsForSubjects: [], - - /** - * The default {@link AccessPolicy} set on new objects uploaded to the repository. - * Each literal object here gets set directly on an {@link AccessRule} model. - * See the {@link AccessRule} list of default attributes for options on what to set here. - * @see {@link AccessRule} - * @type {object[]} - * @since 2.9.0 - * @default [{ - subject: "public", - read: true - }] - * @example - * [{ - * subject: "public", - * read: true - * }] - * // This example would assign public access to all new objects created in MetacatUI. - */ - defaultAccessPolicy: [{ - subject: "public", - read: true - }], - - /** - * When new data objects are added to a {@link DataPackage}, they can either inherit the {@link AccessPolicy} from the - * parent metadata object, or default to the {@link AppConfig#defaultAccessPolicy}. To inherit the {@link AccessPolicy} - * from the parent metadata object, set this config to `true`. - * @type {boolean} - * @default true - * @since 2.15.0 - */ - inheritAccessPolicy: true, - - /** - * The user-facing name for editing the Access Policy. This is displayed as the header of the AccessPolicyView, for example - * @type {string} - * @since 2.9.0 - * @default "Sharing options" - */ - accessPolicyName: "Sharing options", - - /** - * @type {object} - * @property {boolean} accessRuleOptions.read - If true, users will be able to give others read access to their DataONE objects - * @property {boolean} accessRuleOptions.write - If true, users will be able to give others write access to their DataONE objects - * @property {boolean} accessRuleOptions.changePermission - If true, users will be able to give others changePermission access to their DataONE objects - * @since 2.9.0 - * @default { - read: true, - write: true, - changePermission: true - } - * @example - * { - * read: true, - * write: true, - * changePermission: false - * } - * // This example would enable users to edit the read and write access to files, - * // but not change ownership, in the Access Policy View. - */ - accessRuleOptions: { - read: true, - write: true, - changePermission: true - }, - - /** - * @type {object} - * @property {boolean} accessRuleOptionNames.read - The user-facing name of the "read" access in Access Rules - * @property {boolean} accessRuleOptionNames.write - The user-facing name of the "write" access in Access Rules - * @property {boolean} accessRuleOptionNames.changePermission - The user-facing name of the "changePermission" access in Access Rules - * @since 2.9.0 - * @example - * { - * read: "Can view", - * write: "Can edit", - * changePermission: "Is owner" - * } - */ - accessRuleOptionNames: { - read: "Can view", - write: "Can edit", - changePermission: "Is owner" - }, - - /** - * If false, the rightsHolder of a resource will not be displayed in the AccessPolicyView. - * @type {boolean} - * @default true - * @since 2.9.0 - */ - displayRightsHolderInAccessPolicy: true, - - /** - * If false, users will not be able to change the rightsHolder of a resource in the AccessPolicyView - * @type {boolean} - * @default true - * @since 2.9.0 - */ - allowChangeRightsHolder: true, - - /** - * A list of group subjects that will be hidden in the AccessPolicy view to - * everyone except those in the group. This is useful for preventing users from - * removing repository administrative groups from access policies. - * @type {string[]} - * @since 2.9.0 - * @example ["CN=data-admin-group,DC=dataone,DC=org"] - */ - hiddenSubjectsInAccessPolicy: [], - - /** - * The format ID the portal editor serializes a new portal document as - * @type {string} - * @default "https://purl.dataone.org/portals-1.1.0" - * @readonly - * @since 2.17.0 - */ - portalEditorSerializationFormat: "https://purl.dataone.org/portals-1.1.0", - - /** - * If true, the public/private toggle will be displayed in the Sharing Options for portals. - * @type {boolean} - * @default true - * @since 2.9.0 - */ - showPortalPublicToggle: true, - - /** - * The public/private toggle will be displayed in the Sharing Options for portals for only - * the given users or groups. To display the public/private toggle for everyone, - * set `showPortalPublicToggle` to true and keep this array empty. - * @type {string[]} - * @since 2.9.0 - */ - showPortalPublicToggleForSubjects: [], - - /** - * If true, the public/private toggle will be displayed in the Sharing Options for datasets. - * @type {boolean} - * @default true - * @since 2.9.0 - */ - showDatasetPublicToggle: true, - - /** - * The public/private toggle will be displayed in the Sharing Options for datasets for only - * the given users or groups. To display the public/private toggle for everyone, - * set `showDatasetPublicToggle` to true and keep this array empty. - * @type {string[]} - * @since 2.15.0 - */ - showDatasetPublicToggleForSubjects: [], - - /** - * Set to false to hide the display of "My Portals", which shows the user's current portals - * @type {boolean} - * @default true - */ - showMyPortals: true, - /** - * The user-facing term for portals in lower-case and in singular form. - * e.g. "portal" - * @type {string} - * @default "portal" - */ - portalTermSingular: "portal", - /** - * The user-facing term for portals in lower-case and in plural form. - * e.g. "portals". This allows for portal terms with irregular plurals. - * @type {string} - * @default "portals" - */ - portalTermPlural: "portals", - /** - * A URL of a webpage for people to learn more about portals. If no URL is provided, - * links to more info about portals will be omitted. - * @since 2.14.0 - * @type {string} - * @example "https://dataone.org/plus" - * @default null - */ - portalInfoURL: null, - /** - * The URL for a webpage where people can learn more about custom portal search - * filters. If no URL is provided, links to more info about portals will be omitted. - * @since 2.17.0 - * @type {string} - * @example "https://dataone.org/custom-search" - * @default null - */ - portalSearchFiltersInfoURL: null, - /** - * Set to false to prevent ANYONE from creating a new portal. - * @type {boolean} - * @default true - */ - enableCreatePortals: true, - /** - * Limits only the following people or groups to create new portals. If this is left as an empty array, - * then any logged-in user can create a portal. - * @type {string[]} - */ - limitPortalsToSubjects: [], - - /** - * This message will display when a user tries to create a new Portal in the PortalEditor - * when they are not associated with a whitelisted subject in the `limitPortalsToSubjects` list - * @type {string} - */ - portalEditNotAuthCreateMessage: "You have not been authorized to create new portals. Please contact us with any questions.", - - /** - * This message will display when a user tries to access the Portal Editor for a portal - * for which they do not have write permission. - * @type {string} - */ - portalEditNotAuthEditMessage: "The portal owner has not granted you permission to edit this portal. Please contact the owner to be given edit permission.", - - /** - * This message will display when a user tries to create a new portal when they have exceeded their DataONE portal quota - * @type {string} - */ - portalEditNoQuotaMessage: "You have already reached the maximum number of portals for your membership level.", - - /** - * This message will display when there is any non-specific error during the save process of the PortalEditor. - * @type {string} - */ - portalEditSaveErrorMsg: "Something went wrong while attempting to save your changes.", - - /** - * The list of fields that should be required in the portal editor. - * Set individual properties to `true` to require them in the portal editor. - * @type {object} - * @property {boolean} label - Default: true - * @property {boolean} name - Default: true - * @property {boolean} description - Default: false - * @property {boolean} sectionTitle - Default: true - * @property {boolean} sectionIntroduction - Default: false - * @property {boolean} logo - Default: false - */ - portalEditorRequiredFields: { - label: true, - name: true, - description: false, - sectionTitle: true, - sectionIntroduction: false, - logo: false, - //The following fields are not yet supported as required fields in the portal editor - //TODO: Add support for requiring the below fields - sectionImage: false, - acknowledgments: false, - acknowledgmentsLogos: false, - awards: false, - associatedParties: false - }, - - /** - * A list of portals labels that no one should be able to create portals with - * @type {string[]} - * @readonly - * @since 2.11.3 - */ - portalLabelBlockList: [ - "Dataone", - 'urn:node:CN', 'CN', 'cn', - 'urn:node:CNUNM1', 'CNUNM1', 'cn-unm-1', - 'urn:node:CNUCSB1', 'CNUCSB1', 'cn-ucsb-1', - 'urn:node:CNORC1', 'CNORC1', 'cn-orc-1', - 'urn:node:KNB', 'KNB', 'KNB Data Repository', - 'urn:node:ESA', 'ESA', 'ESA Data Registry', - 'urn:node:SANPARKS', 'SANPARKS', 'SANParks Data Repository', - 'urn:node:ORNLDAAC', 'ORNLDAAC', 'ORNL DAAC', - 'urn:node:LTER', 'LTER', 'U.S. LTER Network', - 'urn:node:CDL', 'CDL', 'UC3 Merritt', - 'urn:node:PISCO', 'PISCO', 'PISCO MN', - 'urn:node:ONEShare', 'ONEShare', 'ONEShare DataONE Member Node', - 'urn:node:mnORC1', 'mnORC1', 'DataONE ORC Dedicated Replica Server', - 'urn:node:mnUNM1', 'mnUNM1', 'DataONE UNM Dedicated Replica Server', - 'urn:node:mnUCSB1', 'mnUCSB1', 'DataONE UCSB Dedicated Replica Server', - 'urn:node:TFRI', 'TFRI', 'TFRI Data Catalog', - 'urn:node:USANPN', 'USANPN', 'USA National Phenology Network', - 'urn:node:SEAD', 'SEAD', 'SEAD Virtual Archive', - 'urn:node:GOA', 'GOA', 'Gulf of Alaska Data Portal', - 'urn:node:KUBI', 'KUBI', 'University of Kansas - Biodiversity Institute', - 'urn:node:LTER_EUROPE', 'LTER_EUROPE', 'LTER Europe Member Node', - 'urn:node:DRYAD', 'DRYAD', 'Dryad Digital Repository', - 'urn:node:CLOEBIRD', 'CLOEBIRD', 'Cornell Lab of Ornithology - eBird', - 'urn:node:EDACGSTORE', 'EDACGSTORE', 'EDAC Gstore Repository', - 'urn:node:IOE', 'IOE', 'Montana IoE Data Repository', - 'urn:node:US_MPC', 'US_MPC', 'Minnesota Population Center', - 'urn:node:EDORA', 'EDORA', 'Environmental Data for the Oak Ridge Area (EDORA)', - 'urn:node:RGD', 'RGD', 'Regional and Global biogeochemical dynamics Data (RGD)', - 'urn:node:GLEON', 'GLEON', 'GLEON Data Repository', - 'urn:node:IARC', 'IARC', 'IARC Data Archive', - 'urn:node:NMEPSCOR', 'NMEPSCOR', 'NM EPSCoR Tier 4 Node', - 'urn:node:TERN', 'TERN', 'TERN Australia', - 'urn:node:NKN', 'NKN', 'Northwest Knowledge Network', - 'urn:node:USGS_SDC', 'USGS_SDC', 'USGS Science Data Catalog', - 'urn:node:NRDC', 'NRDC', 'NRDC DataONE member node', - 'urn:node:NCEI', 'NCEI', 'NOAA NCEI Environmental Data Archive', - 'urn:node:PPBIO', 'PPBIO', 'PPBio', - 'urn:node:NEON', 'NEON', 'NEON Member Node', - 'urn:node:TDAR', 'TDAR', 'The Digital Archaeological Record', - 'urn:node:ARCTIC', 'ARCTIC', 'Arctic Data Center', - 'urn:node:BCODMO', 'BCODMO', 'Biological and Chemical Oceanography Data Management Office (BCO-DMO) ', - 'urn:node:GRIIDC', 'GRIIDC', 'Gulf of Mexico Research Initiative Information and Data Cooperative (GRIIDC)', - 'urn:node:R2R', 'R2R', 'Rolling Deck to Repository (R2R)', - 'urn:node:EDI', 'EDI', 'Environmental Data Initiative', - 'urn:node:UIC', 'UIC', 'A Member Node for University of Illinois at Chicago.', - 'urn:node:RW', 'RW', 'Research Workspace', - 'urn:node:FEMC', 'FEMC', 'Forest Ecosystem Monitoring Cooperative Member Node', - 'urn:node:OTS_NDC', 'OTS_NDC', 'Organization for Tropical Studies - Neotropical Data Center', - 'urn:node:PANGAEA', 'PANGAEA', 'PANGAEA', - 'urn:node:ESS_DIVE', 'ESS_DIVE', 'ESS-DIVE: Deep Insight for Earth Science Data', - 'urn:node:CAS_CERN', 'CAS_CERN', 'Chinese Ecosystem Research Network (CERN)', - 'urn:node:FIGSHARE_CARY', 'FIGSHARE_CARY', 'Cary Institute of Ecosystem Studies (powered by Figshare)', - 'urn:node:IEDA_EARTHCHEM', 'IEDA_EARTHCHEM', 'IEDA EARTHCHEM', - 'urn:node:IEDA_USAP', 'IEDA_USAP', 'IEDA USAP', - 'urn:node:IEDA_MGDL', 'IEDA_MGDL', 'IEDA MGDL', - 'urn:node:METAGRIL', 'METAGRIL', 'metaGRIL', - 'urn:node:ARM', 'ARM', 'ARM - Atmospheric Radiation Measurement Research Facility', - "urn:node:CA_OPC", "CA_OPC", "OPC", - "urn:node:TNC_DANGERMOND", "dangermond", "TNC_DANGERMOND", "dangermondpreserve" - ], - - /** - * Limit users to a certain number of portals. This limit will be ignored if {@link AppConfig#enableBookkeeperServices} - * is set to true, because the limit will be enforced by Bookkeeper Quotas instead. - * @type {number} - * @default 100 - * @since 2.14.0 - */ - portalLimit: 100, - - /** - * The default values to use in portals. Default sections are applied when a portal is new. - * Default images are used in new freeform pages in the portal builder. - * The default colors are used when colors haven't been saved to the portal document. - * Colors can be hex codes, rgb codes, or any other form supported by browsers in CSS - * @type {object} - * @property {object[]} sections The default sections for a new portal. Each object within the section array can have a title property and a label property - * @property {string} label The name of the section that will appear in the tab - * @property {string} title A longer title for the section that will appear in the section header - * @property {string} newPortalActiveSectionLabel When a user start the portal builder for a brand new portal, the label for the section that the builder should start on. Can be set to "Data", "Metrics", "Settings", or one of the labels from the default sections described above. - * @property {string[]} sectionImageIdentifiers A list of image pids to use as default images for new markdown sections - * @property {string} primaryColor The color that is used most frequently in the portal view - * @property {string} secondaryColor The color that is used second-most frequently in the portal view - * @property {string} accentColor The color that is rarely used in portal views as an accent color - * @property {string} primaryColorTransparent An rgba() version of the primaryColor that is semi-transparent - * @property {string} secondaryColorTransparent An rgba() version of the secondaryColor that is semi-transparent - * @property {string} accentColorTransparent An rgba() version of the accentColor that is semi-transparent - * @example { - * sections: [ - * { label: "About", - * title: "About our project" - * }, - * { label: "Publications", - * title: "Selected publications by our lab group" - * } - * ], - * newPortalActiveSectionLabel: "About", - * sectionImageIdentifiers: ["urn:uuid:d2f31a83-debf-4d78-bef7-6abe20962581", "urn:uuid:6ad37acd-d0ac-4142-9f42-e5f05ff55564", "urn:uuid:0b6be09f-2e6f-4e7b-a83c-2823495f9608", "urn:uuid:5b4e0347-07ed-4580-b039-6c4df57ed801", "urn:uuid:0cf62da9-a099-440e-9c1e-595a55c0d60d"], - * primaryColor: "#16acc0", - * primaryColorTransparent: "rgba(22, 172, 192, .7)", - * secondaryColor: "#EED268", - * secondaryColorTransparent: "rgba(238, 210, 104, .7)", - * accentColor: "#0f5058", - * accentColorTransparent: "rgba(15, 80, 88, .7)" - * } - * @since 2.14.0 - */ - portalDefaults: { - }, - - /** - * Add an API service URL that retrieves projects data. This is an optional - * configuration in case the memberNode have a third-party service that provides - * their projects information. - * - * If the configuration is not set, set the default projects list in the views using it. - * - * @type {string} - * @private - * @since 2.20.0 #TODO Update version here. - */ - projectsApiUrl: undefined, - /** - * Enable or disable the use of Fluid Earth Viewer visualizations in portals. - * This config option is marked as `private` since this is an experimental feature. - * @type {boolean} - * @private - * @since 2.13.4 - */ - enableFeverVisualizations: false, - /** - * The relative path to the location where the Fluid Earth Viewer (FEVer) is deployed. This should be - * deployed at the same origin as MetacatUI, since your web server configuration and many browsers - * may block iframes from different origins. - * This config option is marked as `private` since this is an experimental feature. - * @type {string} - * @private - * @since 2.13.4 - */ - feverPath: "/fever", - /** - * The full URL to the location where the Fluid Earth Viewer (FEVer) is deployed. - * This URL is constructed during {@link AppModel#initialize} using the {@link AppConfig#baseUrl} - * and {@link AppConfig#feverPath}. - * This config option is marked as `private` since this is an experimental feature. - * @type {string} - * @readonly - * @private - * @since 2.13.4 - */ - feverUrl: "", - - /** If true, then archived content is available in the search index. - * Set to false if this MetacatUI is using a Metacat version before 2.10.0 - * @type {boolean} - * @default true - */ - archivedContentIsIndexed: true, - - /** - * The metadata fields to hide when a user is creating a collection definition using - * the Query Builder View displayed in the portal builder on the data page, or - * anywhere else the EditCollectionView is displayed. Strings listed here should - * exactly match the 'name' for each field provided by the DataONE search index API - * (i.e. should match the Solr field). - * @example ["sem_annotated_by", "mediaType"] - * @type {string[]} - */ - collectionQueryExcludeFields: [ - "sem_annotated_by", "sem_annotates", "sem_comment", "pubDate", - "namedLocation", "contactOrganization", "investigator", "originator", - "originatorText", "serviceInput", - "authorGivenName", "authorSurName", "topic", "webUrl", "_root_", - "collectionQuery", "geohash_1", "geohash_2", "geohash_3", "geohash_4", - "geohash_5", "geohash_6", "geohash_7", "geohash_8", "geohash_9", "label", - "LTERSite", "_version_", "checksumAlgorithm", "keywords", - "parameterText", "project", "topicText", "dataUrl", "fileID", - "isDocumentedBy", "logo", "obsoletes", "origin", "funding", "formatType", - "obsoletedBy", "presentationCat", "mediaType", "mediaTypeProperty", - "relatedOrganizations", "noBoundingBox", "decade", "hasPart", "sensorText", - "sourceText", "termText", "titlestr", "site", "id", "updateDate", - "edition", "gcmdKeyword", "isSpatial", "keyConcept", "ogcUrl", "parameter", - "sensor", "source", "term", "investigatorText", "sku", "_text_", - // Fields that have been made into a special combination field - "beginDate", "endDate", "awardNumber", - // Provenance fields (keep only "prov_hasSources" and "prov_hasDerivations"), - // since they are the only ones indexed on metadata objects - "prov_wasGeneratedBy", "prov_generated", "prov_generatedByExecution", - "prov_generatedByProgram", "prov_generatedByUser", "prov_instanceOfClass", - "prov_used", "prov_usedByExecution", "prov_usedByProgram", "prov_usedByUser", - "prov_wasDerivedFrom", "prov_wasExecutedByExecution", "prov_wasExecutedByUser", - "prov_wasInformedBy" - ], - - /** - * A special field is one that does not exist in the query service index (i.e. - * Solr). It can be a combination of fields that are presented to the user as a - * single field, but which are added to the model as multiple fields. It can also be - * a duplicate of a field that does exist, but presented with a different label (and - * even with different {@link operatorOptions operator options} or - * {@link valueSelectUImap value input} if needed). - * - * @typedef {Object} SpecialField - * @property {string} name - A unique ID to represent this field. It must not match - * the name of any other query fields. - * @property {string[]} fields - The list of real query fields that this abstracted - * field should represent. The query fields listed must exactly match the names of - * the query fields that are retrieved from the query service. - * @property {string} label - A user-facing label to display. - * @property {string} description - A description for this field. - * @property {string} category - The name of the category under which to place this - * field. It must match one of the category names for an existing query field set in - * {@link QueryField#categoriesMap}. - * @property {string[]} [values] - An optional list of filter values. If set, this - * is used to determine whether a pre-existing Query Rule should be displayed as one - * of these special fields, or as a field from the query API. Setting values means - * that the values set on the Query Rule model must exactly match the values set. - * - * @since 2.15.0 - */ - - /** - * A list of additional fields which are not retrieved from the query API (i.e. are - * not Solr fields), but which should be added to the list of options the user can - * select from when building a query in the EditCollectionView. This can be used to - * add abstracted fields which are a combination of multiple query fields, or to add - * a duplicate field that has a different label. - * - * @type {SpecialField[]} - * - * @since 2.15.0 - */ - collectionQuerySpecialFields: [ - { - name: "documents-special-field", - fields: ["documents"], - label: "Contains Data Files", - description: "Limit results to packages that include data files. Without" + - " this rule, results may include packages with metadata but no data.", - category: "General", - values: ["*"] - }, - { - name: "year-data-collection", - fields: ["beginDate", "endDate"], - label: "Year of Data Collection", - description: "The temporal range of content described by the metadata", - category: "Dates" - }, - { - name: "funding-text-award-number", - fields: ["fundingText", "awardNumber"], - label: "Award Number", - description: "The award number for funding associated with a dataset or the " + - "description of funding source", - category: "Awards & funding" - } - ], - - /** - * The names of the query fields that use an object identifier as a value. Filter - * models that use one of these fields are handled specially when building query - * strings - they are OR'ed at the end of queries. They are also given an "OR" - * operator and fieldsOperator attribute when parsed. - * @type {string[]} - * - * @since 2.17.0 - */ - queryIdentifierFields: ["id", "identifier", "seriesId", "isPartOf"], - - /** - * The name of the query fields that specify latitude. Filter models that these - * fields are handled specially, since they must be a float value and have a - * pre-determined minRange and maxRange (-90 to 90). - */ - queryLatitudeFields: ["northBoundCoord", "southBoundCoord"], - - /** - * The name of the query fields that specify longitude. Filter models that these - * fields are handled specially, since they must be a float value and have a - * pre-determined minRange and maxRange (-180 to 180). - */ - queryLongitudeFields: ["eastBoundCoord", "westBoundCoord"], - - /** - * The names of the query fields that may require special treatment in the - * UI. For example, upgrade the view for a Filter from a FilterView to - * a SemanticFilterView or to block certain UIBuilders in FilterEditorView - * that don't make sense for a semantic field. - * - * @type {string[]} - * @since 2.22.0 - */ - querySemanticFields: ["sem_annotation"], - - /** - * The isPartOf filter is added to all new portals built in the Portal - * Builder automatically. It is required for dataset owners to include - * their dataset in a specific portal collection. By default, this filter - * is hidden. Set to false to make this filter visible. - * @type {boolean} + * intellectualRights: true, + * keywordSets: false, + * methods: false, + * samplingDescription: false, + * studyExtentDescription: false, + * temporalCoverage: false, + * title: true, + * contact: true, + * principalInvestigator: true + * } */ - hideIsPartOfFilter: true, + emlEditorRequiredFields: { + abstract: true, + alternateIdentifier: false, + dataSensitivity: false, + funding: false, + generalTaxonomicCoverage: false, + taxonCoverage: false, + geoCoverage: false, + intellectualRights: true, + keywordSets: false, + methods: false, + samplingDescription: false, + studyExtentDescription: false, + temporalCoverage: false, + title: true, + creator: true, + contact: true, + }, - /** - * The default {@link FilterGroup}s to use in the data catalog search ({@link CatalogSearchView}). - * This is an array of literal objects that will be directly set on the {@link FilterGroup} models. Refer to the {@link FilterGroup#defaults} for - * options. - * @type {FilterGroup#defaults[]} + /** + * A list of required fields for each EMLParty (People) in the dataset editor. + * This is a literal object where the keys are the EML Party type (e.g. creator, principalInvestigator) {@link see EMLParty.partytypes} + * and the values are arrays of field names. + * By default, EMLPartys are *always* required to have an individual's name, position name, or organization name. + * @type {object} + * @since 2.21.0 + * @example + * { + * contact: ["email"], + * creator: ["email", "address", "phone"] + * principalInvestigator: ["organizationName"] + * } + * @default + * { + * } + */ + emlEditorRequiredFields_EMLParty: {}, + + /** + * An array of science metadata format IDs that are editable in MetacatUI. + * Metadata documents with these format IDs will have an Edit button and will be + * editable in the Editor Views. + * This should only be changed if you have extended MetacatUI to edit a new format, + * or if you want to disable editing of a specific format ID. + * @type {string[]} + * @default [ + "eml://ecoinformatics.org/eml-2.1.1", + "https://eml.ecoinformatics.org/eml-2.2.0" + ] + * @example + * [ + * "eml://ecoinformatics.org/eml-2.1.1", + * "https://eml.ecoinformatics.org/eml-2.2.0" + * ] + * @readonly + */ + editableFormats: [ + "eml://ecoinformatics.org/eml-2.1.1", + "https://eml.ecoinformatics.org/eml-2.2.0", + ], + + /** + * The format ID the dataset editor serializes new EML as + * @type {string} + * @default "https://eml.ecoinformatics.org/eml-2.2.0" + * @readonly + * @since 2.13.0 + */ + editorSerializationFormat: "https://eml.ecoinformatics.org/eml-2.2.0", + + /** + * The XML schema location the dataset editor will use when creating new EML. This should + * correspond with {@link AppConfig#editorSerializationFormat} + * @type {string} + * @default "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd" + * @readonly + * @since 2.13.0 + */ + editorSchemaLocation: + "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd", + + /** + * The text to use for the eml system attribute. The system attribute + * indicates the data management system within which an identifier is in + * scope and therefore unique. This is typically a URL (Uniform Resource + * Locator) that indicates a data management system. All identifiers that + * share a system must be unique. In other words, if the same identifier + * is used in two locations with identical systems, then by definition the + * objects at which they point are in fact the same object. + * @type {string} + * @since 2.26.0 + * @link https://eml.ecoinformatics.org/schema/eml-resource_xsd#SystemType + * @link https://eml.ecoinformatics.org/schema/eml_xsd + */ + emlSystem: "knb", + + /** + * This error message is displayed when the Editor encounters an error saving + * @type {string} + */ + editorSaveErrorMsg: "Not all of your changes could be submitted.", + /** + * This error message is displayed when the Editor encounters an error saving, and a plain-text draft is saved instead + * @type {string} + */ + editorSaveErrorMsgWithDraft: + "Not all of your changes could be submitted, but a draft " + + "has been saved which can be accessed by our support team. Please contact us.", + /** + * The text of the Save button in the dataset editor. + * @type {string} + * @default "Save dataset" + * @since 2.13.3 + */ + editorSaveButtonText: "Save dataset", + + /** + * A list of keyword thesauri options for the user to choose from in the EML Editor. + * A "None" option will also always display. + * @type {object[]} + * @property {string} label - A readable and short label for the keyword thesaurus that is displayed in the UI + * @property {string} thesaurus - The exact keyword thesaurus name that will be saved in the EML + * @since 2.10.0 + * @default [{ + label: "GCMD", + thesaurus: "NASA Global Change Master Directory (GCMD)" + }] + * @example + * [{ + * label: "GCMD", + * thesaurus: "NASA Global Change Master Directory (GCMD)" + * }] */ - defaultFilterGroups: [ - { - label: "", - filters: [ + emlKeywordThesauri: [ { - fields: ["attribute"], - label: "Data attribute", - placeholder: "density, length, etc.", - icon: "table", - description: "Measurement type, e.g. density, temperature, species" + label: "GCMD", + thesaurus: "NASA Global Change Master Directory (GCMD)", }, + ], + + /** + * If true, questions related to Data Sensitivity will be shown in the EML Editor. + * @type {boolean} + * @default true + * @since 2.19.0 + */ + enableDataSensitivityInEditor: true, + + /** + * The URL of a webpage that shows more information about Data Sensitivity and DataTags. This will be used + * for links in help text throughout the app, such as next to Data Sensitivity questions in the dataset editor. + * + * @type {string} + * @default "http://datatags.org" + * @since 2.19.0 + */ + dataSensitivityInfoURL: "http://datatags.org", + + /** + * In the editor, sometimes it is useful to have guided questions for the Methods section + * in addition to the generic numbered method steps. These custom methods are defined here + * as an array of literal objects that define each custom Methods question. Custom methods + * are serialized to the EML as regular method steps, but with an unchangeable title, defined here, + * in order to identify them. + * + * @typedef {object} CustomEMLMethod + * @property {string[]} titleOptions One or more titles that may exist in an EML Method Step that identify that Method Step as a custom method type. THe first title in the array is serialized to the EML XML. + * @property {string} id A unique identifier for this custom method type. + * @property {boolean} required If true, this custom method will be a required field for submission in the EML editor. + * @example [{ + "titleOptions": ["Ethical Research Procedures"], + "id": "ethical-research-procedures", + "required": false + }] + * @since 2.19.0 + */ + /** + * In the editor, sometimes it is useful to have guided questions for the Methods section + * in addition to the generic numbered method steps. These custom methods are defined here + * as an array of literal objects that define each custom Methods question. Custom methods + * are serialized to the EML as regular method steps, but with an unchangeable title, defined here, + * in order to identify them. + * @type {CustomEMLMethod} + * @since 2.19.0 + */ + customEMLMethods: [], + + /** + * Configuration options for a drop down list of taxa. + * @typedef {object} AppConfig#quickAddTaxaList + * @type {Object} + * @property {string} label - The label for the dropdown menu + * @property {string} placeholder - The placeholder text for the input field + * @property {EMLTaxonCoverage#taxonomicClassification[]} taxa - The list of taxa to show in the dropdown menu + * @example + * { + * label: "Primates", + * placeholder: "Select one or more primates", + * taxa: [ + * { + * commonName: "Bonobo", + * taxonRankName: "Species", + * taxonRankValue: "Pan paniscus", + * taxonId: { + * provider: "ncbi", + * value: "9597" + * } + * }, + * { + * commonName: "Chimpanzee", + * ... + * }, + * ... + * } + * @since 2.24.0 + */ + + /** + * A list of taxa to show in the Taxa Quick Add section of the EML editor. + * This can be used to expedite entry of taxa that are common in the + * repository's domain. The quickAddTaxa is a list of objects, each + * defining a separate dropdown interface. This way, common taxa can + * be grouped together. + * Alternative, provide a SID for a JSON data object that is stored in the + * repository. The JSON must be in the same format as required for this + * configuration option. + * @since 2.24.0 + * @type {AppConfig#quickAddTaxaList[] | string} + * @example + * [ + * { + * label: "Bats" + * placeholder: "Select one or more bats", + * taxa: [ ... ] + * }, + * { + * label: "Birds" + * placeholder: "Select one or more birds", + * taxa: [ ... ] + * } + * ] + */ + quickAddTaxa: [], + + /** + * The base URL for the repository. This only needs to be changed if the repository + * is hosted at a different origin than the MetacatUI origin. This URL is used to contruct all + * of the DataONE REST API URLs. If you are testing MetacatUI against a development repository + * at an external location, this is where you would set that external repository URL. + * @type {string} + * @default window.location.origin || (window.location.protocol + "//" + window.location.host) + */ + baseUrl: + window.location.origin || + window.location.protocol + "//" + window.location.host, + + /** + * The directory that metacat is installed in at the `baseUrl`. For example, if you + * have metacat installed in the tomcat webapps directory as `metacat`, then this should be set + * to "/metacat". Or if you renamed the metacat webapp to `catalog`, then it should be `/catalog`. + * @type {string} + * @default "/metacat" + */ + context: MetacatUI.AppConfig.metacatContext || "/metacat", + + /** + * The URL fragment for the DataONE Member Node (MN) API. + * @type {string} + * @default '/d1/mn/v2' + */ + d1Service: "/d1/mn/v2", + /** + * The base URL of the DataONE Coordinating Node (CN). CHange this if you + * are testing a deployment in a development environment. + * @type {string} + * @default "https://cn.dataone.org" + * @example "https://cn-stage.test.dataone.org" + */ + d1CNBaseUrl: "https://cn.dataone.org", + /** + * The URL fragment for the DataONE Coordinating Node (CN) API. + * @type {string} + * @default '/cn/v2' + */ + d1CNService: "/cn/v2", + /** + * The URL for the DataONE Search MetacatUI. This only needs to be changed + * if you want to point to a development environment. + * @type {string} + * @default "https://search.dataone.org" + * @readonly + * @since 2.13.0 + */ + dataoneSearchUrl: "https://search.dataone.org", + /** + * The URL for the DataONE listNodes() API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNCore.listNodes) + * @type {string} + */ + nodeServiceUrl: null, + /** + * The URL for the DataONE View API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#module-MNView) + * @type {string} + */ + viewServiceUrl: null, + /** + * The URL for the DataONE getPackage() API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNPackage.getPackage) + * + * @type {string} + */ + packageServiceUrl: null, + /** + * The URL for the Metacat Publish service. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * @type {string} + */ + publishServiceUrl: null, + /** + * The URL for the DataONE isAuthorized() API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNAuthorization.isAuthorized) + * @type {string} + */ + authServiceUrl: null, + /** + * The URL for the DataONE query API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNQuery.query) + * @type {string} + */ + queryServiceUrl: null, + /** + * The URL for the DataONE reserveIdentifier() API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNCore.reserveIdentifier) + * @type {string} + */ + reserveServiceUrl: null, + /** + * The URL for the DataONE system metadata API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNRead.getSystemMetadata + * and https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNStorage.updateSystemMetadata) + * @type {string} + */ + metaServiceUrl: null, + /** + * The URL for the DataONE system metadata API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNRead.getSystemMetadata + * and https://releases.dataone.org/online/api-documentation-v2.0/apis/MN_APIs.html#MNStorage.updateSystemMetadata) + * @type {string} + */ + objectServiceUrl: null, + /** + * The URL for the DataONE Formats API. This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNCore.listFormats) + * @type {string} + */ + formatsServiceUrl: null, + /** + * The URL fragment for the DataONE Formats API. This is combined with the AppConfig#formatsServiceUrl + * @type {string} + * @default "/formats" + */ + formatsUrl: "/formats", + + /** + * If true, parts of the UI (most notably, "funding" field in the dataset editor) + * may look up NSF Award information + * @type {boolean} + * @default false + */ + useNSFAwardAPI: false, + /** + * The URL for the NSF Award API, which can be used by the {@link LookupModel} + * to look up award information for the dataset editor or other views. The + * URL must point to a proxy that can make requests to the NSF Award API, + * since it does not support CORS. + * @type {string} + * @default "/research.gov/awardapi-service/v1/awards.json" + */ + grantsUrl: "/research.gov/awardapi-service/v1/awards.json", + + /** + * The base URL for the ORCID REST services + * @type {string} + * @default "https:/orcid.org" + */ + orcidBaseUrl: "https:/orcid.org", + + /** + * The URL for the ORCID search API, which can be used to search for information + * about people using their ORCID, email, name, etc. + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * @type {string} + */ + orcidSearchUrl: null, + + /** + * The URL for the Metacat API. The Metacat API has been deprecated and is kept here + * for compatability with Metacat repositories that are using the old x509 certificate + * authentication mechanism. This is deprecated since authentication is now done via + * the DataONE Portal service using auth tokens. (Using the {@link AppConfig#tokenUrl}) + * This URL is contructed dynamically when the AppModel is initialized. + * Only override this if you are an advanced user and have a reason to! + * @type {string} + */ + metacatServiceUrl: null, + + /** + * If false, the /monitor/status (the service that returns the status of various DataONE services) will not be used. + * @type {boolean} + * @default true + * @since 2.9.0 + */ + enableMonitorStatus: true, + + /** + * The URL for the service that returns the status of various DataONE services. + * The only supported status so far is the search index queue -- the number of + * objects that are waiting to be indexed in the Solr search index. + * This URL is contructed dynamically when the + * AppModel is initialized. Only override this if you are an advanced user and have a reason to! + * @type {string} + * @since 2.9.0 + */ + monitorStatusUrl: "", + + /** + * If true, users will see a page with sign-in troubleshooting tips + * @type {boolean} + * @default true + * @since 2.13.3 + */ + showSignInHelp: true, + /** + * If true, users can sign in using CILogon as the identity provider. + * ORCID is the only recommended identity provider. CILogon may be deprecated + * in the future. + * @type {boolean} + * @default false + */ + enableCILogonSignIn: false, + /** + * The URL for the DataONE Sign In API using CILogon as the identity provider + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * @type {string} + */ + signInUrl: null, + /** + * The URL for the DataONE Sign Out API + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * @type {string} + */ + signOutUrl: null, + /** + * The URL for the DataONE Sign In API using ORCID as the identity provider + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * @type {string} + */ + signInUrlOrcid: null, + + /** + * Enable DataONE LDAP authentication. If true, users can sign in from an LDAP account that is in the DataONE CN LDAP directory. + * This is not recommended, as DataONE is moving towards supporting only ORCID logins for users. + * This LDAP authentication is separate from the File-based authentication for the Metacat Admin interface. + * @type {boolean} + * @default false + * @since 2.11.0 + */ + enableLdapSignIn: false, + /** + * The URL for the DataONE Sign In API using LDAP as the identity provider + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * @type {string} + */ + signInUrlLdap: null, + + /** + * The URL for the DataONE Token API using ORCID as the identity provider + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * @type {string} + */ + tokenUrl: null, + /** + * The URL for the DataONE echoCredentials() API + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNDiagnostic.echoCredentials) + * @type {string} + */ + checkTokenUrl: null, + /** + * The URL for the DataONE Identity API + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#module-CNIdentity) + * @type {string} + */ + accountsUrl: null, + /** + * The URL for the DataONE Pending Maps API + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNIdentity.getPendingMapIdentity) + * @type {string} + */ + pendingMapsUrl: null, + /** + * The URL for the DataONE mapIdentity() API + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNIdentity.mapIdentity) + * @type {string} + */ + accountsMapsUrl: null, + /** + * The URL for the DataONE Groups API + * This URL is constructed dynamically once the {@link AppModel} is initialized. + * (see https://releases.dataone.org/online/api-documentation-v2.0/apis/CN_APIs.html#CNIdentity.createGroup) + * @type {string} + */ + groupsUrl: null, + /** + * The URL for the DataONE metadata assessment service + * @type {string} + * @default "https://api.dataone.org/quality" + */ + mdqBaseUrl: "https://api.dataone.org/quality", + /** + * Metadata Assessment Suite IDs for the dataset assessment reports. + * @type {string[]} + * @default ["FAIR-suite-0.4.0"] + */ + mdqSuiteIds: ["FAIR-suite-0.4.0"], + /** + * Metadata Assessment Suite labels for the dataset assessment reports + * @type {string[]} + * @default ["FAIR Suite v0.4.0"] + */ + mdqSuiteLabels: ["FAIR Suite v0.4.0"], + /** + * Metadata Assessment Suite IDs for the aggregated assessment charts + * @type {string[]} + * @default ["FAIR-suite-0.4.0"] + */ + mdqAggregatedSuiteIds: ["FAIR-suite-0.4.0"], + /** + * Metadata Assessment Suite labels for the aggregated assessment charts + * @type {string[]} + * @default ["FAIR Suite v0.4.0"] + */ + mdqAggregatedSuiteLabels: ["FAIR Suite v0.4.0"], + /** + * The metadata formats for which to display metadata assessment reports + * @type {string[]} + * @default ["eml*", "https://eml*", "*isotc211*"] + */ + mdqFormatIds: ["eml*", "https://eml*", "*isotc211*"], + + /** + * Metrics endpoint url + * @type {string} + */ + metricsUrl: "https://logproc-stage-ucsb-1.test.dataone.org/metrics", + + /** + * Forwards collection Query to Metrics Service if enabled + * @type {boolean} + * @default true + */ + metricsForwardCollectionQuery: true, + + /** + * DataONE Citation reporting endpoint url + * @type {string} + */ + dataoneCitationsUrl: + "https://logproc-stage-ucsb-1.test.dataone.org/citations", + + /** + * Hide or show the report Citation button in the dataset landing page. + * @type {boolean} + * @default true + */ + hideReportCitationButton: false, + + /** + * Hide or show the aggregated citations chart in the StatsView. + * These charts are only available for DataONE Plus members or Hosted Repositories. + * (see https://dataone.org) + * @type {boolean} + * @default true + * @since 2.9.0 + */ + hideSummaryCitationsChart: true, + /** + * Hide or show the aggregated downloads chart in the StatsView + * These charts are only available for DataONE Plus members or Hosted Repositories. + * (see https://dataone.org) + * @type {boolean} + * @default true + * @since 2.9.0 + */ + hideSummaryDownloadsChart: true, + /** + * Hide or show the aggregated metadata assessment chart in the StatsView + * These charts are only available for DataONE Plus members or Hosted Repositories. + * (see https://dataone.org) + * @type {boolean} + * @default true + * @since 2.9.0 + */ + hideSummaryMetadataAssessment: true, + /** + * Hide or show the aggregated views chart in the StatsView + * These charts are only available for DataONE Plus members or Hosted Repositories. + * (see https://dataone.org) + * @type {boolean} + * @default true + * @since 2.9.0 + */ + hideSummaryViewsChart: true, + + /** + * Metrics flag for the Dataset Landing Page + * Enable this flag to enable metrics display + * @type {boolean} + * @default true + */ + displayDatasetMetrics: true, + + /** + * If true, displays the dataset metrics tooltips on the metrics buttons. + * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} + * @type {boolean} + * @default true + */ + displayDatasetMetricsTooltip: true, + /** + * If true, displays the datasets metric modal windows on the dataset landing page + * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} + * @type {boolean} + * @default true + */ + displayMetricModals: true, + /** + * If true, displays the dataset citation metrics on the dataset landing page + * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} + * @type {boolean} + * @default true + */ + displayDatasetCitationMetric: true, + /** + * If true, displays the dataset download metrics on the dataset landing page + * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} + * @type {boolean} + * @default true + */ + displayDatasetDownloadMetric: true, + /** + * If true, displays the dataset view metrics on the dataset landing page + * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} + * @type {boolean} + * @default true + */ + displayDatasetViewMetric: true, + /** + * If true, displays the citation registration tool on the dataset landing page + * @type {boolean} + * @default true + * @since 2.15.0 + */ + displayRegisterCitationTool: true, + /** + * If true, displays the "Edit" button on the dataset landing page + * @type {boolean} + * @default true + */ + displayDatasetEditButton: true, + /** + * If true, displays the metadata assessment metrics on the dataset landing page + * @type {boolean} + * @default false + */ + displayDatasetQualityMetric: false, + /** + * If true, displays the WholeTale "Analyze" button on the dataset landing page + * @type {boolean} + * @default false + */ + displayDatasetAnalyzeButton: false, + /** + * If true, displays various buttons on the dataset landing page for dataset owners + * @type {boolean} + * @default false + */ + displayDatasetControls: true, + /** Hide metrics display for SolrResult models that match the given properties. + * Properties can be functions, which are given the SolrResult model value as a parameter. + * Turn off all dataset metrics displays using the {@link AppConfig#displayDatasetMetrics} + * @type {object} + * @example + * { + * formatId: "eml://ecoinformatics.org/eml-2.1.1", + * isPublic: true, + * dateUploaded: function(date){ + * return new Date(date) < new Date('1995-12-17T03:24:00'); + * } + * } + * // This example would hide metrics for any objects that are: + * // EML 2.1.1 OR public OR were uploaded before 12/17/1995. + */ + hideMetricsWhen: null, + + /** + * The bounding box path color to use in the Google Static Map images on the dataset landing pages. + * Specify the color either as a 24-bit (example: color=0xFFFFCC) or 32-bit hexadecimal value + * (example: color=0xFFFFCCFF), or from the set: black, brown, green, purple, yellow, blue, gray, orange, red, white. + * For more information, see the Google Statis Maps API docs: https://developers.google.com/maps/documentation/maps-static/start#PathStyles + * @type {string} + * @default "0xDA4D3Aff" (red) + * @since 2.13.0 + */ + datasetMapPathColor: "0xDA4D3Aff", + + /** + * The bounding box fill color to use in the Google Static Map images on the dataset landing pages. + * If you don't want to fill in the bounding boxes with a color, set this to null or undefined. + * Specify the color either as a 24-bit (example: color=0xFFFFCC) or 32-bit hexadecimal value + * (example: color=0xFFFFCCFF), or from the set: black, brown, green, purple, yellow, blue, gray, orange, red, white. + * For more information, see the Google Statis Maps API docs: https://developers.google.com/maps/documentation/maps-static/start#PathStyles + * @type {string} + * @default "0xFFFF0033" (light yellow) + * @since 2.13.0 + */ + datasetMapFillColor: "0xFFFF0033", + + /** + * The hue/color of the tiles drawn on the map when searching for data. + * This should be a three-digit hue degree between 0 and 360. (Try https://hslpicker.com) + * This is set on the {@link Map} model when it is initialized. + * @type {string} + * @default "192" (blue) + * @since 2.13.3 + */ + searchMapTileHue: "192", + + /** + * If true, the dataset landing pages will generate Schema.org-compliant JSONLD + * and insert it into the page. + * @type {boolean} + * @default true + */ + isJSONLDEnabled: true, + + /** + * If true, users can see a "Publish" button in the MetadataView, which makes the metadata + * document public and gives it a DOI identifier. + * If false, the button will be hidden completely. + * @type {boolean} + * @default true + */ + enablePublishDOI: true, + + /** + * A list of users or groups who exclusively will be able to see and use the "Publish" button, + * which makes the metadata document public and gives it a DOI identifier. + * Anyone not in this list will not be able to see the Publish button. + * `enablePublishDOI` must be set to `true` for this to take effect. + * @type {string[]} + */ + enablePublishDOIForSubjects: [], + + /** + * If true, users can change the AccessPolicy for any of their objects. + * This is equivalent to setting {@link AppConfig#allowAccessPolicyChangesPortals} and + * {@link AppConfig#allowAccessPolicyChangesDatasets} to `true`. + * @type {boolean} + * @default true + * @since 2.9.0 + */ + allowAccessPolicyChanges: true, + + /** + * If true, users can change the AccessPolicy for their portals only. + * @type {boolean} + * @default true + * @since 2.15.0 + */ + allowAccessPolicyChangesPortals: true, + + /** + * Limit portal Access policy editing to only a defined list of people or groups. + * To let everyone edit access policies for their own objects, keep this as an empty array + * and make sure {@link AppConfig#allowAccessPolicyChangesPortals} is set to `true` + * @type {boolean} + * @default [] + * @since 2.15.0 + */ + allowAccessPolicyChangesPortalsForSubjects: [], + + /** + * If true, users can change the AccessPolicy for their datasets only. + * @type {boolean} + * @default true + * @since 2.15.0 + */ + allowAccessPolicyChangesDatasets: true, + + /** + * Limit dataset Access policy editing to only a defined list of people or groups. + * To let everyone edit access policies for their own objects, keep this as an empty array + * and make sure {@link AppConfig#allowAccessPolicyChangesDatasets} is set to `true` + * @type {boolean} + * @default true + * @since 2.15.0 + */ + allowAccessPolicyChangesDatasetsForSubjects: [], + + /** + * The default {@link AccessPolicy} set on new objects uploaded to the repository. + * Each literal object here gets set directly on an {@link AccessRule} model. + * See the {@link AccessRule} list of default attributes for options on what to set here. + * @see {@link AccessRule} + * @type {object[]} + * @since 2.9.0 + * @default [{ + subject: "public", + read: true + }] + * @example + * [{ + * subject: "public", + * read: true + * }] + * // This example would assign public access to all new objects created in MetacatUI. + */ + defaultAccessPolicy: [ { - fields: ["sem_annotation"], - label: "Annotation", - placeholder: "Search for class...", - icon: "tag", - description: "Semantic annotations" + subject: "public", + read: true, }, + ], + + /** + * When new data objects are added to a {@link DataPackage}, they can either inherit the {@link AccessPolicy} from the + * parent metadata object, or default to the {@link AppConfig#defaultAccessPolicy}. To inherit the {@link AccessPolicy} + * from the parent metadata object, set this config to `true`. + * @type {boolean} + * @default true + * @since 2.15.0 + */ + inheritAccessPolicy: true, + + /** + * The user-facing name for editing the Access Policy. This is displayed as the header of the AccessPolicyView, for example + * @type {string} + * @since 2.9.0 + * @default "Sharing options" + */ + accessPolicyName: "Sharing options", + + /** + * @type {object} + * @property {boolean} accessRuleOptions.read - If true, users will be able to give others read access to their DataONE objects + * @property {boolean} accessRuleOptions.write - If true, users will be able to give others write access to their DataONE objects + * @property {boolean} accessRuleOptions.changePermission - If true, users will be able to give others changePermission access to their DataONE objects + * @since 2.9.0 + * @default { + read: true, + write: true, + changePermission: true + } + * @example + * { + * read: true, + * write: true, + * changePermission: false + * } + * // This example would enable users to edit the read and write access to files, + * // but not change ownership, in the Access Policy View. + */ + accessRuleOptions: { + read: true, + write: true, + changePermission: true, + }, + + /** + * @type {object} + * @property {boolean} accessRuleOptionNames.read - The user-facing name of the "read" access in Access Rules + * @property {boolean} accessRuleOptionNames.write - The user-facing name of the "write" access in Access Rules + * @property {boolean} accessRuleOptionNames.changePermission - The user-facing name of the "changePermission" access in Access Rules + * @since 2.9.0 + * @example + * { + * read: "Can view", + * write: "Can edit", + * changePermission: "Is owner" + * } + */ + accessRuleOptionNames: { + read: "Can view", + write: "Can edit", + changePermission: "Is owner", + }, + + /** + * If false, the rightsHolder of a resource will not be displayed in the AccessPolicyView. + * @type {boolean} + * @default true + * @since 2.9.0 + */ + displayRightsHolderInAccessPolicy: true, + + /** + * If false, users will not be able to change the rightsHolder of a resource in the AccessPolicyView + * @type {boolean} + * @default true + * @since 2.9.0 + */ + allowChangeRightsHolder: true, + + /** + * A list of group subjects that will be hidden in the AccessPolicy view to + * everyone except those in the group. This is useful for preventing users from + * removing repository administrative groups from access policies. + * @type {string[]} + * @since 2.9.0 + * @example ["CN=data-admin-group,DC=dataone,DC=org"] + */ + hiddenSubjectsInAccessPolicy: [], + + /** + * The format ID the portal editor serializes a new portal document as + * @type {string} + * @default "https://purl.dataone.org/portals-1.1.0" + * @readonly + * @since 2.17.0 + */ + portalEditorSerializationFormat: + "https://purl.dataone.org/portals-1.1.0", + + /** + * If true, the public/private toggle will be displayed in the Sharing Options for portals. + * @type {boolean} + * @default true + * @since 2.9.0 + */ + showPortalPublicToggle: true, + + /** + * The public/private toggle will be displayed in the Sharing Options for portals for only + * the given users or groups. To display the public/private toggle for everyone, + * set `showPortalPublicToggle` to true and keep this array empty. + * @type {string[]} + * @since 2.9.0 + */ + showPortalPublicToggleForSubjects: [], + + /** + * If true, the public/private toggle will be displayed in the Sharing Options for datasets. + * @type {boolean} + * @default true + * @since 2.9.0 + */ + showDatasetPublicToggle: true, + + /** + * The public/private toggle will be displayed in the Sharing Options for datasets for only + * the given users or groups. To display the public/private toggle for everyone, + * set `showDatasetPublicToggle` to true and keep this array empty. + * @type {string[]} + * @since 2.15.0 + */ + showDatasetPublicToggleForSubjects: [], + + /** + * Set to false to hide the display of "My Portals", which shows the user's current portals + * @type {boolean} + * @default true + */ + showMyPortals: true, + /** + * The user-facing term for portals in lower-case and in singular form. + * e.g. "portal" + * @type {string} + * @default "portal" + */ + portalTermSingular: "portal", + /** + * The user-facing term for portals in lower-case and in plural form. + * e.g. "portals". This allows for portal terms with irregular plurals. + * @type {string} + * @default "portals" + */ + portalTermPlural: "portals", + /** + * A URL of a webpage for people to learn more about portals. If no URL is provided, + * links to more info about portals will be omitted. + * @since 2.14.0 + * @type {string} + * @example "https://dataone.org/plus" + * @default null + */ + portalInfoURL: null, + /** + * The URL for a webpage where people can learn more about custom portal search + * filters. If no URL is provided, links to more info about portals will be omitted. + * @since 2.17.0 + * @type {string} + * @example "https://dataone.org/custom-search" + * @default null + */ + portalSearchFiltersInfoURL: null, + /** + * Set to false to prevent ANYONE from creating a new portal. + * @type {boolean} + * @default true + */ + enableCreatePortals: true, + /** + * Limits only the following people or groups to create new portals. If this is left as an empty array, + * then any logged-in user can create a portal. + * @type {string[]} + */ + limitPortalsToSubjects: [], + + /** + * This message will display when a user tries to create a new Portal in the PortalEditor + * when they are not associated with a whitelisted subject in the `limitPortalsToSubjects` list + * @type {string} + */ + portalEditNotAuthCreateMessage: + "You have not been authorized to create new portals. Please contact us with any questions.", + + /** + * This message will display when a user tries to access the Portal Editor for a portal + * for which they do not have write permission. + * @type {string} + */ + portalEditNotAuthEditMessage: + "The portal owner has not granted you permission to edit this portal. Please contact the owner to be given edit permission.", + + /** + * This message will display when a user tries to create a new portal when they have exceeded their DataONE portal quota + * @type {string} + */ + portalEditNoQuotaMessage: + "You have already reached the maximum number of portals for your membership level.", + + /** + * This message will display when there is any non-specific error during the save process of the PortalEditor. + * @type {string} + */ + portalEditSaveErrorMsg: + "Something went wrong while attempting to save your changes.", + + /** + * The list of fields that should be required in the portal editor. + * Set individual properties to `true` to require them in the portal editor. + * @type {object} + * @property {boolean} label - Default: true + * @property {boolean} name - Default: true + * @property {boolean} description - Default: false + * @property {boolean} sectionTitle - Default: true + * @property {boolean} sectionIntroduction - Default: false + * @property {boolean} logo - Default: false + */ + portalEditorRequiredFields: { + label: true, + name: true, + description: false, + sectionTitle: true, + sectionIntroduction: false, + logo: false, + //The following fields are not yet supported as required fields in the portal editor + //TODO: Add support for requiring the below fields + sectionImage: false, + acknowledgments: false, + acknowledgmentsLogos: false, + awards: false, + associatedParties: false, + }, + + /** + * A list of portals labels that no one should be able to create portals with + * @type {string[]} + * @readonly + * @since 2.11.3 + */ + portalLabelBlockList: [ + "Dataone", + "urn:node:CN", + "CN", + "cn", + "urn:node:CNUNM1", + "CNUNM1", + "cn-unm-1", + "urn:node:CNUCSB1", + "CNUCSB1", + "cn-ucsb-1", + "urn:node:CNORC1", + "CNORC1", + "cn-orc-1", + "urn:node:KNB", + "KNB", + "KNB Data Repository", + "urn:node:ESA", + "ESA", + "ESA Data Registry", + "urn:node:SANPARKS", + "SANPARKS", + "SANParks Data Repository", + "urn:node:ORNLDAAC", + "ORNLDAAC", + "ORNL DAAC", + "urn:node:LTER", + "LTER", + "U.S. LTER Network", + "urn:node:CDL", + "CDL", + "UC3 Merritt", + "urn:node:PISCO", + "PISCO", + "PISCO MN", + "urn:node:ONEShare", + "ONEShare", + "ONEShare DataONE Member Node", + "urn:node:mnORC1", + "mnORC1", + "DataONE ORC Dedicated Replica Server", + "urn:node:mnUNM1", + "mnUNM1", + "DataONE UNM Dedicated Replica Server", + "urn:node:mnUCSB1", + "mnUCSB1", + "DataONE UCSB Dedicated Replica Server", + "urn:node:TFRI", + "TFRI", + "TFRI Data Catalog", + "urn:node:USANPN", + "USANPN", + "USA National Phenology Network", + "urn:node:SEAD", + "SEAD", + "SEAD Virtual Archive", + "urn:node:GOA", + "GOA", + "Gulf of Alaska Data Portal", + "urn:node:KUBI", + "KUBI", + "University of Kansas - Biodiversity Institute", + "urn:node:LTER_EUROPE", + "LTER_EUROPE", + "LTER Europe Member Node", + "urn:node:DRYAD", + "DRYAD", + "Dryad Digital Repository", + "urn:node:CLOEBIRD", + "CLOEBIRD", + "Cornell Lab of Ornithology - eBird", + "urn:node:EDACGSTORE", + "EDACGSTORE", + "EDAC Gstore Repository", + "urn:node:IOE", + "IOE", + "Montana IoE Data Repository", + "urn:node:US_MPC", + "US_MPC", + "Minnesota Population Center", + "urn:node:EDORA", + "EDORA", + "Environmental Data for the Oak Ridge Area (EDORA)", + "urn:node:RGD", + "RGD", + "Regional and Global biogeochemical dynamics Data (RGD)", + "urn:node:GLEON", + "GLEON", + "GLEON Data Repository", + "urn:node:IARC", + "IARC", + "IARC Data Archive", + "urn:node:NMEPSCOR", + "NMEPSCOR", + "NM EPSCoR Tier 4 Node", + "urn:node:TERN", + "TERN", + "TERN Australia", + "urn:node:NKN", + "NKN", + "Northwest Knowledge Network", + "urn:node:USGS_SDC", + "USGS_SDC", + "USGS Science Data Catalog", + "urn:node:NRDC", + "NRDC", + "NRDC DataONE member node", + "urn:node:NCEI", + "NCEI", + "NOAA NCEI Environmental Data Archive", + "urn:node:PPBIO", + "PPBIO", + "PPBio", + "urn:node:NEON", + "NEON", + "NEON Member Node", + "urn:node:TDAR", + "TDAR", + "The Digital Archaeological Record", + "urn:node:ARCTIC", + "ARCTIC", + "Arctic Data Center", + "urn:node:BCODMO", + "BCODMO", + "Biological and Chemical Oceanography Data Management Office (BCO-DMO) ", + "urn:node:GRIIDC", + "GRIIDC", + "Gulf of Mexico Research Initiative Information and Data Cooperative (GRIIDC)", + "urn:node:R2R", + "R2R", + "Rolling Deck to Repository (R2R)", + "urn:node:EDI", + "EDI", + "Environmental Data Initiative", + "urn:node:UIC", + "UIC", + "A Member Node for University of Illinois at Chicago.", + "urn:node:RW", + "RW", + "Research Workspace", + "urn:node:FEMC", + "FEMC", + "Forest Ecosystem Monitoring Cooperative Member Node", + "urn:node:OTS_NDC", + "OTS_NDC", + "Organization for Tropical Studies - Neotropical Data Center", + "urn:node:PANGAEA", + "PANGAEA", + "PANGAEA", + "urn:node:ESS_DIVE", + "ESS_DIVE", + "ESS-DIVE: Deep Insight for Earth Science Data", + "urn:node:CAS_CERN", + "CAS_CERN", + "Chinese Ecosystem Research Network (CERN)", + "urn:node:FIGSHARE_CARY", + "FIGSHARE_CARY", + "Cary Institute of Ecosystem Studies (powered by Figshare)", + "urn:node:IEDA_EARTHCHEM", + "IEDA_EARTHCHEM", + "IEDA EARTHCHEM", + "urn:node:IEDA_USAP", + "IEDA_USAP", + "IEDA USAP", + "urn:node:IEDA_MGDL", + "IEDA_MGDL", + "IEDA MGDL", + "urn:node:METAGRIL", + "METAGRIL", + "metaGRIL", + "urn:node:ARM", + "ARM", + "ARM - Atmospheric Radiation Measurement Research Facility", + "urn:node:CA_OPC", + "CA_OPC", + "OPC", + "urn:node:TNC_DANGERMOND", + "dangermond", + "TNC_DANGERMOND", + "dangermondpreserve", + ], + + /** + * Limit users to a certain number of portals. This limit will be ignored if {@link AppConfig#enableBookkeeperServices} + * is set to true, because the limit will be enforced by Bookkeeper Quotas instead. + * @type {number} + * @default 100 + * @since 2.14.0 + */ + portalLimit: 100, + + /** + * The default values to use in portals. Default sections are applied when a portal is new. + * Default images are used in new freeform pages in the portal builder. + * The default colors are used when colors haven't been saved to the portal document. + * Colors can be hex codes, rgb codes, or any other form supported by browsers in CSS + * @type {object} + * @property {object[]} sections The default sections for a new portal. Each object within the section array can have a title property and a label property + * @property {string} label The name of the section that will appear in the tab + * @property {string} title A longer title for the section that will appear in the section header + * @property {string} newPortalActiveSectionLabel When a user start the portal builder for a brand new portal, the label for the section that the builder should start on. Can be set to "Data", "Metrics", "Settings", or one of the labels from the default sections described above. + * @property {string[]} sectionImageIdentifiers A list of image pids to use as default images for new markdown sections + * @property {string} primaryColor The color that is used most frequently in the portal view + * @property {string} secondaryColor The color that is used second-most frequently in the portal view + * @property {string} accentColor The color that is rarely used in portal views as an accent color + * @property {string} primaryColorTransparent An rgba() version of the primaryColor that is semi-transparent + * @property {string} secondaryColorTransparent An rgba() version of the secondaryColor that is semi-transparent + * @property {string} accentColorTransparent An rgba() version of the accentColor that is semi-transparent + * @example { + * sections: [ + * { label: "About", + * title: "About our project" + * }, + * { label: "Publications", + * title: "Selected publications by our lab group" + * } + * ], + * newPortalActiveSectionLabel: "About", + * sectionImageIdentifiers: ["urn:uuid:d2f31a83-debf-4d78-bef7-6abe20962581", "urn:uuid:6ad37acd-d0ac-4142-9f42-e5f05ff55564", "urn:uuid:0b6be09f-2e6f-4e7b-a83c-2823495f9608", "urn:uuid:5b4e0347-07ed-4580-b039-6c4df57ed801", "urn:uuid:0cf62da9-a099-440e-9c1e-595a55c0d60d"], + * primaryColor: "#16acc0", + * primaryColorTransparent: "rgba(22, 172, 192, .7)", + * secondaryColor: "#EED268", + * secondaryColorTransparent: "rgba(238, 210, 104, .7)", + * accentColor: "#0f5058", + * accentColorTransparent: "rgba(15, 80, 88, .7)" + * } + * @since 2.14.0 + */ + portalDefaults: {}, + + /** + * Add an API service URL that retrieves projects data. This is an optional + * configuration in case the memberNode have a third-party service that provides + * their projects information. + * + * If the configuration is not set, set the default projects list in the views using it. + * + * @type {string} + * @private + * @since 2.20.0 #TODO Update version here. + */ + projectsApiUrl: undefined, + /** + * Enable or disable the use of Fluid Earth Viewer visualizations in portals. + * This config option is marked as `private` since this is an experimental feature. + * @type {boolean} + * @private + * @since 2.13.4 + */ + enableFeverVisualizations: false, + /** + * The relative path to the location where the Fluid Earth Viewer (FEVer) is deployed. This should be + * deployed at the same origin as MetacatUI, since your web server configuration and many browsers + * may block iframes from different origins. + * This config option is marked as `private` since this is an experimental feature. + * @type {string} + * @private + * @since 2.13.4 + */ + feverPath: "/fever", + /** + * The full URL to the location where the Fluid Earth Viewer (FEVer) is deployed. + * This URL is constructed during {@link AppModel#initialize} using the {@link AppConfig#baseUrl} + * and {@link AppConfig#feverPath}. + * This config option is marked as `private` since this is an experimental feature. + * @type {string} + * @readonly + * @private + * @since 2.13.4 + */ + feverUrl: "", + + /** If true, then archived content is available in the search index. + * Set to false if this MetacatUI is using a Metacat version before 2.10.0 + * @type {boolean} + * @default true + */ + archivedContentIsIndexed: true, + + /** + * The metadata fields to hide when a user is creating a collection definition using + * the Query Builder View displayed in the portal builder on the data page, or + * anywhere else the EditCollectionView is displayed. Strings listed here should + * exactly match the 'name' for each field provided by the DataONE search index API + * (i.e. should match the Solr field). + * @example ["sem_annotated_by", "mediaType"] + * @type {string[]} + */ + collectionQueryExcludeFields: [ + "sem_annotated_by", + "sem_annotates", + "sem_comment", + "pubDate", + "namedLocation", + "contactOrganization", + "investigator", + "originator", + "originatorText", + "serviceInput", + "authorGivenName", + "authorSurName", + "topic", + "webUrl", + "_root_", + "collectionQuery", + "geohash_1", + "geohash_2", + "geohash_3", + "geohash_4", + "geohash_5", + "geohash_6", + "geohash_7", + "geohash_8", + "geohash_9", + "label", + "LTERSite", + "_version_", + "checksumAlgorithm", + "keywords", + "parameterText", + "project", + "topicText", + "dataUrl", + "fileID", + "isDocumentedBy", + "logo", + "obsoletes", + "origin", + "funding", + "formatType", + "obsoletedBy", + "presentationCat", + "mediaType", + "mediaTypeProperty", + "relatedOrganizations", + "noBoundingBox", + "decade", + "hasPart", + "sensorText", + "sourceText", + "termText", + "titlestr", + "site", + "id", + "updateDate", + "edition", + "gcmdKeyword", + "isSpatial", + "keyConcept", + "ogcUrl", + "parameter", + "sensor", + "source", + "term", + "investigatorText", + "sku", + "_text_", + // Fields that have been made into a special combination field + "beginDate", + "endDate", + "awardNumber", + // Provenance fields (keep only "prov_hasSources" and "prov_hasDerivations"), + // since they are the only ones indexed on metadata objects + "prov_wasGeneratedBy", + "prov_generated", + "prov_generatedByExecution", + "prov_generatedByProgram", + "prov_generatedByUser", + "prov_instanceOfClass", + "prov_used", + "prov_usedByExecution", + "prov_usedByProgram", + "prov_usedByUser", + "prov_wasDerivedFrom", + "prov_wasExecutedByExecution", + "prov_wasExecutedByUser", + "prov_wasInformedBy", + ], + + /** + * A special field is one that does not exist in the query service index (i.e. + * Solr). It can be a combination of fields that are presented to the user as a + * single field, but which are added to the model as multiple fields. It can also be + * a duplicate of a field that does exist, but presented with a different label (and + * even with different {@link operatorOptions operator options} or + * {@link valueSelectUImap value input} if needed). + * + * @typedef {Object} SpecialField + * @property {string} name - A unique ID to represent this field. It must not match + * the name of any other query fields. + * @property {string[]} fields - The list of real query fields that this abstracted + * field should represent. The query fields listed must exactly match the names of + * the query fields that are retrieved from the query service. + * @property {string} label - A user-facing label to display. + * @property {string} description - A description for this field. + * @property {string} category - The name of the category under which to place this + * field. It must match one of the category names for an existing query field set in + * {@link QueryField#categoriesMap}. + * @property {string[]} [values] - An optional list of filter values. If set, this + * is used to determine whether a pre-existing Query Rule should be displayed as one + * of these special fields, or as a field from the query API. Setting values means + * that the values set on the Query Rule model must exactly match the values set. + * + * @since 2.15.0 + */ + + /** + * A list of additional fields which are not retrieved from the query API (i.e. are + * not Solr fields), but which should be added to the list of options the user can + * select from when building a query in the EditCollectionView. This can be used to + * add abstracted fields which are a combination of multiple query fields, or to add + * a duplicate field that has a different label. + * + * @type {SpecialField[]} + * + * @since 2.15.0 + */ + collectionQuerySpecialFields: [ { - filterType: "ToggleFilter", + name: "documents-special-field", fields: ["documents"], label: "Contains Data Files", - placeholder: "Only results with data", - trueLabel: "Required", - falseLabel: null, - trueValue: "*", - matchSubstring: false, - icon: "table", - description: "Checking this option will only return packages that include data files. Leaving this unchecked may return packages that only include metadata." - }, - { - fields: ["originText"], - label: "Creator", - placeholder: "Name", - icon: "user", - description: "The name of the creator or originator of a dataset" - }, - { - filterType: "DateFilter", - fields: ["datePublished", "dateUploaded"], - label: "Publish Year", - rangeMin: 1800, - icon: "calendar", - description: "Only show results that were published within the year range" - }, - { - filterType: "DateFilter", - fields: ["beginDate"], - label: "Year of data coverage", - rangeMin: 1800, - icon: "calendar", - description: "Only show results with data collected within the year range" + description: + "Limit results to packages that include data files. Without" + + " this rule, results may include packages with metadata but no data.", + category: "General", + values: ["*"], }, { - fields: ["identifier", "documents", "resourceMap", "seriesId"], - label: "Identifier", - placeholder: "DOI or ID", - icon: "bullseye", - description: "Find datasets if you have all or part of its DOI or ID", - operator: "OR", - fieldsOperator: "OR" + name: "year-data-collection", + fields: ["beginDate", "endDate"], + label: "Year of Data Collection", + description: + "The temporal range of content described by the metadata", + category: "Dates", }, { - fields: ["kingdom", "phylum", "class", "order", "family", "genus", "species"], - label: "Taxon", - placeholder: "Class, family, etc.", - icon: "sitemap", - description: "Find data about any taxonomic rank", - matchSubstring: true, - fieldsOperator: "OR" + name: "funding-text-award-number", + fields: ["fundingText", "awardNumber"], + label: "Award Number", + description: + "The award number for funding associated with a dataset or the " + + "description of funding source", + category: "Awards & funding", }, + ], + + /** + * The names of the query fields that use an object identifier as a value. Filter + * models that use one of these fields are handled specially when building query + * strings - they are OR'ed at the end of queries. They are also given an "OR" + * operator and fieldsOperator attribute when parsed. + * @type {string[]} + * + * @since 2.17.0 + */ + queryIdentifierFields: ["id", "identifier", "seriesId", "isPartOf"], + + /** + * The name of the query fields that specify latitude. Filter models that these + * fields are handled specially, since they must be a float value and have a + * pre-determined minRange and maxRange (-90 to 90). + */ + queryLatitudeFields: ["northBoundCoord", "southBoundCoord"], + + /** + * The name of the query fields that specify longitude. Filter models that these + * fields are handled specially, since they must be a float value and have a + * pre-determined minRange and maxRange (-180 to 180). + */ + queryLongitudeFields: ["eastBoundCoord", "westBoundCoord"], + + /** + * The names of the query fields that may require special treatment in the + * UI. For example, upgrade the view for a Filter from a FilterView to + * a SemanticFilterView or to block certain UIBuilders in FilterEditorView + * that don't make sense for a semantic field. + * + * @type {string[]} + * @since 2.22.0 + */ + querySemanticFields: ["sem_annotation"], + + /** + * The isPartOf filter is added to all new portals built in the Portal + * Builder automatically. It is required for dataset owners to include + * their dataset in a specific portal collection. By default, this filter + * is hidden. Set to false to make this filter visible. + * @type {boolean} + */ + hideIsPartOfFilter: true, + + /** + * The default {@link FilterGroup}s to use in the data catalog search ({@link CatalogSearchView}). + * This is an array of literal objects that will be directly set on the {@link FilterGroup} models. Refer to the {@link FilterGroup#defaults} for + * options. + * @type {FilterGroup#defaults[]} + */ + defaultFilterGroups: [ { - fields: ["siteText"], - label: "Location", - placeholder: "Geographic region", - icon: "globe", - description: "The geographic region or study site, as described by the submitter" + label: "", + filters: [ + { + fields: ["attribute"], + label: "Data attribute", + placeholder: "density, length, etc.", + icon: "table", + description: + "Measurement type, e.g. density, temperature, species", + }, + { + fields: ["sem_annotation"], + label: "Annotation", + placeholder: "Search for class...", + icon: "tag", + description: "Semantic annotations", + }, + { + filterType: "ToggleFilter", + fields: ["documents"], + label: "Contains Data Files", + placeholder: "Only results with data", + trueLabel: "Required", + falseLabel: null, + trueValue: "*", + matchSubstring: false, + icon: "table", + description: + "Checking this option will only return packages that include data files. Leaving this unchecked may return packages that only include metadata.", + }, + { + fields: ["originText"], + label: "Creator", + placeholder: "Name", + icon: "user", + description: + "The name of the creator or originator of a dataset", + }, + { + filterType: "DateFilter", + fields: ["datePublished", "dateUploaded"], + label: "Publish Year", + rangeMin: 1800, + icon: "calendar", + description: + "Only show results that were published within the year range", + }, + { + filterType: "DateFilter", + fields: ["beginDate"], + label: "Year of data coverage", + rangeMin: 1800, + icon: "calendar", + description: + "Only show results with data collected within the year range", + }, + { + fields: [ + "identifier", + "documents", + "resourceMap", + "seriesId", + ], + label: "Identifier", + placeholder: "DOI or ID", + icon: "bullseye", + description: + "Find datasets if you have all or part of its DOI or ID", + operator: "OR", + fieldsOperator: "OR", + }, + { + fields: [ + "kingdom", + "phylum", + "class", + "order", + "family", + "genus", + "species", + ], + label: "Taxon", + placeholder: "Class, family, etc.", + icon: "sitemap", + description: "Find data about any taxonomic rank", + matchSubstring: true, + fieldsOperator: "OR", + }, + { + fields: ["siteText"], + label: "Location", + placeholder: "Geographic region", + icon: "globe", + description: + "The geographic region or study site, as described by the submitter", + }, + ], }, - ] - } - ], - - /** - * The document fields to return when conducting a search. This is the list of fields returned by the main catalog search view. - * @type {string[]} - * @since 2.22.0 - * @example ["id", "title", "obsoletedBy"] - */ - defaultSearchFields: ["id", "seriesId", "title", "origin", "pubDate","dateUploaded","abstract","resourceMap","beginDate","endDate","read_count_i","geohash_9","datasource","isPublic","project","documents","label","logo","formatId","northBoundCoord","southBoundCoord","eastBoundCoord","westBoundCoord"], - - /** - * Semantic annotation configuration - * Include your Bioportal api key to show ontology information for metadata annotations - * see: http://bioportal.bioontology.org/account - * @type {string} - */ - bioportalAPIKey: "", - /** - * The Bioportal REST API URL, which is set dynamically only if a bioportalAPIKey is configured - * @type {string} - * @default "https://data.bioontology.org/search" - */ - bioportalSearchUrl: "https://data.bioontology.org/search", - /** - * This attribute stores cache of ontology information that is looked up in Bioportal, so that duplicate REST calls don't need to be made. - * @type {object} - */ - bioportalLookupCache: {}, - /** - * Set this option to true to display the annotation icon in search result rows when a dataset has an annotation - * @type {boolean} - */ - showAnnotationIndicator: false, - - /** - * A list of unsupported User-Agent regular expressions for browsers that will not work well with MetacatUI. - * A warning message will display on the page for anyone using one of these browsers. - * @type {RegExp[]} - * @since 2.10.0 - * @default [/(?:\b(MS)?IE\s+|\bTrident\/7\.0;.*\s+rv:)(\d+)/] - * @example [/(?:\b(MS)?IE\s+|\bTrident\/7\.0;.*\s+rv:)(\d+)/] - */ - unsupportedBrowsers: [/(?:\b(MS)?IE\s+|\bTrident\/7\.0;.*\s+rv:)(\d+)/], - - /** - * A list of alternate repositories to use for fetching and saving DataONEObjects. - * In the AppConfig, this is an array of {@link NodeModel#members} attributes, in JSON form. - * These are the same attributes retireved from the Node Info document, via the d1/mn/v2/node API. - * The only required attributes are name, identifier, and baseURL. - * @type {object[]} - * @example [{ - * name: "Metacat MN", - * identifier: "urn:node:METACAT", - * baseURL: "https://my-metacat.org/metacat/d1/mn" - * }] - * - * @since 2.14.0 - */ - alternateRepositories: [], - - /** - * The node identifier of the alternate repository that is used for fetching and saving DataONEObjects. - * this attribute is dynamically set by MetacatUI to keep track of the currently active alt repo. - * To specify a repository that should be active by default, set {@link AppConfig#defaultAlternateRepositoryId} - * @type {string} - * @example "urn:node:METACAT" - * @since 2.14.0 - * @readonly - */ - activeAlternateRepositoryId: null, - - /** - * The node identifier of the alternate repository that should be used for fetching and saving DataONEObjects. - * Since there can be multiple alternate repositories configured, this attribute can be used to specify which - * one is actively in use. - * @type {string} - * @example "urn:node:METACAT" - * @since 2.14.0 - */ - defaultAlternateRepositoryId: null, - - /** - * Enable or disable the DataONE Bookkeeper services. If enabled, Portal Views will use the DataONE Plus - * paid features for active subscriptions. If disabled, the Portal Views will assume - * all portals are in inactive/free, and will only render free features. - * @type {boolean} - * @since 2.14.0 - */ - enableBookkeeperServices: false, - /** - * The base URL for the DataONE Bookkeeper services, which manage the DataONE membership plans, such as - * Hosted Repositories and Plus. - * See https://github.com/DataONEorg/bookkeeper for more info on this service. - * @type {string} - * @since 2.14.0 - */ - bookkeeperBaseUrl: "https://api.test.dataone.org:30443/bookkeeper/v1", - /** - * The URL for the DataONE Bookkeeper Quota API, e.g. listQuotas(), getQuota(), createQuota(), etc. - * This full URL is contructed using {@link AppModel#bookkeeperBaseUrl} when the AppModel is initialized. - * @readonly - * @type {string} - * @since 2.14.0 - */ - bookkeeperQuotasUrl: null, - /** - * The URL for the DataONE Bookkeeper Usages API, e.g. listUsages(), getUsage(), createUsage(), etc. - * This full URL is contructed using {@link AppModel#bookkeeperBaseUrl} when the AppModel is initialized. - * @readonly - * @type {string} - * @since 2.14.0 - */ - bookkeeperUsagesUrl: null, - /** - * The URL for the DataONE Bookkeeper Subscriptions API, e.g. listSubscriptions(), fetchSubscription(), createSubscription(), etc. - * This full URL is contructed using {@link AppModel#bookkeeperBaseUrl} when the AppModel is initialized. - * @readonly - * @type {string} - * @since 2.14.0 - */ - bookkeeperSubscriptionsUrl: null, - /** - * The URL for the DataONE Bookkeeper Customers API, e.g. listCustomers(), getCustomer(), createCustomer(), etc. - * This full URL is contructed using {@link AppModel#bookkeeperBaseUrl} when the AppModel is initialized. - * @readonly - * @type {string} - * @since 2.14.0 - */ - bookkeeperCustomersUrl: null, - - /** - * The name of the DataONE Plus membership plan, which is used in messaging throughout the UI. - * This is only used if the enableBookkeeperServices setting is set to true. - * @type {string} - * @default "DataONE Plus" - */ - dataonePlusName: "DataONE Plus", - - //These two DataONE Plus Preview attributes are for a special DataONE Plus tag of MetacatUI - // and won't be released in an offical MetacatUI version, since they will be replaced by bookkeeper - dataonePlusPreviewMode: false, - dataonePlusPreviewPortals: [], - /* - * List of Repositories that are DataONE Hosted Repos. - * DataONE Hosted Repo features are displayed only for these members. - * @type {string[]} - * @readonly - * @since 2.13.0 - * ------------------------------------ - * This config will not be displayed in the JSDoc documentation since it is - * temporary and only useful for internal DataONE purposes. This functionality will be replaced - * with the DataONE Bookkeeper service, eventually. - */ - dataoneHostedRepos: ["urn:node:KNB", "urn:node:ARCTIC", "urn:node:CA_OPC", "urn:node:ESS_DIVE", "urn:node:CERP_SFWMD"], - - /** - * The length of random portal label generated during preview/trial mode of DataONE Plus - * @readonly - * @type {number} - * @default 7 - * @since 2.14.0 - */ - randomLabelNumericLength: 7, - - /** - * If enabled (by setting to true), Cesium maps will be used in the interface. - * If a {@link AppConfig#cesiumToken} is not provided, Cesium features will be disabled. - * @type {boolean} - * @default false - * @since 2.18.0 - */ - enableCesium: true, - - /** - * Your Access Token for the Cesium API, which can be retrieved from - * {@link https://cesium.com/ion/tokens}. - * @type {string} - * @since 2.18.0 - * @example eyJhbGciOiJIUzI1R5cCI6IkpXVCJ9.eyJqdGkiOiJmYzUwYjI0ZC0yN2Y4LTRiZjItOdCI6MTYwODIyNDg5MH0.KwCI2-4cHjFYXrR6-mUrwkhh1UdNARK7NxFLpFftjeg - */ - cesiumToken: "", - - /** - * Your Access Token for the Bing Maps Imagery API, which can be retrieved from - * https://www.bingmapsportal.com/. Required if any Cesium layers use imagery - * directly from Bing. - * @type {string} - * @since 2.18.0 - * @example AtZjkdlajkl_jklcCAO_1JYafsvAjU1nkd9jdD6CDnHyamndlasdt5CB7xs - */ - bingMapsKey: "", - - /** - * Enable or disable showing the MeasurementTypeView in the Editor's - * attribute modal dialog. The {@link AppModel#bioportalAPIKey} must be set to a valid Bioportal - * API key for the ontology tree widget to work. - * @type {boolean} - * @since 2.17.0 - * @default false - */ - enableMeasurementTypeView: false, - - /** - * As of 2.22.0, the {@link DataCatalogView} is being soft-deprecated and replaced with the new {@link CatalogSearchView}. - * To give MetacatUI operators time to transition to the new {@link CatalogSearchView}, this configuration option can be - * enabled (by setting to `true`) and will tell MetacatUI to use the legacy {@link DataCatalogView}. It is highly suggested - * that MetacatUI operators switch to supporting the new {@link CatalogSearchView} as soon as possible as the legacy {@link DataCatalogView} - * will be fully deprecated and removed in the future. - * @since 2.22.0 - * @type {boolean} - * @default false - */ - useDeprecatedDataCatalogView: true, - - /** - * The following configuration options are deprecated or experimental and should only be changed by advanced users - */ - /** - * The URL for the DataONE log service. This service has been replaced with the DataONE metrics service - * (which has not been publicly released), so this configuration will be deprecated in the future. - * This URL is constructed dynamically upon AppModel intialization. - * @type {string} - * @deprecated - */ - d1LogServiceUrl: null, - - /** - * This configuration option is deprecated. This is only used by the {@link DataCatalogView} and {@link DataCatalogViewWithFilters}, - * both of which have been replaced by the {@link CatalogSearchView}. The search mode is now controlled directly on the {@link CatalogSearchView} - * instead of controlled at the global level here. - * @deprecated - */ - searchMode: MetacatUI.mapKey ? 'map' : 'list', - - /** - * This Bioportal REST API URL is used by the experimental and unsupported AnnotatorView to get multiple ontology class info at once. - * @deprecated - */ - //bioportalBatchUrl: "https://data.bioontology.org/batch" - - /** - * The packageFormat is the identifier for the version of bagit used when downloading data packages. The format should - * not contain any additional characters after, for example a backslash. - * For hierarchical dowloads, use application%2Fbagit-1.0 - * @type {string} - * @default "application%2Fbagit-1.0" - * @example application%2Fbagit-097 - */ - packageFormat: 'application%2Fbagit-1.0' - }, MetacatUI.AppConfig), - - defaultView: "data", - - initialize: function() { - - //If no base URL is specified, then user the DataONE CN base URL - if(!this.get("baseUrl")){ - this.set("baseUrl", this.get("d1CNBaseUrl")); - this.set("d1Service", this.get("d1CNService")); - } - - //Set the DataONE MN API URLs - this.set( this.getDataONEMNAPIs() ); - - //Determine if this instance of MetacatUI is pointing to a CN, rather than a MN - this.set("isCN", (this.get("d1Service").indexOf("cn/v2") > 0)); - - this.set('metacatServiceUrl', this.get('baseUrl') + this.get('context') + '/metacat'); - - // Metadata quality report services - this.set('mdqSuitesServiceUrl', this.get("mdqBaseUrl") + "/suites/"); - this.set('mdqRunsServiceUrl', this.get('mdqBaseUrl') + "/runs/"); + ], + + /** + * The document fields to return when conducting a search. This is the list of fields returned by the main catalog search view. + * @type {string[]} + * @since 2.22.0 + * @example ["id", "title", "obsoletedBy"] + */ + defaultSearchFields: [ + "id", + "seriesId", + "title", + "origin", + "pubDate", + "dateUploaded", + "abstract", + "resourceMap", + "beginDate", + "endDate", + "read_count_i", + "geohash_9", + "datasource", + "isPublic", + "project", + "documents", + "label", + "logo", + "formatId", + "northBoundCoord", + "southBoundCoord", + "eastBoundCoord", + "westBoundCoord", + ], + + /** + * Semantic annotation configuration + * Include your Bioportal api key to show ontology information for metadata annotations + * see: http://bioportal.bioontology.org/account + * @type {string} + */ + bioportalAPIKey: "", + /** + * The Bioportal REST API URL, which is set dynamically only if a bioportalAPIKey is configured + * @type {string} + * @default "https://data.bioontology.org/search" + */ + bioportalSearchUrl: "https://data.bioontology.org/search", + /** + * This attribute stores cache of ontology information that is looked up in Bioportal, so that duplicate REST calls don't need to be made. + * @type {object} + */ + bioportalLookupCache: {}, + /** + * Set this option to true to display the annotation icon in search result rows when a dataset has an annotation + * @type {boolean} + */ + showAnnotationIndicator: false, + + /** + * A list of unsupported User-Agent regular expressions for browsers that will not work well with MetacatUI. + * A warning message will display on the page for anyone using one of these browsers. + * @type {RegExp[]} + * @since 2.10.0 + * @default [/(?:\b(MS)?IE\s+|\bTrident\/7\.0;.*\s+rv:)(\d+)/] + * @example [/(?:\b(MS)?IE\s+|\bTrident\/7\.0;.*\s+rv:)(\d+)/] + */ + unsupportedBrowsers: [ + /(?:\b(MS)?IE\s+|\bTrident\/7\.0;.*\s+rv:)(\d+)/, + ], + + /** + * A list of alternate repositories to use for fetching and saving DataONEObjects. + * In the AppConfig, this is an array of {@link NodeModel#members} attributes, in JSON form. + * These are the same attributes retireved from the Node Info document, via the d1/mn/v2/node API. + * The only required attributes are name, identifier, and baseURL. + * @type {object[]} + * @example [{ + * name: "Metacat MN", + * identifier: "urn:node:METACAT", + * baseURL: "https://my-metacat.org/metacat/d1/mn" + * }] + * + * @since 2.14.0 + */ + alternateRepositories: [], + + /** + * The node identifier of the alternate repository that is used for fetching and saving DataONEObjects. + * this attribute is dynamically set by MetacatUI to keep track of the currently active alt repo. + * To specify a repository that should be active by default, set {@link AppConfig#defaultAlternateRepositoryId} + * @type {string} + * @example "urn:node:METACAT" + * @since 2.14.0 + * @readonly + */ + activeAlternateRepositoryId: null, + + /** + * The node identifier of the alternate repository that should be used for fetching and saving DataONEObjects. + * Since there can be multiple alternate repositories configured, this attribute can be used to specify which + * one is actively in use. + * @type {string} + * @example "urn:node:METACAT" + * @since 2.14.0 + */ + defaultAlternateRepositoryId: null, + + /** + * Enable or disable the DataONE Bookkeeper services. If enabled, Portal Views will use the DataONE Plus + * paid features for active subscriptions. If disabled, the Portal Views will assume + * all portals are in inactive/free, and will only render free features. + * @type {boolean} + * @since 2.14.0 + */ + enableBookkeeperServices: false, + /** + * The base URL for the DataONE Bookkeeper services, which manage the DataONE membership plans, such as + * Hosted Repositories and Plus. + * See https://github.com/DataONEorg/bookkeeper for more info on this service. + * @type {string} + * @since 2.14.0 + */ + bookkeeperBaseUrl: "https://api.test.dataone.org:30443/bookkeeper/v1", + /** + * The URL for the DataONE Bookkeeper Quota API, e.g. listQuotas(), getQuota(), createQuota(), etc. + * This full URL is contructed using {@link AppModel#bookkeeperBaseUrl} when the AppModel is initialized. + * @readonly + * @type {string} + * @since 2.14.0 + */ + bookkeeperQuotasUrl: null, + /** + * The URL for the DataONE Bookkeeper Usages API, e.g. listUsages(), getUsage(), createUsage(), etc. + * This full URL is contructed using {@link AppModel#bookkeeperBaseUrl} when the AppModel is initialized. + * @readonly + * @type {string} + * @since 2.14.0 + */ + bookkeeperUsagesUrl: null, + /** + * The URL for the DataONE Bookkeeper Subscriptions API, e.g. listSubscriptions(), fetchSubscription(), createSubscription(), etc. + * This full URL is contructed using {@link AppModel#bookkeeperBaseUrl} when the AppModel is initialized. + * @readonly + * @type {string} + * @since 2.14.0 + */ + bookkeeperSubscriptionsUrl: null, + /** + * The URL for the DataONE Bookkeeper Customers API, e.g. listCustomers(), getCustomer(), createCustomer(), etc. + * This full URL is contructed using {@link AppModel#bookkeeperBaseUrl} when the AppModel is initialized. + * @readonly + * @type {string} + * @since 2.14.0 + */ + bookkeeperCustomersUrl: null, + + /** + * The name of the DataONE Plus membership plan, which is used in messaging throughout the UI. + * This is only used if the enableBookkeeperServices setting is set to true. + * @type {string} + * @default "DataONE Plus" + */ + dataonePlusName: "DataONE Plus", + + //These two DataONE Plus Preview attributes are for a special DataONE Plus tag of MetacatUI + // and won't be released in an offical MetacatUI version, since they will be replaced by bookkeeper + dataonePlusPreviewMode: false, + dataonePlusPreviewPortals: [], + /* + * List of Repositories that are DataONE Hosted Repos. + * DataONE Hosted Repo features are displayed only for these members. + * @type {string[]} + * @readonly + * @since 2.13.0 + * ------------------------------------ + * This config will not be displayed in the JSDoc documentation since it is + * temporary and only useful for internal DataONE purposes. This functionality will be replaced + * with the DataONE Bookkeeper service, eventually. + */ + dataoneHostedRepos: [ + "urn:node:KNB", + "urn:node:ARCTIC", + "urn:node:CA_OPC", + "urn:node:ESS_DIVE", + "urn:node:CERP_SFWMD", + ], + + /** + * The length of random portal label generated during preview/trial mode of DataONE Plus + * @readonly + * @type {number} + * @default 7 + * @since 2.14.0 + */ + randomLabelNumericLength: 7, + + /** + * If enabled (by setting to true), Cesium maps will be used in the interface. + * If a {@link AppConfig#cesiumToken} is not provided, Cesium features will be disabled. + * @type {boolean} + * @default false + * @since 2.18.0 + */ + enableCesium: true, + + /** + * Your Access Token for the Cesium API, which can be retrieved from + * {@link https://cesium.com/ion/tokens}. + * @type {string} + * @since 2.18.0 + * @example eyJhbGciOiJIUzI1R5cCI6IkpXVCJ9.eyJqdGkiOiJmYzUwYjI0ZC0yN2Y4LTRiZjItOdCI6MTYwODIyNDg5MH0.KwCI2-4cHjFYXrR6-mUrwkhh1UdNARK7NxFLpFftjeg + */ + cesiumToken: "", + + /** + * Your Access Token for the Bing Maps Imagery API, which can be retrieved from + * https://www.bingmapsportal.com/. Required if any Cesium layers use imagery + * directly from Bing. + * @type {string} + * @since 2.18.0 + * @example AtZjkdlajkl_jklcCAO_1JYafsvAjU1nkd9jdD6CDnHyamndlasdt5CB7xs + */ + bingMapsKey: "", + + /** + * Enable or disable showing the MeasurementTypeView in the Editor's + * attribute modal dialog. The {@link AppModel#bioportalAPIKey} must be set to a valid Bioportal + * API key for the ontology tree widget to work. + * @type {boolean} + * @since 2.17.0 + * @default false + */ + enableMeasurementTypeView: false, + + /** + * As of 2.22.0, the {@link DataCatalogView} is being soft-deprecated and replaced with the new {@link CatalogSearchView}. + * To give MetacatUI operators time to transition to the new {@link CatalogSearchView}, this configuration option can be + * enabled (by setting to `true`) and will tell MetacatUI to use the legacy {@link DataCatalogView}. It is highly suggested + * that MetacatUI operators switch to supporting the new {@link CatalogSearchView} as soon as possible as the legacy {@link DataCatalogView} + * will be fully deprecated and removed in the future. + * @since 2.22.0 + * @type {boolean} + * @default false + */ + useDeprecatedDataCatalogView: true, + + /** + * The following configuration options are deprecated or experimental and should only be changed by advanced users + */ + /** + * The URL for the DataONE log service. This service has been replaced with the DataONE metrics service + * (which has not been publicly released), so this configuration will be deprecated in the future. + * This URL is constructed dynamically upon AppModel intialization. + * @type {string} + * @deprecated + */ + d1LogServiceUrl: null, + + /** + * This configuration option is deprecated. This is only used by the {@link DataCatalogView} and {@link DataCatalogViewWithFilters}, + * both of which have been replaced by the {@link CatalogSearchView}. The search mode is now controlled directly on the {@link CatalogSearchView} + * instead of controlled at the global level here. + * @deprecated + */ + searchMode: MetacatUI.mapKey ? "map" : "list", + + /** + * This Bioportal REST API URL is used by the experimental and unsupported AnnotatorView to get multiple ontology class info at once. + * @deprecated + */ + //bioportalBatchUrl: "https://data.bioontology.org/batch" + + /** + * The packageFormat is the identifier for the version of bagit used when downloading data packages. The format should + * not contain any additional characters after, for example a backslash. + * For hierarchical dowloads, use application%2Fbagit-1.0 + * @type {string} + * @default "application%2Fbagit-1.0" + * @example application%2Fbagit-097 + */ + packageFormat: "application%2Fbagit-1.0", + }, + MetacatUI.AppConfig, + ), - //DataONE CN API - if(this.get("d1CNBaseUrl")){ + defaultView: "data", - //Add a forward slash to the end of the base URL if there isn't one - var d1CNBaseUrl = this.get("d1CNBaseUrl"); - if( d1CNBaseUrl.charAt( d1CNBaseUrl.length-1 ) == "/" ){ - d1CNBaseUrl = d1CNBaseUrl.substring(0, d1CNBaseUrl.length-1); - this.set("d1CNBaseUrl", d1CNBaseUrl); + initialize: function () { + //If no base URL is specified, then user the DataONE CN base URL + if (!this.get("baseUrl")) { + this.set("baseUrl", this.get("d1CNBaseUrl")); + this.set("d1Service", this.get("d1CNService")); } - //Account services - if(typeof this.get("accountsUrl") != "undefined"){ - this.set("accountsUrl", d1CNBaseUrl + this.get("d1CNService") + "/accounts/"); + //Set the DataONE MN API URLs + this.set(this.getDataONEMNAPIs()); - if(typeof this.get("pendingMapsUrl") != "undefined") - this.set("pendingMapsUrl", this.get("accountsUrl") + "pendingmap/"); + //Determine if this instance of MetacatUI is pointing to a CN, rather than a MN + this.set("isCN", this.get("d1Service").indexOf("cn/v2") > 0); - if(typeof this.get("accountsMapsUrl") != "undefined") - this.set("accountsMapsUrl", this.get("accountsUrl") + "map/"); + this.set( + "metacatServiceUrl", + this.get("baseUrl") + this.get("context") + "/metacat", + ); - if(typeof this.get("groupsUrl") != "undefined") - this.set("groupsUrl", d1CNBaseUrl + this.get("d1CNService") + "/groups/"); - } + // Metadata quality report services + this.set("mdqSuitesServiceUrl", this.get("mdqBaseUrl") + "/suites/"); + this.set("mdqRunsServiceUrl", this.get("mdqBaseUrl") + "/runs/"); - if(typeof this.get("d1LogServiceUrl") != "undefined") - this.set('d1LogServiceUrl', d1CNBaseUrl + this.get('d1CNService') + '/query/logsolr/?'); + //DataONE CN API + if (this.get("d1CNBaseUrl")) { + //Add a forward slash to the end of the base URL if there isn't one + var d1CNBaseUrl = this.get("d1CNBaseUrl"); + if (d1CNBaseUrl.charAt(d1CNBaseUrl.length - 1) == "/") { + d1CNBaseUrl = d1CNBaseUrl.substring(0, d1CNBaseUrl.length - 1); + this.set("d1CNBaseUrl", d1CNBaseUrl); + } - this.set("nodeServiceUrl", d1CNBaseUrl + this.get("d1CNService") + "/node/"); - this.set('resolveServiceUrl', d1CNBaseUrl + this.get('d1CNService') + '/resolve/'); - this.set("reserveServiceUrl", d1CNBaseUrl + this.get("d1CNService") + "/reserve"); + //Account services + if (typeof this.get("accountsUrl") != "undefined") { + this.set( + "accountsUrl", + d1CNBaseUrl + this.get("d1CNService") + "/accounts/", + ); + + if (typeof this.get("pendingMapsUrl") != "undefined") + this.set( + "pendingMapsUrl", + this.get("accountsUrl") + "pendingmap/", + ); + + if (typeof this.get("accountsMapsUrl") != "undefined") + this.set("accountsMapsUrl", this.get("accountsUrl") + "map/"); + + if (typeof this.get("groupsUrl") != "undefined") + this.set( + "groupsUrl", + d1CNBaseUrl + this.get("d1CNService") + "/groups/", + ); + } - //Token URLs - if(typeof this.get("tokenUrl") != "undefined"){ - this.set("tokenUrl", d1CNBaseUrl + "/portal/" + "token"); + if (typeof this.get("d1LogServiceUrl") != "undefined") + this.set( + "d1LogServiceUrl", + d1CNBaseUrl + this.get("d1CNService") + "/query/logsolr/?", + ); - this.set("checkTokenUrl", d1CNBaseUrl + this.get("d1CNService") + "/diag/subject"); + this.set( + "nodeServiceUrl", + d1CNBaseUrl + this.get("d1CNService") + "/node/", + ); + this.set( + "resolveServiceUrl", + d1CNBaseUrl + this.get("d1CNService") + "/resolve/", + ); + this.set( + "reserveServiceUrl", + d1CNBaseUrl + this.get("d1CNService") + "/reserve", + ); - //The sign-in and out URLs - allow these to be turned off by removing them in the defaults above (hence the check for undefined) - if(this.get("enableCILogonSignIn") || typeof this.get("signInUrl") !== "undefined") - this.set("signInUrl", d1CNBaseUrl + "/portal/" + "startRequest?target="); - if(typeof this.get("signInUrlOrcid") !== "undefined") - this.set("signInUrlOrcid", d1CNBaseUrl + "/portal/" + "oauth?action=start&target="); + //Token URLs + if (typeof this.get("tokenUrl") != "undefined") { + this.set("tokenUrl", d1CNBaseUrl + "/portal/" + "token"); + + this.set( + "checkTokenUrl", + d1CNBaseUrl + this.get("d1CNService") + "/diag/subject", + ); + + //The sign-in and out URLs - allow these to be turned off by removing them in the defaults above (hence the check for undefined) + if ( + this.get("enableCILogonSignIn") || + typeof this.get("signInUrl") !== "undefined" + ) + this.set( + "signInUrl", + d1CNBaseUrl + "/portal/" + "startRequest?target=", + ); + if (typeof this.get("signInUrlOrcid") !== "undefined") + this.set( + "signInUrlOrcid", + d1CNBaseUrl + "/portal/" + "oauth?action=start&target=", + ); + + if (this.get("enableLdapSignIn") && !this.get("signInUrlLdap")) { + this.set( + "signInUrlLdap", + d1CNBaseUrl + "/portal/" + "ldap?target=", + ); + } - if(this.get("enableLdapSignIn") && !this.get("signInUrlLdap")){ - this.set("signInUrlLdap", d1CNBaseUrl + "/portal/" + "ldap?target="); + if (this.get("orcidBaseUrl")) + this.set( + "orcidSearchUrl", + this.get("orcidBaseUrl") + "/v1.1/search/orcid-bio?q=", + ); + + if ( + typeof this.get("signInUrl") !== "undefined" || + typeof this.get("signInUrlOrcid") !== "undefined" + ) + this.set("signOutUrl", d1CNBaseUrl + "/portal/" + "logout"); } + // Object format list + if (typeof this.get("formatsUrl") != "undefined") { + this.set( + "formatsServiceUrl", + d1CNBaseUrl + this.get("d1CNService") + this.get("formatsUrl"), + ); + } - if(this.get('orcidBaseUrl')) - this.set('orcidSearchUrl', this.get('orcidBaseUrl') + '/v1.1/search/orcid-bio?q='); + //ORCID search + if (typeof this.get("orcidBaseUrl") != "undefined") + this.set( + "orcidSearchUrl", + this.get("orcidBaseUrl") + "/search/orcid-bio?q=", + ); + } - if((typeof this.get("signInUrl") !== "undefined") || (typeof this.get("signInUrlOrcid") !== "undefined")) - this.set("signOutUrl", d1CNBaseUrl + "/portal/" + "logout"); + // Metadata quality report services + this.set("mdqSuitesServiceUrl", this.get("mdqBaseUrl") + "/suites/"); + this.set("mdqRunsServiceUrl", this.get("mdqBaseUrl") + "/runs/"); + this.set("mdqScoresServiceUrl", this.get("mdqBaseUrl") + "/scores/"); + //Construct the DataONE Bookkeeper service API URLs + if (this.get("enableBookkeeperServices")) { + this.set( + "bookkeeperSubscriptionsUrl", + this.get("bookkeeperBaseUrl") + "/subscriptions", + ); + this.set( + "bookkeeperCustomersUrl", + this.get("bookkeeperBaseUrl") + "/customers", + ); + this.set( + "bookkeeperQuotasUrl", + this.get("bookkeeperBaseUrl") + "/quotas", + ); + this.set( + "bookkeeperUsagesUrl", + this.get("bookkeeperBaseUrl") + "/usages", + ); } - // Object format list - if ( typeof this.get("formatsUrl") != "undefined" ) { - this.set("formatsServiceUrl", - d1CNBaseUrl + this.get("d1CNService") + this.get("formatsUrl")); + //Construct the Fluid Earth Fever URL + if ( + this.get("enableFeverVisualizations") && + this.get("feverPath") && + !this.get("feverUrl") + ) { + this.set("feverUrl", this.get("baseUrl") + this.get("feverPath")); } - //ORCID search - if(typeof this.get("orcidBaseUrl") != "undefined") - this.set('orcidSearchUrl', this.get('orcidBaseUrl') + '/search/orcid-bio?q='); - - } - - // Metadata quality report services - this.set('mdqSuitesServiceUrl', this.get("mdqBaseUrl") + "/suites/"); - this.set('mdqRunsServiceUrl', this.get('mdqBaseUrl') + "/runs/"); - this.set('mdqScoresServiceUrl', this.get('mdqBaseUrl') + "/scores/"); - - //Construct the DataONE Bookkeeper service API URLs - if( this.get("enableBookkeeperServices") ){ - this.set("bookkeeperSubscriptionsUrl", this.get("bookkeeperBaseUrl") + "/subscriptions"); - this.set("bookkeeperCustomersUrl", this.get("bookkeeperBaseUrl") + "/customers"); - this.set("bookkeeperQuotasUrl", this.get("bookkeeperBaseUrl") + "/quotas"); - this.set("bookkeeperUsagesUrl", this.get("bookkeeperBaseUrl") + "/usages"); - } + this.on("change:pid", this.changePid); - //Construct the Fluid Earth Fever URL - if( this.get("enableFeverVisualizations") && this.get("feverPath") && !this.get("feverUrl") ){ - this.set("feverUrl", this.get("baseUrl") + this.get("feverPath")); - } + //For backward-compatbility, set the theme and themeTitle variables using the + // attributes set on this model, which are taken from the AppConfig + MetacatUI.theme = this.get("theme"); + MetacatUI.themeTitle = this.get("repositoryName"); - this.on("change:pid", this.changePid); - - //For backward-compatbility, set the theme and themeTitle variables using the - // attributes set on this model, which are taken from the AppConfig - MetacatUI.theme = this.get("theme"); - MetacatUI.themeTitle = this.get("repositoryName"); - - //Set up the alternative repositories - _.map(this.get("alternateRepositories"), function(repo){ - repo = _.extend(repo, this.getDataONEMNAPIs(repo.baseURL)); - }, this); - - }, + //Set up the alternative repositories + _.map( + this.get("alternateRepositories"), + function (repo) { + repo = _.extend(repo, this.getDataONEMNAPIs(repo.baseURL)); + }, + this, + ); + }, - /** - * Constructs the DataONE API URLs for the given baseUrl - * @param {string} [baseUrl] - The baseUrl to use in the URLs. If not specified, it uses the AppModel attributes. - * @returns {object} - */ - getDataONEMNAPIs: function(baseUrl){ + /** + * Constructs the DataONE API URLs for the given baseUrl + * @param {string} [baseUrl] - The baseUrl to use in the URLs. If not specified, it uses the AppModel attributes. + * @returns {object} + */ + getDataONEMNAPIs: function (baseUrl) { + var urls = {}; - var urls = {}; + //Get the baseUrl from this model if one isn't given + if (typeof baseUrl == "undefined") { + var baseUrl = this.get("baseUrl"); + } - //Get the baseUrl from this model if one isn't given - if( typeof baseUrl == "undefined" ){ - var baseUrl = this.get("baseUrl"); - } + //Remove a forward slash to the end of the base URL if there is one + if (baseUrl.charAt(baseUrl.length - 1) == "/") { + baseUrl = baseUrl.substring(0, baseUrl.length - 1); + } - //Remove a forward slash to the end of the base URL if there is one - if( baseUrl.charAt( baseUrl.length-1 ) == "/" ){ - baseUrl = baseUrl.substring(0, baseUrl.length-1); - } + //If the baseUrl doesn't have the full DataONE MN API structure, then construct it + if (baseUrl.indexOf("/d1/mn") == -1) { + //Get the Dataone API fragment, which is either "/d1/mn/v2" or "/cn/v2" + var d1Service = this.get("d1Service"); + if (typeof d1Service != "string" || !d1Service.length) { + d1Service = "/d1/mn/v2"; + } else if (d1Service.charAt(0) != "/") { + d1Service = "/" + d1Service; + } - //If the baseUrl doesn't have the full DataONE MN API structure, then construct it - if( baseUrl.indexOf("/d1/mn") == -1 ){ + //Get the Metacat context, and make sure it starts with a forward slash + var context = this.get("context"); + if (typeof context != "string" || !context.length) { + context = ""; + } else if (context.charAt(0) != "/") { + context = "/" + context; + } - //Get the Dataone API fragment, which is either "/d1/mn/v2" or "/cn/v2" - var d1Service = this.get('d1Service'); - if( typeof d1Service != "string" || !d1Service.length ){ - d1Service = "/d1/mn/v2"; + //Construct the base URL + baseUrl = baseUrl + context + d1Service; } - else if( d1Service.charAt(0) != "/" ){ - d1Service = "/" + d1Service; + //Otherwise, just make sure the API version is appended to the base URL + else if (baseUrl.substring(baseUrl.length - 3) != "/v2") { + d1Service = "/d1/mn"; + baseUrl = baseUrl + "/v2"; } - //Get the Metacat context, and make sure it starts with a forward slash - var context = this.get("context"); - if( typeof context != "string" || !context.length ){ - context = ""; - } - else if( context.charAt(0) != "/" ){ - context = "/" + context; + // these are pretty standard, but can be customized if needed + urls.viewServiceUrl = baseUrl + "/views/metacatui/"; + urls.publishServiceUrl = baseUrl + "/publish/"; + urls.authServiceUrl = baseUrl + "/isAuthorized/"; + urls.queryServiceUrl = baseUrl + "/query/solr/?"; + urls.metaServiceUrl = baseUrl + "/meta/"; + urls.packageServiceUrl = + baseUrl + "/packages/" + this.get("packageFormat") + "/"; + + if (d1Service.indexOf("mn") > 0) { + urls.objectServiceUrl = baseUrl + "/object/"; } - //Construct the base URL - baseUrl = baseUrl + context + d1Service; - } - //Otherwise, just make sure the API version is appended to the base URL - else if( baseUrl.substring( baseUrl.length-3 ) != "/v2" ){ - d1Service = "/d1/mn"; - baseUrl = baseUrl + "/v2"; - } - - // these are pretty standard, but can be customized if needed - urls.viewServiceUrl = baseUrl + '/views/metacatui/'; - urls.publishServiceUrl = baseUrl + '/publish/'; - urls.authServiceUrl = baseUrl + '/isAuthorized/'; - urls.queryServiceUrl = baseUrl + '/query/solr/?'; - urls.metaServiceUrl = baseUrl + '/meta/'; - urls.packageServiceUrl = baseUrl + '/packages/'+this.get('packageFormat')+'/'; - - if( d1Service.indexOf("mn") > 0 ){ - urls.objectServiceUrl = baseUrl + '/object/'; - } - - if( this.get("enableMonitorStatus") ){ - urls.monitorStatusUrl = baseUrl + "/monitor/status"; - } - - return urls; + if (this.get("enableMonitorStatus")) { + urls.monitorStatusUrl = baseUrl + "/monitor/status"; + } - }, + return urls; + }, - changePid: function(model, name){ - this.set("previousPid", model.previous("pid")); - }, + changePid: function (model, name) { + this.set("previousPid", model.previous("pid")); + }, - /** - * Gets the currently-active alternative repository that is configured in this AppModel. - * @returns {object} - */ - getActiveAltRepo: function(){ - //Get the alternative repositories to use for uploading objects - var altRepos = this.get("alternateRepositories"), + /** + * Gets the currently-active alternative repository that is configured in this AppModel. + * @returns {object} + */ + getActiveAltRepo: function () { + //Get the alternative repositories to use for uploading objects + var altRepos = this.get("alternateRepositories"), activeAltRepo; - //Get the active alt repo - if( altRepos.length && this.get("activeAlternateRepositoryId") ){ - activeAltRepo = _.findWhere(altRepos, {identifier: this.get("activeAlternateRepositoryId") }); + //Get the active alt repo + if (altRepos.length && this.get("activeAlternateRepositoryId")) { + activeAltRepo = _.findWhere(altRepos, { + identifier: this.get("activeAlternateRepositoryId"), + }); - return activeAltRepo || null; - } - else{ - return null; - } - }, + return activeAltRepo || null; + } else { + return null; + } + }, - /** - * Gets the default alternate repository and sets it as the active alternate repository. - * If a default alt repo ({@link AppConfig#defaultAlternateRepositoryId}) isn't configured, - * the first alt repo in the {@link AppConfig#alternateRepositories} list is used. - * @fires AppModel#change:activeAlternateRepositoryId - */ - setActiveAltRepo: function(){ - //Get the alternative repositories to use for uploading objects - var altRepos = this.get("alternateRepositories"), + /** + * Gets the default alternate repository and sets it as the active alternate repository. + * If a default alt repo ({@link AppConfig#defaultAlternateRepositoryId}) isn't configured, + * the first alt repo in the {@link AppConfig#alternateRepositories} list is used. + * @fires AppModel#change:activeAlternateRepositoryId + */ + setActiveAltRepo: function () { + //Get the alternative repositories to use for uploading objects + var altRepos = this.get("alternateRepositories"), defaultAltRepo; - if( !altRepos.length ){ - return; - } + if (!altRepos.length) { + return; + } - //If a default alt repo is configured, set that as the active alt repo - if( this.get("defaultAlternateRepositoryId") ){ - defaultAltRepo = _.findWhere(altRepos, {identifier: this.get("defaultAlternateRepositoryId") }); - if( defaultAltRepo ){ - this.set("activeAlternateRepositoryId", defaultAltRepo.identifier); + //If a default alt repo is configured, set that as the active alt repo + if (this.get("defaultAlternateRepositoryId")) { + defaultAltRepo = _.findWhere(altRepos, { + identifier: this.get("defaultAlternateRepositoryId"), + }); + if (defaultAltRepo) { + this.set("activeAlternateRepositoryId", defaultAltRepo.identifier); + } } - } - //Otherwise, use the first alt repo in the list - if( !defaultAltRepo ){ - this.set("activeAlternateRepositoryId", altRepos[0].identifier); - } + //Otherwise, use the first alt repo in the list + if (!defaultAltRepo) { + this.set("activeAlternateRepositoryId", altRepos[0].identifier); + } }, /** @@ -2389,13 +2712,13 @@

Source: src/js/models/AppModel.js

"Content-Type": "application/json", }, }) - .then((response) => response.json()) - .then((data) => { - this.set("quickAddTaxa", data); - }) - .catch((error) => { - console.log("Error fetching taxa", error); - }); + .then((response) => response.json()) + .then((data) => { + this.set("quickAddTaxa", data); + }) + .catch((error) => { + console.log("Error fetching taxa", error); + }); }, /** @@ -2412,21 +2735,21 @@

Source: src/js/models/AppModel.js

*/ addCSS: function (css, id) { try { - if (!MetacatUI.loadedCSS) { - MetacatUI.loadedCSS = [] - } - if (!MetacatUI.loadedCSS.includes(id)) { - MetacatUI.loadedCSS.push(id); - var style = document.createElement('style'); + if (!MetacatUI.loadedCSS) { + MetacatUI.loadedCSS = []; + } + if (!MetacatUI.loadedCSS.includes(id)) { + MetacatUI.loadedCSS.push(id); + var style = document.createElement("style"); style.id = id; - style.appendChild(document.createTextNode(css)); - document.querySelector("head").appendChild(style); + style.appendChild(document.createTextNode(css)); + document.querySelector("head").appendChild(style); } - } - catch (error) { + } catch (error) { console.log( - 'There was an error adding CSS to the app' + - '. Error details: ' + error + "There was an error adding CSS to the app" + + ". Error details: " + + error, ); } }, @@ -2441,20 +2764,20 @@

Source: src/js/models/AppModel.js

removeCSS: function (id) { try { if (!MetacatUI.loadedCSS) { - MetacatUI.loadedCSS = [] + MetacatUI.loadedCSS = []; } if (MetacatUI.loadedCSS.includes(id)) { - MetacatUI.loadedCSS = MetacatUI.loadedCSS.filter(e => e !== id); - var sheet = document.querySelector("head #" + id) + MetacatUI.loadedCSS = MetacatUI.loadedCSS.filter((e) => e !== id); + var sheet = document.querySelector("head #" + id); if (sheet) { - sheet.remove() + sheet.remove(); } } - } - catch (error) { + } catch (error) { console.log( - 'There was an error removing CSS from the app' + - '. Error details: ' + error + "There was an error removing CSS from the app" + + ". Error details: " + + error, ); } }, @@ -2544,8 +2867,8 @@

Source: src/js/models/AppModel.js

if (doiURLMatch) return "doi:" + doiURLMatch[3]; return ""; }, - - }); + }, + ); return AppModel; });
diff --git a/docs/docs/src_js_models_CitationModel.js.html b/docs/docs/src_js_models_CitationModel.js.html index a60a60b8d..d26e9d53a 100644 --- a/docs/docs/src_js_models_CitationModel.js.html +++ b/docs/docs/src_js_models_CitationModel.js.html @@ -44,14 +44,13 @@

Source: src/js/models/CitationModel.js

-
/* global define */
-"use strict";
+            
"use strict";
 
 define(["jquery", "underscore", "backbone", "collections/Citations"], function (
   $,
   _,
   Backbone,
-  Citations
+  Citations,
 ) {
   /**
    * @class CitationModel
@@ -213,7 +212,7 @@ 

Source: src/js/models/CitationModel.js

delete item.origin; // Format the authors in the origin array item.originArray = item.originArray.map((author) => - this.formatAuthor(author) + this.formatAuthor(author), ); // Get the publish year const date = @@ -241,7 +240,7 @@

Source: src/js/models/CitationModel.js

} catch (error) { console.log( "Error parsing a CitationModel. Returning response as-is.", - error + error, ); return response; } @@ -336,7 +335,7 @@

Source: src/js/models/CitationModel.js

this.listenTo( attrs.citationMetadata, "update", - this.trigger.bind(this, "change") + this.trigger.bind(this, "change"), ); } } @@ -348,7 +347,7 @@

Source: src/js/models/CitationModel.js

"Error in custom set() method on CitationModel. Will attempt to set" + " using with Backbone set(). Attributes and error stack trace:", { key, val, options }, - error + error, ); Backbone.Model.prototype.set.call(this, key, val, options); } @@ -396,7 +395,7 @@

Source: src/js/models/CitationModel.js

Backbone.Model.prototype.set.call( this, "sourceModel", - newSourceModel + newSourceModel, ); this.populateFromModel(newSourceModel); } catch (error) { @@ -440,7 +439,7 @@

Source: src/js/models/CitationModel.js

"Error populating a CitationModel from the model: ", newSourceModel, " Error: ", - error + error, ); } }, @@ -464,7 +463,7 @@

Source: src/js/models/CitationModel.js

console.log( "Error getting year from the sourceModel. Model and error:", sourceModel, - error + error, ); return this.defaults().year_of_publishing; } @@ -499,7 +498,7 @@

Source: src/js/models/CitationModel.js

console.log( "Error getting title from the sourceModel. Model and error:", sourceModel, - error + error, ); return this.defaults().title; } @@ -538,7 +537,7 @@

Source: src/js/models/CitationModel.js

console.log( "Error getting journal from the sourceModel. Model and error:", sourceModel, - error + error, ); return this.defaults().journal; } @@ -582,7 +581,7 @@

Source: src/js/models/CitationModel.js

console.log( "Error getting originArray from the sourceModel. Model and error:", sourceModel, - error + error, ); return this.defaults().originArray; } @@ -603,7 +602,7 @@

Source: src/js/models/CitationModel.js

console.log( "Error getting the pid from the sourceModel. Model and error:", sourceModel, - error + error, ); return this.defaults().pid; } @@ -624,7 +623,7 @@

Source: src/js/models/CitationModel.js

console.log( "Error getting the seriesId from the sourceModel. Model and error:", sourceModel, - error + error, ); return this.defaults().seriesId; } @@ -651,7 +650,7 @@

Source: src/js/models/CitationModel.js

console.log( "Error getting the viewUrl from the sourceModel. Model and error:", sourceModel, - error + error, ); return this.defaults().viewUrl; } @@ -681,7 +680,7 @@

Source: src/js/models/CitationModel.js

console.log( "There was an error formatting an author, returning " + "the author input as is.", - error + error, ); return author; } @@ -768,7 +767,7 @@

Source: src/js/models/CitationModel.js

// Any remaining lowercase words are assumed to be non-dropping particles const nonDroppingParticles = parts.filter((part) => - part.match(/^[a-z]+$/) + part.match(/^[a-z]+$/), ); if (nonDroppingParticles.length > 0) { name["non-dropping-particle"] = nonDroppingParticles.join(" "); @@ -810,7 +809,7 @@

Source: src/js/models/CitationModel.js

} catch (error) { console.log( "There was an error getting the year from the date, returning null.", - error + error, ); return null; } @@ -826,7 +825,7 @@

Source: src/js/models/CitationModel.js

try { if (!orcid) return false; const regex = new RegExp( - "^https?:\\/\\/orcid.org\\/(\\d{4}-){3}(\\d{3}[0-9X])$" + "^https?:\\/\\/orcid.org\\/(\\d{4}-){3}(\\d{3}[0-9X])$", ); return regex.test(orcid); } catch { @@ -868,7 +867,7 @@

Source: src/js/models/CitationModel.js

} catch (error) { console.log( "There was an error getting the name from the orcid.", - error + error, ); } }, @@ -894,7 +893,7 @@

Source: src/js/models/CitationModel.js

console.log( `There was an error checking if the citation is from node ${node}.` + `Returning false.`, - error + error, ); return false; } @@ -918,7 +917,7 @@

Source: src/js/models/CitationModel.js

} catch (error) { console.log( "There was an error converting the origin string to an array.", - error + error, ); return this.defaults().originArray; } @@ -954,7 +953,7 @@

Source: src/js/models/CitationModel.js

} catch (error) { console.log( "There was an error converting the origin array to a string.", - error + error, ); return this.defaults().origin; } @@ -1070,7 +1069,7 @@

Source: src/js/models/CitationModel.js

} catch (error) { console.log( "There was an error finding the DOI for the citation. Returning null", - error + error, ); return null; } @@ -1130,7 +1129,7 @@

Source: src/js/models/CitationModel.js

} return ""; }, - } + }, ); return Citation; diff --git a/docs/docs/src_js_models_CollectionModel.js.html b/docs/docs/src_js_models_CollectionModel.js.html index b39f56994..8c0dc2d84 100644 --- a/docs/docs/src_js_models_CollectionModel.js.html +++ b/docs/docs/src_js_models_CollectionModel.js.html @@ -44,715 +44,744 @@

Source: src/js/models/CollectionModel.js

-
/* global define */
-define(["jquery",
-        "underscore",
-        "backbone",
-        "uuid",
-        "collections/Filters",
-        "collections/SolrResults",
-        "models/DataONEObject",
-        "models/filters/Filter",
-        "models/filters/FilterGroup",
-        "models/Search",
-      ],
-    function($, _, Backbone, uuid, Filters, SolrResults, DataONEObject, Filter, FilterGroup, Search) {
-
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "uuid",
+  "collections/Filters",
+  "collections/SolrResults",
+  "models/DataONEObject",
+  "models/filters/Filter",
+  "models/filters/FilterGroup",
+  "models/Search",
+], function (
+  $,
+  _,
+  Backbone,
+  uuid,
+  Filters,
+  SolrResults,
+  DataONEObject,
+  Filter,
+  FilterGroup,
+  Search,
+) {
   /**
-  * @class CollectionModel
-  * @classdesc A collection of datasets, defined by one or more search filters
-  * @classcategory Models
-  * @name CollectionModel
-  * @extends DataONEObject
-  * @constructor
-  */
-	var CollectionModel = DataONEObject.extend(
-    /** @lends CollectionModel.prototype */{
-
-    /**
-    * The name of this Model
-    * @type {string}
-    * @readonly
-    */
-    type: "Collection",
-
-    /**
-    * Default attributes for CollectionModels
-    * @type {Object}
-    * @property {string[]} ignoreQueryGroups - Deprecated
-    * @property {FilterGroup} definition - The parent-level Filter Group model that represents the collection definition.
-    * @property {Filters} definitionFilters - A Filters collection that stores definition filters that have been serialized to the Collection. The same filters that are stored in the definition.
-    * @property {Search} searchModel - A Search model with a Filters collection that contains the filters associated with this collection
-    * @property {SolrResults} searchResults - A SolrResults collection that contains the filtered search results of datasets in this collection
-    * @property {SolrResults} allSearchResults - A SolrResults collection that contains the unfiltered search results of all datasets in this collection
-    */
-    defaults: function(){
-      return _.extend(DataONEObject.prototype.defaults(), {
-        name: null,
-        label: null,
-        originalLabel: null,
-        labelBlockList: ["new"],
-        description: null,
-        formatId: "https://purl.dataone.org/collections-1.1.0",
-        formatType: "METADATA",
-        type: "collection",
-        definition: null,
-        definitionFilters: null,
-        searchModel: null,
-        searchResults: new SolrResults(),
-        allSearchResults: null
-      });
-    },
-
-    /**
-     * The default Backbone.Model.initialize() function
-    */
-    initialize: function(options){
-
-      //Call the super class initialize function
-      DataONEObject.prototype.initialize.call(this, options);
-
-      this.listenToOnce(this.get("searchResults"), "sync", this.cacheSearchResults);
-
-      //If the searchResults collection is replaced at any time, reset the listener
-      this.off("change:searchResults")
-      this.on("change:searchResults", function(searchResults){
-        this.listenToOnce(this.get("searchResults"), "sync", this.cacheSearchResults);
-      });
-      
-      // Update the search model when the definition filters are updated.
-      // Definition filters may be updated by the user in the Query Builder,
-      // or they may be updated automatically by this model (e.g. when adding
-      // an isPartOf filter).
-      this.off("change:definition");
-      this.on("change:definition", function(){
-        this.stopListening(this.get("definition"), "update change");
-        this.listenTo(
-          this.get("definition"),
-          "update change",
-          this.updateSearchModel
+   * @class CollectionModel
+   * @classdesc A collection of datasets, defined by one or more search filters
+   * @classcategory Models
+   * @name CollectionModel
+   * @extends DataONEObject
+   * @constructor
+   */
+  var CollectionModel = DataONEObject.extend(
+    /** @lends CollectionModel.prototype */ {
+      /**
+       * The name of this Model
+       * @type {string}
+       * @readonly
+       */
+      type: "Collection",
+
+      /**
+       * Default attributes for CollectionModels
+       * @type {Object}
+       * @property {string[]} ignoreQueryGroups - Deprecated
+       * @property {FilterGroup} definition - The parent-level Filter Group model that represents the collection definition.
+       * @property {Filters} definitionFilters - A Filters collection that stores definition filters that have been serialized to the Collection. The same filters that are stored in the definition.
+       * @property {Search} searchModel - A Search model with a Filters collection that contains the filters associated with this collection
+       * @property {SolrResults} searchResults - A SolrResults collection that contains the filtered search results of datasets in this collection
+       * @property {SolrResults} allSearchResults - A SolrResults collection that contains the unfiltered search results of all datasets in this collection
+       */
+      defaults: function () {
+        return _.extend(DataONEObject.prototype.defaults(), {
+          name: null,
+          label: null,
+          originalLabel: null,
+          labelBlockList: ["new"],
+          description: null,
+          formatId: "https://purl.dataone.org/collections-1.1.0",
+          formatType: "METADATA",
+          type: "collection",
+          definition: null,
+          definitionFilters: null,
+          searchModel: null,
+          searchResults: new SolrResults(),
+          allSearchResults: null,
+        });
+      },
+
+      /**
+       * The default Backbone.Model.initialize() function
+       */
+      initialize: function (options) {
+        //Call the super class initialize function
+        DataONEObject.prototype.initialize.call(this, options);
+
+        this.listenToOnce(
+          this.get("searchResults"),
+          "sync",
+          this.cacheSearchResults,
         );
-      }, this);
 
-      //Create a search model
-      this.set("searchModel", this.createSearchModel());
+        //If the searchResults collection is replaced at any time, reset the listener
+        this.off("change:searchResults");
+        this.on("change:searchResults", function (searchResults) {
+          this.listenToOnce(
+            this.get("searchResults"),
+            "sync",
+            this.cacheSearchResults,
+          );
+        });
 
-      // Create a Filters collection to store the definition filters. Add the catalog
-      // search query fragment to the definition Filter Group model.
-      this.set("definition", new FilterGroup({ catalogSearch: true, nodeName: "definition" }));
-      this.set("definitionFilters", this.get("definition").get("filters"));
+        // Update the search model when the definition filters are updated.
+        // Definition filters may be updated by the user in the Query Builder,
+        // or they may be updated automatically by this model (e.g. when adding
+        // an isPartOf filter).
+        this.off("change:definition");
+        this.on(
+          "change:definition",
+          function () {
+            this.stopListening(this.get("definition"), "update change");
+            this.listenTo(
+              this.get("definition"),
+              "update change",
+              this.updateSearchModel,
+            );
+          },
+          this,
+        );
 
-      // Update the blocklist with the node/repository labels
-      var nodeBlockList = MetacatUI.appModel.get("portalLabelBlockList");
-      if (nodeBlockList !== undefined && Array.isArray(nodeBlockList)) {
-        this.set("labelBlockList", this.get("labelBlockList").concat(nodeBlockList));
-      }
+        //Create a search model
+        this.set("searchModel", this.createSearchModel());
 
-    },
-    
-    /**    
-     * updateSearchModel - This function is called when any changes are made to
-     * the definition filters. The function adds, removes, or updates models
-     * in the Search Model filters when models are changed in the collection
-     * Definition Filters.
-     *      
-     * @param  {Filter|Filters} model The model or collection that has been
-     * changed (filter models) or updated (filters collection). This is ignored.
-     * @param  {object} record     The data passed by backbone that indicates
-     * which models have been added, removed, or updated. If the only change was
-     * to a pre-existing model attribute, then the object will be empty.   
-     */     
-    updateSearchModel: function(model, record){
-      
-      try {
-        var model = this;
-        
-        // Merge the updated definition Filter Group model with the Search Model collection.
-        this.get("searchModel").get("filters").add(
-          model.get("definition"),
-          { merge: true }
+        // Create a Filters collection to store the definition filters. Add the catalog
+        // search query fragment to the definition Filter Group model.
+        this.set(
+          "definition",
+          new FilterGroup({ catalogSearch: true, nodeName: "definition" }),
         );
+        this.set("definitionFilters", this.get("definition").get("filters"));
+
+        // Update the blocklist with the node/repository labels
+        var nodeBlockList = MetacatUI.appModel.get("portalLabelBlockList");
+        if (nodeBlockList !== undefined && Array.isArray(nodeBlockList)) {
+          this.set(
+            "labelBlockList",
+            this.get("labelBlockList").concat(nodeBlockList),
+          );
+        }
+      },
+
+      /**
+       * updateSearchModel - This function is called when any changes are made to
+       * the definition filters. The function adds, removes, or updates models
+       * in the Search Model filters when models are changed in the collection
+       * Definition Filters.
+       *
+       * @param  {Filter|Filters} model The model or collection that has been
+       * changed (filter models) or updated (filters collection). This is ignored.
+       * @param  {object} record     The data passed by backbone that indicates
+       * which models have been added, removed, or updated. If the only change was
+       * to a pre-existing model attribute, then the object will be empty.
+       */
+      updateSearchModel: function (model, record) {
+        try {
+          var model = this;
+
+          // Merge the updated definition Filter Group model with the Search Model collection.
+          this.get("searchModel")
+            .get("filters")
+            .add(model.get("definition"), { merge: true });
+        } catch (e) {
+          console.log(
+            "Failed to update the Search Model collection when the " +
+              "Definition Model collection changed, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       *
+       *
+       */
+      url: function () {
+        return (
+          MetacatUI.appModel.get("objectServiceUrl") +
+          encodeURIComponent(this.get("id"))
+        );
+      },
+
+      /**
+       * Overrides the default Backbone.Model.fetch() function to provide some custom
+       * fetch options
+       *
+       */
+      fetch: function () {
+        var model = this;
 
-      } catch (e) {
-        console.log("Failed to update the Search Model collection when the " +
-        "Definition Model collection changed, error message: " + e);
-      }
-    },
-
-    /**
-     *
-     *
-    */
-    url: function(){
-      return MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(this.get("id"));
-    },
+        var requestSettings = {
+          dataType: "xml",
+          error: function () {
+            model.trigger("error");
+          },
+        };
 
-    /**
-     * Overrides the default Backbone.Model.fetch() function to provide some custom
-     * fetch options
-     *
-    */
-    fetch: function(){
-      var model = this;
-
-      var requestSettings = {
-        dataType: "xml",
-        error: function(){
-          model.trigger("error");
+        //Add the authorization header and other AJAX settings
+        requestSettings = _.extend(
+          requestSettings,
+          MetacatUI.appUserModel.createAjaxSettings(),
+        );
+        return Backbone.Model.prototype.fetch.call(this, requestSettings);
+      },
+
+      /**
+       * Sends an AJAX request to fetch the system metadata for this object.
+       * Will not trigger a sync event since it does not use Backbone.Model.fetch
+       */
+      fetchSystemMetadata: function (options) {
+        if (!options) var options = {};
+        else options = _.clone(options);
+
+        //Get the active alternative repository, if one is configured
+        var activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
+
+        if (activeAltRepo) {
+          baseUrl = activeAltRepo.metaServiceUrl;
+        } else {
+          baseUrl = MetacatUI.appModel.get("metaServiceUrl");
         }
-      }
 
-      //Add the authorization header and other AJAX settings
-      requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());
-      return Backbone.Model.prototype.fetch.call(this, requestSettings);
-    },
+        //Exit if no base URL was found
+        if (!baseUrl) {
+          return;
+        }
 
-    /**
-     * Sends an AJAX request to fetch the system metadata for this object.
-     * Will not trigger a sync event since it does not use Backbone.Model.fetch
-     */
-    fetchSystemMetadata: function(options){
-
-      if(!options) var options = {};
-      else options = _.clone(options);
-
-      //Get the active alternative repository, if one is configured
-      var activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
-
-      if( activeAltRepo ){
-        baseUrl = activeAltRepo.metaServiceUrl;
-      }
-      else{
-        baseUrl = MetacatUI.appModel.get("metaServiceUrl");
-      }
-
-      //Exit if no base URL was found
-      if( !baseUrl ){
-        return;
-      }
-
-      var model = this,
-        fetchOptions = _.extend({
-          url: baseUrl + encodeURIComponent(this.get("id") || this.get("seriesId")),
-          dataType: "text",
-          success: function(response){
-            model.set(DataONEObject.prototype.parse.call(model, response));
-            model.trigger("systemMetadataSync");
-          },
-          error: function(){
-            model.trigger('error');
-          }
-        }, options);
+        var model = this,
+          fetchOptions = _.extend(
+            {
+              url:
+                baseUrl +
+                encodeURIComponent(this.get("id") || this.get("seriesId")),
+              dataType: "text",
+              success: function (response) {
+                model.set(DataONEObject.prototype.parse.call(model, response));
+                model.trigger("systemMetadataSync");
+              },
+              error: function () {
+                model.trigger("error");
+              },
+            },
+            options,
+          );
 
         //Add the authorization header and other AJAX settings
         _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings());
 
         $.ajax(fetchOptions);
-    },
-
-    /**
-     * Overrides the default Backbone.Model.parse() function to parse the custom
-     * collection XML document
-     *
-     * @param {XMLDocument} response - The XMLDocument returned from the fetch() AJAX call
-     * @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
-    */
-    parse: function(json){
-
-      //Start the empty JSON object
-      var modelJSON = {},
+      },
+
+      /**
+       * Overrides the default Backbone.Model.parse() function to parse the custom
+       * collection XML document
+       *
+       * @param {XMLDocument} response - The XMLDocument returned from the fetch() AJAX call
+       * @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
+       */
+      parse: function (json) {
+        //Start the empty JSON object
+        var modelJSON = {},
           collectionNode;
 
-      //Iterate over each root XML node to find the collection node
-      $(response).children().each(function(i, el){
-        if( el.tagName.indexOf("collection") > -1 ){
-          collectionNode = el;
-          return false;
+        //Iterate over each root XML node to find the collection node
+        $(response)
+          .children()
+          .each(function (i, el) {
+            if (el.tagName.indexOf("collection") > -1) {
+              collectionNode = el;
+              return false;
+            }
+          });
+
+        //If a collection XML node wasn't found, return an empty JSON object
+        if (typeof collectionNode == "undefined" || !collectionNode) return {};
+
+        //Parse the collection XML and return it
+        return this.parseCollectionXML(collectionNode);
+      },
+
+      /**
+       * Parses the collection XML into a JSON object
+       *
+       * @param {Element} rootNode - The XML Element that contains all the collection nodes
+       * @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
+       */
+      parseCollectionXML: function (rootNode) {
+        // Get and save the namespace version number. It should be 1.0.0 or 1.1.0. Version
+        // 1.0.0 portals will be updated to 1.1.0 on save. We need to know which version
+        // while parsing to keep rendering of the old versions of collections/portals
+        // consistent with how they were rendered before MetacatUI was updated to handle
+        // 1.1.0 collections/portals - e.g. the fieldsOperator attribute in filters.
+        var namespace = rootNode.namespaceURI,
+          versionRegex = /\d\.\d\.\d$/g,
+          version = namespace.match(versionRegex);
+        if (version && version.length && version[0] != "") {
+          this.set("originalVersion", version[0]);
         }
-      });
 
-      //If a collection XML node wasn't found, return an empty JSON object
-      if( typeof collectionNode == "undefined" || !collectionNode )
-        return {};
+        var modelJSON = {};
 
-      //Parse the collection XML and return it
-      return this.parseCollectionXML(collectionNode);
+        //Parse the simple text nodes
+        modelJSON.name = this.parseTextNode(rootNode, "name");
+        modelJSON.label = this.parseTextNode(rootNode, "label");
+        modelJSON.description = this.parseTextNode(rootNode, "description");
 
-    },
+        //Create a Filters collection to contain the collection definition Filters
+        var definitionXML = rootNode.getElementsByTagName("definition")[0];
+        // Add the catalog search query fragment to the definition Filter Group model
+        modelJSON.definition = new FilterGroup({
+          objectDOM: definitionXML,
+          catalogSearch: true,
+        });
+        modelJSON.definitionFilters = modelJSON.definition.get("filters");
 
-    /**
-     * Parses the collection XML into a JSON object
-     *
-     * @param {Element} rootNode - The XML Element that contains all the collection nodes
-     * @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
-    */
-    parseCollectionXML: function( rootNode ){
-
-      // Get and save the namespace version number. It should be 1.0.0 or 1.1.0. Version
-      // 1.0.0 portals will be updated to 1.1.0 on save. We need to know which version
-      // while parsing to keep rendering of the old versions of collections/portals
-      // consistent with how they were rendered before MetacatUI was updated to handle
-      // 1.1.0 collections/portals - e.g. the fieldsOperator attribute in filters.
-      var namespace = rootNode.namespaceURI,
-          versionRegex = /\d\.\d\.\d$/g,
-          version = namespace.match(versionRegex);
-      if(version && version.length && version[0] != ""){
-        this.set("originalVersion", version[0])
-      }
-
-      var modelJSON = {};
-
-      //Parse the simple text nodes
-      modelJSON.name = this.parseTextNode(rootNode, "name");
-      modelJSON.label = this.parseTextNode(rootNode, "label");
-      modelJSON.description = this.parseTextNode(rootNode, "description");
-      
-      //Create a Filters collection to contain the collection definition Filters
-      var definitionXML = rootNode.getElementsByTagName("definition")[0]
-      // Add the catalog search query fragment to the definition Filter Group model
-      modelJSON.definition = new FilterGroup({ objectDOM: definitionXML, catalogSearch: true });
-      modelJSON.definitionFilters = modelJSON.definition.get("filters")
-
-      //Create a Search model for this collection's filters
-      modelJSON.searchModel = this.createSearchModel();
-      // Add all the filters from the Collection definition to the search model as a single
-      // Filter Group model.
-      modelJSON.searchModel.get("filters").add(modelJSON.definition);
-
-      // If we are parsing the first version of a collection or portal
-      if(this.get("originalVersion") === "1.0.0"){
-        modelJSON = this.updateTo110(modelJSON);
-      }
-
-      return modelJSON
-    },
-    
-    /**
-     * Takes parsed Collections 1.0.0 XML in JSON format and makes any changes required so
-     * that collections are still represented in MetacatUI as they were before MetacatUI
-     * was updated to support 1.1.0 Collections.
-     * @param {JSON} modelJSON Parsed 1.0.0 Collections XML, in JSON
-     * @return {JSON} The updated JSON, compatible with 1.1.0 changes
-     */
-    updateTo110: function(modelJSON){
-      try {
-        // For version 1.0.0 filters, MetacatUI used the "operator" attribute to set the
-        // operator between both fields and values. In 1.1.0, we now have the
-        // "fieldsOperator" attribute. (Since "AND" was the default, only the "OR"
-        // operator is ever serialized). Therefore, if a version 1.0.0 filter has "OR" as
-        // the operator, we should also set the "fieldOperator" to "OR".
-        modelJSON.definitionFilters.each(function(filterModel){
-          if(filterModel.get("operator") === "OR"){
-            filterModel.set("fieldsOperator", "OR")
-          }
-        }, this);
-        return modelJSON
-      } catch (error) {
-        console.log("Error trying to update a 1.0.0 Collection to 1.1.0 " + 
-        "returning the JSON unchanged. Error details: " + error );
-        return modelJSON
-      }
-    },
+        //Create a Search model for this collection's filters
+        modelJSON.searchModel = this.createSearchModel();
+        // Add all the filters from the Collection definition to the search model as a single
+        // Filter Group model.
+        modelJSON.searchModel.get("filters").add(modelJSON.definition);
 
-    /**
-     * Generate a UUID, reserve it using the DataOne API, and set it on the model
-     */
-    reserveSeriesId: function(){
-
-      // Create a new series ID
-      var seriesId = "urn:uuid:" + uuid.v4();
-
-      // Set the seriesId on the portal model right away, since reserving takes
-      // time. This will be updated in the rare case that the first seriesId was
-      // already taken.
-      this.set("seriesId", seriesId);
-
-      // Reserve a series ID for the new portal
-      var model = this;
-      var options = {
-        url: MetacatUI.appModel.get("reserveServiceUrl"),
-        type: "POST",
-        data: { pid: seriesId },
-        tryCount : 0,
-        // If a generated seriesId is already reserved, how many times to retry
-        retryLimit : 5,
-        success: function(xhr){
-          // If the first seriesId was taken, then update the model with the
-          // successfully reserved seriesId.
-          if(this.tryCount > 0) {
-            model.set("seriesId", $(xhr).find("identifier").text());
-          }
-        },
-        error : function(xhr) {
-          // If the seriesId was already reserved, try again
-          if (xhr.status == 409) {
-              this.tryCount++;
-              if (this.tryCount <= this.retryLimit) {
-                  // Generate another seriesId
-                  this.data = { pid:"urn:uuid:" + uuid.v4() };
-                  // Send the reserve request again
-                  $.ajax(this);
-                  return;
-              }
-              return;
-          // If the user isn't logged in, or doesn't have write access
-          } else if (xhr.status = 401 ){
-            model.set("isAuthorized", false);
-          } else {
-            parsedResponse = $(xhr.responseText).not("style, title").text();
-            model.set("errorMessage", parsedResponse);
-          }
+        // If we are parsing the first version of a collection or portal
+        if (this.get("originalVersion") === "1.0.0") {
+          modelJSON = this.updateTo110(modelJSON);
         }
-      }
-      _.extend(options, MetacatUI.appUserModel.createAjaxSettings());
-      $.ajax(options);
-    },
 
-    /**
-     * Creates a FilterModel with isPartOf as the field and this collection's
-     * seriesId as the value, then adds it to the definitionFilters collection.
-     * (which will also add it to the searchFilters collection)
-     * @param {string} [seriesId] - the seriesId of the collection or portal
-     * @return {Filter} The new isPartOf filter that was created
-     */
-    addIsPartOfFilter: function(seriesId){
-      
-      try {
-        // If no seriesId is given
-        if(!seriesId){
-          // Use the seriesId set on the model
-          if(this.get("seriesId")){
-            seriesId = this.get("seriesId");
-          // If there's no seriesId on the model, make and reserve one
-          } else {
-            //Create and reserve a new seriesId
-            this.reserveSeriesId();
-            seriesId = this.get("seriesId");
-
-            // Set a listener to create an isPartOf filter using the seriesId once
-            // the series Id is set. Just in case the first seriesId generated was
-            // already reserved, update the isPartOf filters on the subsequent
-            // attempts to create and resere an ID.
-            this.on("change:seriesId", function(seriesId){
-              this.addIsPartOfFilter();
-            });
-
-          }
+        return modelJSON;
+      },
+
+      /**
+       * Takes parsed Collections 1.0.0 XML in JSON format and makes any changes required so
+       * that collections are still represented in MetacatUI as they were before MetacatUI
+       * was updated to support 1.1.0 Collections.
+       * @param {JSON} modelJSON Parsed 1.0.0 Collections XML, in JSON
+       * @return {JSON} The updated JSON, compatible with 1.1.0 changes
+       */
+      updateTo110: function (modelJSON) {
+        try {
+          // For version 1.0.0 filters, MetacatUI used the "operator" attribute to set the
+          // operator between both fields and values. In 1.1.0, we now have the
+          // "fieldsOperator" attribute. (Since "AND" was the default, only the "OR"
+          // operator is ever serialized). Therefore, if a version 1.0.0 filter has "OR" as
+          // the operator, we should also set the "fieldOperator" to "OR".
+          modelJSON.definitionFilters.each(function (filterModel) {
+            if (filterModel.get("operator") === "OR") {
+              filterModel.set("fieldsOperator", "OR");
+            }
+          }, this);
+          return modelJSON;
+        } catch (error) {
+          console.log(
+            "Error trying to update a 1.0.0 Collection to 1.1.0 " +
+              "returning the JSON unchanged. Error details: " +
+              error,
+          );
+          return modelJSON;
         }
+      },
 
-        // Create the new isPartOf filter attributes object
-        // NOTE: All other attributes are set in Filter.initialize();
-        var isPartOfAttributes = {
-          fields: ["isPartOf"],
-          values: [seriesId],
-          matchSubstring: false,
-          operator: "OR"
-        };
-        
-        // Remove any existing isPartOf filters, and add the new isPartOf filter
-      
-        // NOTE:
-        // 1. Changes to the definition filters will automatically update the
-        // Search Model filters because of the listener set in initialize().
-        // 2. Add the new Filter model by passing a list of attributes to the
-        // Filters collection, instead of by passing a new Filter, in order
-        // to trigger 'update' and 'change' events for other models and views.
-        
-        this.get("definitionFilters").removeFiltersByField("isPartOf");
-        var filterModel = this.get("definitionFilters").add(isPartOfAttributes);
-        
-        return filterModel
-      } catch (e) {
-        console.log("Failed to create and add a new isPartOf Filter, error message: " + e);
-      }
-      
-    },
+      /**
+       * Generate a UUID, reserve it using the DataOne API, and set it on the model
+       */
+      reserveSeriesId: function () {
+        // Create a new series ID
+        var seriesId = "urn:uuid:" + uuid.v4();
 
-    /**
-     * Gets the text content of the XML node matching the given node name
-     *
-     * @param {Element} parentNode - The parent node to select from
-     * @param {string} nodeName - The name of the XML node to parse
-     * @param {boolean} isMultiple - If true, parses the nodes into an array
-     * @return {(string|Array)} - Returns a string or array of strings of the text content
-    */
-    parseTextNode: function( parentNode, nodeName, isMultiple ){
-      var node = $(parentNode).children(nodeName);
-
-      //If no matching nodes were found, return falsey values
-      if( !node || !node.length ){
-
-        //Return an empty array if the isMultiple flag is true
-        if( isMultiple )
-          return [];
-        //Return null if the isMultiple flag is false
-        else
-          return null;
-      }
-      //If exactly one node is found and we are only expecting one, return the text content
-      else if( node.length == 1 && !isMultiple ){
-        return node[0].textContent.trim();
-      }
-      //If more than one node is found, parse into an array
-      else{
-
-        return _.map(node, function(node){
-          return node.textContent.trim() || null;
-        });
+        // Set the seriesId on the portal model right away, since reserving takes
+        // time. This will be updated in the rare case that the first seriesId was
+        // already taken.
+        this.set("seriesId", seriesId);
 
-      }
-    },
+        // Reserve a series ID for the new portal
+        var model = this;
+        var options = {
+          url: MetacatUI.appModel.get("reserveServiceUrl"),
+          type: "POST",
+          data: { pid: seriesId },
+          tryCount: 0,
+          // If a generated seriesId is already reserved, how many times to retry
+          retryLimit: 5,
+          success: function (xhr) {
+            // If the first seriesId was taken, then update the model with the
+            // successfully reserved seriesId.
+            if (this.tryCount > 0) {
+              model.set("seriesId", $(xhr).find("identifier").text());
+            }
+          },
+          error: function (xhr) {
+            // If the seriesId was already reserved, try again
+            if (xhr.status == 409) {
+              this.tryCount++;
+              if (this.tryCount <= this.retryLimit) {
+                // Generate another seriesId
+                this.data = { pid: "urn:uuid:" + uuid.v4() };
+                // Send the reserve request again
+                $.ajax(this);
+                return;
+              }
+              return;
+              // If the user isn't logged in, or doesn't have write access
+            } else if ((xhr.status = 401)) {
+              model.set("isAuthorized", false);
+            } else {
+              parsedResponse = $(xhr.responseText).not("style, title").text();
+              model.set("errorMessage", parsedResponse);
+            }
+          },
+        };
+        _.extend(options, MetacatUI.appUserModel.createAjaxSettings());
+        $.ajax(options);
+      },
+
+      /**
+       * Creates a FilterModel with isPartOf as the field and this collection's
+       * seriesId as the value, then adds it to the definitionFilters collection.
+       * (which will also add it to the searchFilters collection)
+       * @param {string} [seriesId] - the seriesId of the collection or portal
+       * @return {Filter} The new isPartOf filter that was created
+       */
+      addIsPartOfFilter: function (seriesId) {
+        try {
+          // If no seriesId is given
+          if (!seriesId) {
+            // Use the seriesId set on the model
+            if (this.get("seriesId")) {
+              seriesId = this.get("seriesId");
+              // If there's no seriesId on the model, make and reserve one
+            } else {
+              //Create and reserve a new seriesId
+              this.reserveSeriesId();
+              seriesId = this.get("seriesId");
+
+              // Set a listener to create an isPartOf filter using the seriesId once
+              // the series Id is set. Just in case the first seriesId generated was
+              // already reserved, update the isPartOf filters on the subsequent
+              // attempts to create and resere an ID.
+              this.on("change:seriesId", function (seriesId) {
+                this.addIsPartOfFilter();
+              });
+            }
+          }
 
-    /**
-     * Updates collection XML with values in the collection model
-     *
-     * @param {XMLDocument} objectDOM the XML element to be updated
-     * @return {XMLElement} An updated XML element
-    */
-    updateCollectionDOM: function(objectDOM){
-
-      // Get or make objectDOM
-      if(!objectDOM){
-        if (this.get("objectDOM")) {
-          var objectDOM = this.get("objectDOM").cloneNode(true);
-          $(objectDOM).empty();
-        } else {
+          // Create the new isPartOf filter attributes object
+          // NOTE: All other attributes are set in Filter.initialize();
+          var isPartOfAttributes = {
+            fields: ["isPartOf"],
+            values: [seriesId],
+            matchSubstring: false,
+            operator: "OR",
+          };
+
+          // Remove any existing isPartOf filters, and add the new isPartOf filter
+
+          // NOTE:
+          // 1. Changes to the definition filters will automatically update the
+          // Search Model filters because of the listener set in initialize().
+          // 2. Add the new Filter model by passing a list of attributes to the
+          // Filters collection, instead of by passing a new Filter, in order
+          // to trigger 'update' and 'change' events for other models and views.
+
+          this.get("definitionFilters").removeFiltersByField("isPartOf");
+          var filterModel =
+            this.get("definitionFilters").add(isPartOfAttributes);
+
+          return filterModel;
+        } catch (e) {
+          console.log(
+            "Failed to create and add a new isPartOf Filter, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * Gets the text content of the XML node matching the given node name
+       *
+       * @param {Element} parentNode - The parent node to select from
+       * @param {string} nodeName - The name of the XML node to parse
+       * @param {boolean} isMultiple - If true, parses the nodes into an array
+       * @return {(string|Array)} - Returns a string or array of strings of the text content
+       */
+      parseTextNode: function (parentNode, nodeName, isMultiple) {
+        var node = $(parentNode).children(nodeName);
+
+        //If no matching nodes were found, return falsey values
+        if (!node || !node.length) {
+          //Return an empty array if the isMultiple flag is true
+          if (isMultiple) return [];
+          //Return null if the isMultiple flag is false
+          else return null;
+        }
+        //If exactly one node is found and we are only expecting one, return the text content
+        else if (node.length == 1 && !isMultiple) {
+          return node[0].textContent.trim();
+        }
+        //If more than one node is found, parse into an array
+        else {
+          return _.map(node, function (node) {
+            return node.textContent.trim() || null;
+          });
+        }
+      },
+
+      /**
+       * Updates collection XML with values in the collection model
+       *
+       * @param {XMLDocument} objectDOM the XML element to be updated
+       * @return {XMLElement} An updated XML element
+       */
+      updateCollectionDOM: function (objectDOM) {
+        // Get or make objectDOM
+        if (!objectDOM) {
+          if (this.get("objectDOM")) {
+            var objectDOM = this.get("objectDOM").cloneNode(true);
+            $(objectDOM).empty();
+          } else {
             // create an XML collection element from scratch
             var objectDOM = $(this.createXML()).children()[0];
+          }
         }
-      }
 
-      // Set schema version. May need to be updated from 1.0.0 to 1.1.0.
-      // The formatId is the same as the namespace URI.
-      var currentNamespace = this.defaults().formatId;
-    
-      // The NS attribute name could be xmlns:por or xmlns:col
-      objectDOM.attributes.forEach(function(attr){
-        if(attr.name.match(/^xmlns/)){
-          if(attr.value !== currentNamespace){
-            var newObjectDOM = this.createXML().documentElement;
-            while (objectDOM.firstChild) {
-              newObjectDOM.appendChild(objectDOM.firstChild);
+        // Set schema version. May need to be updated from 1.0.0 to 1.1.0.
+        // The formatId is the same as the namespace URI.
+        var currentNamespace = this.defaults().formatId;
+
+        // The NS attribute name could be xmlns:por or xmlns:col
+        objectDOM.attributes.forEach(function (attr) {
+          if (attr.name.match(/^xmlns/)) {
+            if (attr.value !== currentNamespace) {
+              var newObjectDOM = this.createXML().documentElement;
+              while (objectDOM.firstChild) {
+                newObjectDOM.appendChild(objectDOM.firstChild);
+              }
+              objectDOM = newObjectDOM;
             }
-            objectDOM = newObjectDOM
           }
-        }
-      }, this);
-
-      // Remove definition node if it exists in XML already
-      $(objectDOM).find("definition").remove();
-
-      // Get the filters that are currently applied to the search.
-      var definitionSerialized = this.get("definition").updateDOM();
-      objectDOM.ownerDocument.adoptNode(definitionSerialized);
-
-      //If at least one filter was serialized, add the <definition> node
-      if (definitionSerialized.childNodes.length) {
-        $(objectDOM).prepend(definitionSerialized);
-      }
-
-      // Get and update the simple text strings (everything but definition)
-      // in reverse order because we prepend them consecutively to objectDOM
-      var collectionTextData = {
-        description: this.get("description"),
-        name: this.get("name"),
-        label: this.get("label")
-      }
-
-      _.map(collectionTextData, function(value, nodeName){
-
-        // Remove the node if it exists
-        // Use children() and not find() as there are sub-children named label
-        $(objectDOM).children(nodeName).remove();
-
-        // Don't serialize falsey values
-        if(value && (typeof value == "string" && value.trim().length)){
-          // Make new sub-node
-          var collectionSubnodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
-          $(collectionSubnodeSerialized).text(value);
-          // Append new sub-node to the start of the objectDOM
-          $(objectDOM).prepend(collectionSubnodeSerialized);
-        }
-
-      });
-
-      return objectDOM;
-
-    },
-
-    /**
-     * Initialize the object XML for a brand spankin' new collection
-     * @return {Element}
-    */
-    createXML: function() {
-
-      var xmlString = "<col:collection xmlns:col=\"https://purl.dataone.org/collections-1.1.0\"></col:collection>",
-          xmlNew = $.parseXML(xmlString),
-          colNode = xmlNew.getElementsByTagName("col:collections")[0];
-
-      // set attributes
-      colNode.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
-      colNode.setAttribute("xsi:schemaLocation", "https://purl.dataone.org/collections-1.1.0");
-
-      this.set("ownerDocument", colNode.ownerDocument);
-
-      return(xmlNew);
-    },
+        }, this);
 
-    /**
-    * Creates a new instance of a Search model with a Filters collection.
-    * The Search model is created and returned and NOT set directly on the model in
-    * this function, because this function is called during parse(), when we're not ready
-    * to set attributes directly on the model yet.
-    * @return {Search}
-    */
-    createSearchModel: function(){
-      var search = new Search();
-      // Do not set "catalogSearch" to true, even though the search model is specifically
-      // created in order to do a catalog search. Instead, we set the definition
-      // filterGroup model catalogSearch = true. This allows us to append the query
-      // fragment with ID fields AFTER the catalog query fragment.
-      search.set("filters", new Filters());
-      return search;
-    },
+        // Remove definition node if it exists in XML already
+        $(objectDOM).find("definition").remove();
 
-    /**
-    * This is a shortcut function that returns the query for the datasets in this portal,
-    *  using the Search model for this portal. This is the full query that includes the filters not
-    *  serialized to the portal XML, such as the filters used for the DataCatalogView.
-    *
-    */
-    getQuery: function(){
+        // Get the filters that are currently applied to the search.
+        var definitionSerialized = this.get("definition").updateDOM();
+        objectDOM.ownerDocument.adoptNode(definitionSerialized);
 
-      return this.get("definition").getQuery();
+        //If at least one filter was serialized, add the <definition> node
+        if (definitionSerialized.childNodes.length) {
+          $(objectDOM).prepend(definitionSerialized);
+        }
 
-    },
+        // Get and update the simple text strings (everything but definition)
+        // in reverse order because we prepend them consecutively to objectDOM
+        var collectionTextData = {
+          description: this.get("description"),
+          name: this.get("name"),
+          label: this.get("label"),
+        };
 
-    /**
-     * Creates a copy of the SolrResults collection and saves it in this
-     * model so that there is always access to the unfiltered list of datasets
-     *
-     * @param {SolrResults} searchResults - The SolrResults collection to cache
-    */
-    cacheSearchResults: function(searchResults){
+        _.map(collectionTextData, function (value, nodeName) {
+          // Remove the node if it exists
+          // Use children() and not find() as there are sub-children named label
+          $(objectDOM).children(nodeName).remove();
+
+          // Don't serialize falsey values
+          if (value && typeof value == "string" && value.trim().length) {
+            // Make new sub-node
+            var collectionSubnodeSerialized =
+              objectDOM.ownerDocument.createElement(nodeName);
+            $(collectionSubnodeSerialized).text(value);
+            // Append new sub-node to the start of the objectDOM
+            $(objectDOM).prepend(collectionSubnodeSerialized);
+          }
+        });
 
-      //Save a copy of the SolrResults so that we always have a copy of
-      // the unfiltered list of datasets
-      this.set("allSearchResults", searchResults.clone());
+        return objectDOM;
+      },
 
-      //Make a copy of the facetCounts object
-      var allSearchResults = this.get("allSearchResults");
-      allSearchResults.facetCounts = Object.assign({}, searchResults.facetCounts);
+      /**
+       * Initialize the object XML for a brand spankin' new collection
+       * @return {Element}
+       */
+      createXML: function () {
+        var xmlString =
+            '<col:collection xmlns:col="https://purl.dataone.org/collections-1.1.0"></col:collection>',
+          xmlNew = $.parseXML(xmlString),
+          colNode = xmlNew.getElementsByTagName("col:collections")[0];
 
-    },
+        // set attributes
+        colNode.setAttribute(
+          "xmlns:xsi",
+          "http://www.w3.org/2001/XMLSchema-instance",
+        );
+        colNode.setAttribute(
+          "xsi:schemaLocation",
+          "https://purl.dataone.org/collections-1.1.0",
+        );
 
-    /**
-     * Overrides the default Backbone.Model.validate.function() to
-     * check if this portal model has all the required values necessary
-     * to save to the server.
-     *
-     * @param {Object} [attrs] - A literal object of model attributes to validate.
-     * @param {Object} [options] - A literal object of options for this validation process
-     * @return {Object} If there are errors, an object comprising error
-     *                   messages. If no errors, returns nothing.
-    */
-    validate: function(attrs, options) {
-
-      try{
-
-        var errors = {};
-
-        // Validate label
-        var labelError = this.validateLabel(this.get("label"));
-        if( labelError ){
-          errors.label = labelError;
-        }
+        this.set("ownerDocument", colNode.ownerDocument);
+
+        return xmlNew;
+      },
+
+      /**
+       * Creates a new instance of a Search model with a Filters collection.
+       * The Search model is created and returned and NOT set directly on the model in
+       * this function, because this function is called during parse(), when we're not ready
+       * to set attributes directly on the model yet.
+       * @return {Search}
+       */
+      createSearchModel: function () {
+        var search = new Search();
+        // Do not set "catalogSearch" to true, even though the search model is specifically
+        // created in order to do a catalog search. Instead, we set the definition
+        // filterGroup model catalogSearch = true. This allows us to append the query
+        // fragment with ID fields AFTER the catalog query fragment.
+        search.set("filters", new Filters());
+        return search;
+      },
+
+      /**
+       * This is a shortcut function that returns the query for the datasets in this portal,
+       *  using the Search model for this portal. This is the full query that includes the filters not
+       *  serialized to the portal XML, such as the filters used for the DataCatalogView.
+       *
+       */
+      getQuery: function () {
+        return this.get("definition").getQuery();
+      },
+
+      /**
+       * Creates a copy of the SolrResults collection and saves it in this
+       * model so that there is always access to the unfiltered list of datasets
+       *
+       * @param {SolrResults} searchResults - The SolrResults collection to cache
+       */
+      cacheSearchResults: function (searchResults) {
+        //Save a copy of the SolrResults so that we always have a copy of
+        // the unfiltered list of datasets
+        this.set("allSearchResults", searchResults.clone());
+
+        //Make a copy of the facetCounts object
+        var allSearchResults = this.get("allSearchResults");
+        allSearchResults.facetCounts = Object.assign(
+          {},
+          searchResults.facetCounts,
+        );
+      },
+
+      /**
+       * Overrides the default Backbone.Model.validate.function() to
+       * check if this portal model has all the required values necessary
+       * to save to the server.
+       *
+       * @param {Object} [attrs] - A literal object of model attributes to validate.
+       * @param {Object} [options] - A literal object of options for this validation process
+       * @return {Object} If there are errors, an object comprising error
+       *                   messages. If no errors, returns nothing.
+       */
+      validate: function (attrs, options) {
+        try {
+          var errors = {};
+
+          // Validate label
+          var labelError = this.validateLabel(this.get("label"));
+          if (labelError) {
+            errors.label = labelError;
+          }
 
-        // Validate the definition
-        var definitionError = this.get("definition").validate(attrs, options);
+          // Validate the definition
+          var definitionError = this.get("definition").validate(attrs, options);
+
+          if (definitionError) {
+            if (definitionError.noFilters) {
+              type = this.type.toLowerCase();
+              errors.definition =
+                "Your dataset collection hasn't been created. Add at " +
+                "least one query rule below to find datasets for this " +
+                type +
+                ". For example, to create a " +
+                type +
+                " for datasets from a specific " +
+                "research project, try using the project name field.";
+            } else {
+              // Just show the first error for now.
+              errors.definition = Object.values(definitionError)[0];
+            }
+          }
 
-        if(definitionError){
-          if(definitionError.noFilters){
-            type = this.type.toLowerCase();
-            errors.definition = "Your dataset collection hasn't been created. Add at " +
-              "least one query rule below to find datasets for this " + type +
-              ". For example, to create a " + type + " for datasets from a specific " +
-              "research project, try using the project name field.";
+          if (Object.keys(errors).length) {
+            console.log(errors);
+            return errors;
           } else {
-            // Just show the first error for now.
-            errors.definition = Object.values(definitionError)[0]
+            return;
           }
+        } catch (e) {
+          console.error(e);
         }
-
-        if( Object.keys(errors).length ) {
-          console.log(errors);
-          return errors;
-        } else {
-          return;
-        }
-
-      }
-      catch(e){
-        console.error(e);
-      }
-
-    },
-
-    /**
-     * Checks that a label does not equal a restricted value
-     * (e.g. new temporary name), and that it's encoded properly
-     * for use as part of a url
-     *
-     * @param {string} label - The label to be validated
-     * @return {string} - If the label is invalid, an error message string is returned
-    */
-    validateLabel: function(label){
-
-      try{
-
-        //Validate the label set on the model if one isn't given
-        if(typeof label != "string" ){
-          var label = this.get("label");
-        }
-
-        //If the label is not a string or is an empty string
-        if( typeof label != "string" || !label.trim().length ){
-          //Convert numbers to strings
-          if(typeof label == "number"){
-            label = label.toString();
+      },
+
+      /**
+       * Checks that a label does not equal a restricted value
+       * (e.g. new temporary name), and that it's encoded properly
+       * for use as part of a url
+       *
+       * @param {string} label - The label to be validated
+       * @return {string} - If the label is invalid, an error message string is returned
+       */
+      validateLabel: function (label) {
+        try {
+          //Validate the label set on the model if one isn't given
+          if (typeof label != "string") {
+            var label = this.get("label");
           }
-          else{
-            var type = this.type.toLowerCase();
-            return "Please choose a name for this " + type + " to use in the URL.";
+
+          //If the label is not a string or is an empty string
+          if (typeof label != "string" || !label.trim().length) {
+            //Convert numbers to strings
+            if (typeof label == "number") {
+              label = label.toString();
+            } else {
+              var type = this.type.toLowerCase();
+              return (
+                "Please choose a name for this " + type + " to use in the URL."
+              );
+            }
           }
-        }
 
-        // If the label is a restricted string
-        var blockList = this.get("labelBlockList");
-        if( blockList && Array.isArray(blockList) ){
-          if(blockList.includes(label)){
-            return "This URL is already taken, please try something else";
+          // If the label is a restricted string
+          var blockList = this.get("labelBlockList");
+          if (blockList && Array.isArray(blockList)) {
+            if (blockList.includes(label)) {
+              return "This URL is already taken, please try something else";
+            }
           }
-        }
 
-        // If the label includes illegal characters
-        // (Only allow letters, numbers, underscores and dashes)
-        if(label.match(/[^A-Za-z0-9_-]/g)){
-          return "URLs may only contain letters, numbers, underscores (_), and dashes (-).";
+          // If the label includes illegal characters
+          // (Only allow letters, numbers, underscores and dashes)
+          if (label.match(/[^A-Za-z0-9_-]/g)) {
+            return "URLs may only contain letters, numbers, underscores (_), and dashes (-).";
+          }
+        } catch (e) {
+          //Trigger an error event
+          this.trigger("errorValidatingLabel");
+          console.error(e);
         }
+      },
+    },
+  );
 
-      }
-      catch(e){
-        //Trigger an error event
-        this.trigger("errorValidatingLabel");
-        console.error(e);
-      }
-
-    }
-
-	});
-
-	return CollectionModel;
+  return CollectionModel;
 });
 
diff --git a/docs/docs/src_js_models_DataONEObject.js.html b/docs/docs/src_js_models_DataONEObject.js.html index 5f788df03..777b6600a 100644 --- a/docs/docs/src_js_models_DataONEObject.js.html +++ b/docs/docs/src_js_models_DataONEObject.js.html @@ -44,11 +44,17 @@

Source: src/js/models/DataONEObject.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'uuid', 'he', 'collections/AccessPolicy', 'collections/ObjectFormats', 'md5'],
-    function($, _, Backbone, uuid, he, AccessPolicy, ObjectFormats, md5){
-
-        /**
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "uuid",
+  "he",
+  "collections/AccessPolicy",
+  "collections/ObjectFormats",
+  "md5",
+], function ($, _, Backbone, uuid, he, AccessPolicy, ObjectFormats, md5) {
+  /**
         * @class DataONEObject
         * @classdesc A DataONEObject represents a DataONE object, such as a data file,
         a science metadata object, or a resource map. It stores the system
@@ -59,2418 +65,2625 @@ 

Source: src/js/models/DataONEObject.js

* @classcategory Models * @augments Backbone.Model */ - var DataONEObject = Backbone.Model.extend( - /** @lends DataONEObject.prototype */{ - - type: "DataONEObject", - selectedInEditor: false, // Has this package member been selected and displayed in the provenance editor? - PROV: "http://www.w3.org/ns/prov#", - PROVONE: "http://purl.dataone.org/provone/2015/01/15/ontology#", - - defaults: function(){ - return{ - // System Metadata attributes - serialVersion: null, - identifier: null, - formatId: null, - size: null, - checksum: null, - originalChecksum: null, - checksumAlgorithm: "MD5", - submitter: null, - rightsHolder : null, - accessPolicy: [], //An array of accessPolicy literal JS objects - replicationAllowed: null, - replicationPolicy: [], - obsoletes: null, - obsoletedBy: null, - archived: null, - dateUploaded: null, - dateSysMetadataModified: null, - originMemberNode: null, - authoritativeMemberNode: null, - replica: [], - seriesId: null, // uuid.v4(), (decide if we want to auto-set this) - mediaType: null, - fileName: null, - // Non-system metadata attributes: - isNew: null, - datasource: null, - insert_count_i: null, - read_count_i: null, - changePermission: null, - writePermission: null, - readPermission: null, - isPublic: null, - dateModified: null, - id: "urn:uuid:" + uuid.v4(), - sizeStr: null, - type: "", // Data, Metadata, or DataPackage - formatType: "", - metadataEntity: null, // A model that represents the metadata for this file, e.g. an EMLEntity model - latestVersion: null, - isDocumentedBy: null, - documents: [], - members: [], - resourceMap: [], - nodeLevel: 0, // Indicates hierarchy level in the view for indentation - sortOrder: 2, // Metadata: 1, Data: 2, DataPackage: 3 - synced: false, // True if the full model has been synced - uploadStatus: null, //c=complete, p=in progress, q=queued, e=error, w=warning, no upload status=not in queue - uploadProgress: null, - sysMetaUploadStatus: null, //c=complete, p=in progress, q=queued, e=error, l=loading, no upload status=not in queue - percentLoaded: 0, // Percent the file is read before caclculating the md5 sum - uploadFile: null, // The file reference to be uploaded (JS object: File) - errorMessage: null, - sysMetaErrorCode: null, // The status code given when there is an error updating the system metadata - numSaveAttempts: 0, - notFound: false, //Whether or not this object was found in the system - originalAttrs: [], // An array of original attributes in a DataONEObject - changed: false, // If any attributes have been changed, including attrs in nested objects - hasContentChanges: false, // If attributes outside of originalAttrs have been changed - sysMetaXML: null, // A cached original version of the fetched system metadata document - objectXML: null, // A cached version of the object fetched from the server - isAuthorized: null, // If the stated permission is authorized by the user - isAuthorized_read: null, //If the user has permission to read - isAuthorized_write: null, //If the user has permission to write - isAuthorized_changePermission: null, //If the user has permission to changePermission - createSeriesId: false, //If true, a seriesId will be created when this object is saved. - collections: [], //References to collections that this model is in - possibleAuthMNs: [], //A list of possible authoritative MNs of this object - useAltRepo: false, - isLoadingFiles: false, //Only relevant to Resource Map objects. Is true if there is at least one file still loading into the package. - numLoadingFiles: 0, //Only relevant to Resource Map objects. The number of files still loading into the package. - provSources: [], - provDerivations: [], - prov_generated: [], - prov_generatedByExecution: [], - prov_generatedByProgram: [], - prov_generatedByUser: [], - prov_hasDerivations: [], - prov_hasSources: [], - prov_instanceOfClass: [], - prov_used: [], - prov_usedByExecution: [], - prov_usedByProgram: [], - prov_usedByUser: [], - prov_wasDerivedFrom: [], - prov_wasExecutedByExecution: [], - prov_wasExecutedByUser: [], - prov_wasInformedBy: [] - } - }, - - initialize: function(attrs, options) { - if(typeof attrs == "undefined") var attrs = {}; + var DataONEObject = Backbone.Model.extend( + /** @lends DataONEObject.prototype */ { + type: "DataONEObject", + selectedInEditor: false, // Has this package member been selected and displayed in the provenance editor? + PROV: "http://www.w3.org/ns/prov#", + PROVONE: "http://purl.dataone.org/provone/2015/01/15/ontology#", + + defaults: function () { + return { + // System Metadata attributes + serialVersion: null, + identifier: null, + formatId: null, + size: null, + checksum: null, + originalChecksum: null, + checksumAlgorithm: "MD5", + submitter: null, + rightsHolder: null, + accessPolicy: [], //An array of accessPolicy literal JS objects + replicationAllowed: null, + replicationPolicy: [], + obsoletes: null, + obsoletedBy: null, + archived: null, + dateUploaded: null, + dateSysMetadataModified: null, + originMemberNode: null, + authoritativeMemberNode: null, + replica: [], + seriesId: null, // uuid.v4(), (decide if we want to auto-set this) + mediaType: null, + fileName: null, + // Non-system metadata attributes: + isNew: null, + datasource: null, + insert_count_i: null, + read_count_i: null, + changePermission: null, + writePermission: null, + readPermission: null, + isPublic: null, + dateModified: null, + id: "urn:uuid:" + uuid.v4(), + sizeStr: null, + type: "", // Data, Metadata, or DataPackage + formatType: "", + metadataEntity: null, // A model that represents the metadata for this file, e.g. an EMLEntity model + latestVersion: null, + isDocumentedBy: null, + documents: [], + members: [], + resourceMap: [], + nodeLevel: 0, // Indicates hierarchy level in the view for indentation + sortOrder: 2, // Metadata: 1, Data: 2, DataPackage: 3 + synced: false, // True if the full model has been synced + uploadStatus: null, //c=complete, p=in progress, q=queued, e=error, w=warning, no upload status=not in queue + uploadProgress: null, + sysMetaUploadStatus: null, //c=complete, p=in progress, q=queued, e=error, l=loading, no upload status=not in queue + percentLoaded: 0, // Percent the file is read before caclculating the md5 sum + uploadFile: null, // The file reference to be uploaded (JS object: File) + errorMessage: null, + sysMetaErrorCode: null, // The status code given when there is an error updating the system metadata + numSaveAttempts: 0, + notFound: false, //Whether or not this object was found in the system + originalAttrs: [], // An array of original attributes in a DataONEObject + changed: false, // If any attributes have been changed, including attrs in nested objects + hasContentChanges: false, // If attributes outside of originalAttrs have been changed + sysMetaXML: null, // A cached original version of the fetched system metadata document + objectXML: null, // A cached version of the object fetched from the server + isAuthorized: null, // If the stated permission is authorized by the user + isAuthorized_read: null, //If the user has permission to read + isAuthorized_write: null, //If the user has permission to write + isAuthorized_changePermission: null, //If the user has permission to changePermission + createSeriesId: false, //If true, a seriesId will be created when this object is saved. + collections: [], //References to collections that this model is in + possibleAuthMNs: [], //A list of possible authoritative MNs of this object + useAltRepo: false, + isLoadingFiles: false, //Only relevant to Resource Map objects. Is true if there is at least one file still loading into the package. + numLoadingFiles: 0, //Only relevant to Resource Map objects. The number of files still loading into the package. + provSources: [], + provDerivations: [], + prov_generated: [], + prov_generatedByExecution: [], + prov_generatedByProgram: [], + prov_generatedByUser: [], + prov_hasDerivations: [], + prov_hasSources: [], + prov_instanceOfClass: [], + prov_used: [], + prov_usedByExecution: [], + prov_usedByProgram: [], + prov_usedByUser: [], + prov_wasDerivedFrom: [], + prov_wasExecutedByExecution: [], + prov_wasExecutedByUser: [], + prov_wasInformedBy: [], + }; + }, + + initialize: function (attrs, options) { + if (typeof attrs == "undefined") var attrs = {}; + + this.set("accessPolicy", this.createAccessPolicy()); + + this.on("change:size", this.bytesToSize); + if (attrs.size) this.bytesToSize(); + + // Cache an array of original attribute names to help in handleChange() + if (this.type == "DataONEObject") + this.set("originalAttrs", Object.keys(this.attributes)); + else + this.set( + "originalAttrs", + Object.keys(DataONEObject.prototype.defaults()), + ); + + this.on("successSaving", this.updateRelationships); + + //Save a reference to this DataONEObject model in the metadataEntity model + //whenever the metadataEntity is set + this.on("change:metadataEntity", function () { + var entityMetadataModel = this.get("metadataEntity"); + + if (entityMetadataModel) + entityMetadataModel.set("dataONEObject", this); + }); + + this.on("sync", function () { + this.set("synced", true); + }); + + //Find Member Node object that might be the authoritative MN + //This is helpful when MetacatUI may be displaying content from multiple MNs + this.setPossibleAuthMNs(); + }, - this.set("accessPolicy", this.createAccessPolicy()); - - this.on("change:size", this.bytesToSize); - if(attrs.size) - this.bytesToSize(); + /** + * Maps the lower-case sys meta node names (valid in HTML DOM) to the + * camel-cased sys meta node names (valid in DataONE). + * Used during parse() and serialize() + */ + nodeNameMap: function () { + return { + accesspolicy: "accessPolicy", + accessrule: "accessRule", + authoritativemembernode: "authoritativeMemberNode", + checksumalgorithm: "checksumAlgorithm", + dateuploaded: "dateUploaded", + datesysmetadatamodified: "dateSysMetadataModified", + formatid: "formatId", + filename: "fileName", + nodereference: "nodeReference", + numberreplicas: "numberReplicas", + obsoletedby: "obsoletedBy", + originmembernode: "originMemberNode", + replicamembernode: "replicaMemberNode", + replicationallowed: "replicationAllowed", + replicationpolicy: "replicationPolicy", + replicationstatus: "replicationStatus", + replicaverified: "replicaVerified", + rightsholder: "rightsHolder", + serialversion: "serialVersion", + seriesid: "seriesId", + }; + }, - // Cache an array of original attribute names to help in handleChange() - if(this.type == "DataONEObject") - this.set("originalAttrs", Object.keys(this.attributes)); - else - this.set("originalAttrs", Object.keys(DataONEObject.prototype.defaults())); + /** + * Returns the URL string where this DataONEObject can be fetched from or saved to + * @returns {string} + */ + url: function () { + // With no id, we can't do anything + if (!this.get("id") && !this.get("seriesid")) return ""; + + //Get the active alternative repository, if one is configured + var activeAltRepo = MetacatUI.appModel.getActiveAltRepo(); + + //Start the base URL string + var baseUrl = ""; + + // Determine if we're updating a new/existing object, + // or just its system metadata + // New uploads use the object service URL + if (this.isNew()) { + //Use the object service URL from the alt repo + if (this.get("useAltRepo") && activeAltRepo) { + baseUrl = activeAltRepo.objectServiceUrl; + } + //If this MetacatUI deployment is pointing to a MN, use the object service URL from the AppModel + else { + baseUrl = MetacatUI.appModel.get("objectServiceUrl"); + } - this.on("successSaving", this.updateRelationships); + //Return the full URL + return baseUrl; + } else { + if (this.hasUpdates()) { + if (this.get("hasContentChanges")) { + //Use the object service URL from the alt repo + if (this.get("useAltRepo") && activeAltRepo) { + baseUrl = activeAltRepo.objectServiceUrl; + } else { + baseUrl = MetacatUI.appModel.get("objectServiceUrl"); + } - //Save a reference to this DataONEObject model in the metadataEntity model - //whenever the metadataEntity is set - this.on("change:metadataEntity", function(){ - var entityMetadataModel = this.get("metadataEntity"); + // Exists on the server, use MN.update() + return baseUrl + encodeURIComponent(this.get("oldPid")); + } else { + //Use the meta service URL from the alt repo + if (this.get("useAltRepo") && activeAltRepo) { + baseUrl = activeAltRepo.metaServiceUrl; + } else { + baseUrl = MetacatUI.appModel.get("metaServiceUrl"); + } - if( entityMetadataModel ) - entityMetadataModel.set("dataONEObject", this); + // Exists on the server, use MN.updateSystemMetadata() + return baseUrl + encodeURIComponent(this.get("id")); + } + } else { + //Use the meta service URL from the alt repo + if (this.get("useAltRepo") && activeAltRepo) { + baseUrl = activeAltRepo.metaServiceUrl; + } else { + baseUrl = MetacatUI.appModel.get("metaServiceUrl"); + } - }); + // Use MN.getSystemMetadata() + return ( + baseUrl + + (encodeURIComponent(this.get("id")) || + encodeURIComponent(this.get("seriesid"))) + ); + } + } + }, - this.on("sync", function(){ - this.set("synced", true); + /** + * Create the URL string that is used to download this package + * @returns PackageURL string for this DataONE Object + * @since 2.28.0 + */ + getPackageURL: function () { + var url = null; + + // With no id, we can't do anything + if (!this.get("id") && !this.get("seriesid")) return url; + + //If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from + if ( + MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") > + -1 && + MetacatUI.nodeModel.get("members").length + ) { + var source = this.get("datasource"), + node = _.find(MetacatUI.nodeModel.get("members"), { + identifier: source, }); - //Find Member Node object that might be the authoritative MN - //This is helpful when MetacatUI may be displaying content from multiple MNs - this.setPossibleAuthMNs(); - - }, - - /** - * Maps the lower-case sys meta node names (valid in HTML DOM) to the - * camel-cased sys meta node names (valid in DataONE). - * Used during parse() and serialize() - */ - nodeNameMap: function(){ - return{ - accesspolicy: "accessPolicy", - accessrule: "accessRule", - authoritativemembernode: "authoritativeMemberNode", - checksumalgorithm: "checksumAlgorithm", - dateuploaded: "dateUploaded", - datesysmetadatamodified: "dateSysMetadataModified", - formatid: "formatId", - filename: "fileName", - nodereference: "nodeReference", - numberreplicas: "numberReplicas", - obsoletedby: "obsoletedBy", - originmembernode: "originMemberNode", - replicamembernode: "replicaMemberNode", - replicationallowed: "replicationAllowed", - replicationpolicy: "replicationPolicy", - replicationstatus: "replicationStatus", - replicaverified: "replicaVerified", - rightsholder: "rightsHolder", - serialversion: "serialVersion", - seriesid: "seriesId" - }; - }, - - /** - * Returns the URL string where this DataONEObject can be fetched from or saved to - * @returns {string} - */ - url: function(){ - - // With no id, we can't do anything - if( !this.get("id") && !this.get("seriesid") ) - return ""; + //If this node has MNRead v2 services... + if (node && node.readv2) + url = + node.baseURL + + "/v2/packages/application%2Fbagit-097/" + + encodeURIComponent(this.get("id")); + } else if (MetacatUI.appModel.get("packageServiceUrl")) + url = + MetacatUI.appModel.get("packageServiceUrl") + + encodeURIComponent(this.get("id")); - //Get the active alternative repository, if one is configured - var activeAltRepo = MetacatUI.appModel.getActiveAltRepo(); + return url; + }, - //Start the base URL string - var baseUrl = ""; - - // Determine if we're updating a new/existing object, - // or just its system metadata - // New uploads use the object service URL - if ( this.isNew() ) { + /** + * Overload Backbone.Model.fetch, so that we can set custom options for each fetch() request + */ + fetch: function (options) { + if (!options) var options = {}; + else var options = _.clone(options); + + options.url = this.url(); + + //If we are using the Solr service to retrieve info about this object, then construct a query + if (typeof options != "undefined" && options.solrService) { + //Get basic information + var query = ""; + + //Do not search for seriesId when it is not configured in this model/app + if (typeof this.get("seriesid") === "undefined") + query += 'id:"' + encodeURIComponent(this.get("id")) + '"'; + //If there is no seriesid set, then search for pid or sid + else if (!this.get("seriesid")) + query += + '(id:"' + + encodeURIComponent(this.get("id")) + + '" OR seriesId:"' + + encodeURIComponent(this.get("id")) + + '")'; + //If a seriesId is specified, then search for that + else if (this.get("seriesid") && this.get("id").length > 0) + query += + '(seriesId:"' + + encodeURIComponent(this.get("seriesid")) + + '" AND id:"' + + encodeURIComponent(this.get("id")) + + '")'; + //If only a seriesId is specified, then just search for the most recent version + else if (this.get("seriesid") && !this.get("id")) + query += + 'seriesId:"' + + encodeURIComponent(this.get("id")) + + '" -obsoletedBy:*'; + + //The fields to return + var fl = "formatId,formatType,documents,isDocumentedBy,id,seriesId"; + + //Use the Solr query URL + var solrOptions = { + url: + MetacatUI.appModel.get("queryServiceUrl") + + "q=" + + query + + "&fl=" + + fl + + "&wt=json", + }; + + //Merge with the options passed to this function + var fetchOptions = _.extend(options, solrOptions); + } else if (typeof options != "undefined") { + //Use custom options for retreiving XML + //Merge with the options passed to this function + var fetchOptions = _.extend( + { + dataType: "text", + }, + options, + ); + } else { + //Use custom options for retreiving XML + var fetchOptions = _.extend({ + dataType: "text", + }); + } - //Use the object service URL from the alt repo - if( this.get("useAltRepo") && activeAltRepo ){ - baseUrl = activeAltRepo.objectServiceUrl; - } - //If this MetacatUI deployment is pointing to a MN, use the object service URL from the AppModel - else{ - baseUrl = MetacatUI.appModel.get("objectServiceUrl"); - } + //Add the authorization options + fetchOptions = _.extend( + fetchOptions, + MetacatUI.appUserModel.createAjaxSettings(), + ); - //Return the full URL - return baseUrl; + //Call Backbone.Model.fetch to retrieve the info + return Backbone.Model.prototype.fetch.call(this, fetchOptions); + }, + /** + * This function is called by Backbone.Model.fetch. + * It deserializes the incoming XML from the /meta REST endpoint and converts it into JSON. + */ + parse: function (response) { + // If the response is XML + if (typeof response == "string" && response.indexOf("<") == 0) { + var responseDoc = $.parseHTML(response), + systemMetadata; + + //Save the raw XML in case it needs to be used later + this.set("sysMetaXML", response); + + //Find the XML node for the system metadata + for (var i = 0; i < responseDoc.length; i++) { + if ( + responseDoc[i].nodeType == 1 && + responseDoc[i].localName.indexOf("systemmetadata") > -1 + ) { + systemMetadata = responseDoc[i]; + break; } - else { - if ( this.hasUpdates() ) { - if ( this.get("hasContentChanges") ) { + } - //Use the object service URL from the alt repo - if( this.get("useAltRepo") && activeAltRepo ){ - baseUrl = activeAltRepo.objectServiceUrl; - } - else{ - baseUrl = MetacatUI.appModel.get("objectServiceUrl"); - } + //Parse the XML to JSON + var sysMetaValues = this.toJson(systemMetadata); + + //Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code + _.each( + Object.keys(sysMetaValues), + function (key) { + var camelCasedKey = this.nodeNameMap()[key]; + if (camelCasedKey) { + sysMetaValues[camelCasedKey] = sysMetaValues[key]; + delete sysMetaValues[key]; + } + }, + this, + ); + + //Save the checksum from the system metadata in a separate attribute on the model + sysMetaValues.originalChecksum = sysMetaValues.checksum; + sysMetaValues.checksum = this.defaults().checksum; + + //Save the identifier as the id attribute + sysMetaValues.id = sysMetaValues.identifier; + + //Parse the Access Policy + if ( + this.get("accessPolicy") && + AccessPolicy.prototype.isPrototypeOf(this.get("accessPolicy")) + ) { + this.get("accessPolicy").parse( + $(systemMetadata).find("accesspolicy"), + ); + sysMetaValues.accessPolicy = this.get("accessPolicy"); + } else { + //Create a new AccessPolicy collection, if there isn't one already. + sysMetaValues.accessPolicy = this.createAccessPolicy( + $(systemMetadata).find("accesspolicy"), + ); + } - // Exists on the server, use MN.update() - return baseUrl + (encodeURIComponent(this.get("oldPid"))); + return sysMetaValues; + + // If the response is a list of Solr docs + } else if ( + typeof response === "object" && + response.response && + response.response.docs + ) { + //If no objects were found in the index, mark as notFound and exit + if (!response.response.docs.length) { + this.set("notFound", true); + this.trigger("notFound"); + return; + } - } else { + //Get the Solr document (there should be only one) + var doc = response.response.docs[0]; - //Use the meta service URL from the alt repo - if( this.get("useAltRepo") && activeAltRepo ){ - baseUrl = activeAltRepo.metaServiceUrl; - } - else{ - baseUrl = MetacatUI.appModel.get("metaServiceUrl"); - } + //Take out any empty values + _.each(Object.keys(doc), function (field) { + if (!doc[field] && doc[field] !== 0) delete doc[field]; + }); - // Exists on the server, use MN.updateSystemMetadata() - return baseUrl + (encodeURIComponent(this.get("id"))); + //Remove any erroneous white space from fields + this.removeWhiteSpaceFromSolrFields(doc); - } - } else { - //Use the meta service URL from the alt repo - if( this.get("useAltRepo") && activeAltRepo ){ - baseUrl = activeAltRepo.metaServiceUrl; - } - else{ - baseUrl = MetacatUI.appModel.get("metaServiceUrl"); - } + return doc; + } + // Default to returning the raw response + else return response; + }, - // Use MN.getSystemMetadata() - return baseUrl + - (encodeURIComponent(this.get("id")) || - encodeURIComponent(this.get("seriesid"))); - } - } - }, + /** A utility function for converting XML to JSON */ + toJson: function (xml) { + // Create the return object + var obj = {}; - /** - * Create the URL string that is used to download this package - * @returns PackageURL string for this DataONE Object - * @since 2.28.0 - */ - getPackageURL: function(){ - var url = null; + // do children + if (xml.hasChildNodes()) { + for (var i = 0; i < xml.childNodes.length; i++) { + var item = xml.childNodes.item(i); - // With no id, we can't do anything - if( !this.get("id") && !this.get("seriesid") ) - return url; + //If it's an empty text node, skip it + if (item.nodeType == 3 && !item.nodeValue.trim()) continue; - //If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from - if((MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") > -1) && MetacatUI.nodeModel.get("members").length){ - var source = this.get("datasource"), - node = _.find(MetacatUI.nodeModel.get("members"), {identifier: source}); + //Get the node name + var nodeName = item.localName; - //If this node has MNRead v2 services... - if(node && node.readv2) - url = node.baseURL + "/v2/packages/application%2Fbagit-097/" + encodeURIComponent(this.get("id")); + //If it's a new container node, convert it to JSON and add as a new object attribute + if (typeof obj[nodeName] == "undefined" && item.nodeType == 1) { + obj[nodeName] = this.toJson(item); } - else if(MetacatUI.appModel.get("packageServiceUrl")) - url = MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(this.get("id")); - - return url; - }, - - /** - * Overload Backbone.Model.fetch, so that we can set custom options for each fetch() request - */ - fetch: function(options){ - - if ( ! options ) var options = {}; - else var options = _.clone(options); - - options.url = this.url(); - - //If we are using the Solr service to retrieve info about this object, then construct a query - if((typeof options != "undefined") && options.solrService){ - - //Get basic information - var query = ""; - - //Do not search for seriesId when it is not configured in this model/app - if(typeof this.get("seriesid") === "undefined") - query += 'id:"' + encodeURIComponent(this.get("id")) + '"'; - //If there is no seriesid set, then search for pid or sid - else if(!this.get("seriesid")) - query += '(id:"' + encodeURIComponent(this.get("id")) + '" OR seriesId:"' + encodeURIComponent(this.get("id")) + '")'; - //If a seriesId is specified, then search for that - else if(this.get("seriesid") && (this.get("id").length > 0)) - query += '(seriesId:"' + encodeURIComponent(this.get("seriesid")) + '" AND id:"' + encodeURIComponent(this.get("id")) + '")'; - //If only a seriesId is specified, then just search for the most recent version - else if(this.get("seriesid") && !this.get("id")) - query += 'seriesId:"' + encodeURIComponent(this.get("id")) + '" -obsoletedBy:*'; - - //The fields to return - var fl = "formatId,formatType,documents,isDocumentedBy,id,seriesId"; - - //Use the Solr query URL - var solrOptions = { - url: MetacatUI.appModel.get("queryServiceUrl") + 'q=' + query + "&fl=" + fl + "&wt=json" + //If it's a new text node, just store the text value and add as a new object attribute + else if ( + typeof obj[nodeName] == "undefined" && + item.nodeType == 3 + ) { + obj = + item.nodeValue == "false" + ? false + : item.nodeValue == "true" + ? true + : item.nodeValue; + } + //If this node name is already stored as an object attribute... + else if (typeof obj[nodeName] != "undefined") { + //Cache what we have now + var old = obj[nodeName]; + if (!Array.isArray(old)) old = [old]; + + //Create a new object to store this node info + var newNode = {}; + + //Add the new node info to the existing array we have now + if (item.nodeType == 1) { + newNode = this.toJson(item); + var newArray = old.concat(newNode); + } else if (item.nodeType == 3) { + newNode = item.nodeValue; + var newArray = old.concat(newNode); } - //Merge with the options passed to this function - var fetchOptions = _.extend(options, solrOptions); - } - else if(typeof options != "undefined"){ - //Use custom options for retreiving XML - //Merge with the options passed to this function - var fetchOptions = _.extend({ - dataType: "text" - }, options); - } - else{ - //Use custom options for retreiving XML - var fetchOptions = _.extend({ - dataType: "text" + //Store the attributes for this node + _.each(item.attributes, function (attr) { + newNode[attr.localName] = attr.nodeValue; }); + + //Replace the old array with the updated one + obj[nodeName] = newArray; + + //Exit + continue; } - //Add the authorization options - fetchOptions = _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); + //Store the attributes for this node + /*_.each(item.attributes, function(attr){ + obj[nodeName][attr.localName] = attr.nodeValue; + });*/ + } + } + return obj; + }, - //Call Backbone.Model.fetch to retrieve the info - return Backbone.Model.prototype.fetch.call(this, fetchOptions); + /** + Serialize the DataONE object JSON to XML + @param {object} json - the JSON object to convert to XML + @param {Element} containerNode - an HTML element to insertt the resulting XML into + @returns {Element} The updated HTML Element + */ + toXML: function (json, containerNode) { + if (typeof json == "string") { + containerNode.textContent = json; + return containerNode; + } - }, + for (var i = 0; i < Object.keys(json).length; i++) { + var key = Object.keys(json)[i], + contents = json[key] || json[key]; - /** - * This function is called by Backbone.Model.fetch. - * It deserializes the incoming XML from the /meta REST endpoint and converts it into JSON. - */ - parse: function(response){ + var node = document.createElement(key); - // If the response is XML - if( (typeof response == "string") && response.indexOf("<") == 0 ) { + //Skip this attribute if it is not populated + if (!contents || (Array.isArray(contents) && !contents.length)) + continue; - var responseDoc = $.parseHTML(response), - systemMetadata; + //If it's a simple text node + if (typeof contents == "string") { + containerNode.textContent = contents; + return containerNode; + } else if (Array.isArray(contents)) { + var allNewNodes = []; - //Save the raw XML in case it needs to be used later - this.set("sysMetaXML", response); + for (var ii = 0; ii < contents.length; ii++) { + allNewNodes.push(this.toXML(contents[ii], $(node).clone()[0])); + } - //Find the XML node for the system metadata - for(var i=0; i<responseDoc.length; i++){ - if((responseDoc[i].nodeType == 1) && (responseDoc[i].localName.indexOf("systemmetadata") > -1)){ - systemMetadata = responseDoc[i]; - break; - } - } + if (allNewNodes.length) node = allNewNodes; + } else if (typeof contents == "object") { + $(node).append(this.toXML(contents, node)); + var attributeNames = _.without(Object.keys(json[key]), "content"); + } - //Parse the XML to JSON - var sysMetaValues = this.toJson(systemMetadata); + $(containerNode).append(node); + } - //Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code - _.each(Object.keys(sysMetaValues), function(key){ - var camelCasedKey = this.nodeNameMap()[key]; - if(camelCasedKey){ - sysMetaValues[camelCasedKey] = sysMetaValues[key]; - delete sysMetaValues[key]; - } - }, this); + return containerNode; + }, - //Save the checksum from the system metadata in a separate attribute on the model - sysMetaValues.originalChecksum = sysMetaValues.checksum; - sysMetaValues.checksum = this.defaults().checksum; + /** + * Saves the DataONEObject System Metadata to the server + */ + save: function (attributes, options) { + // Set missing file names before saving + if (!this.get("fileName")) { + this.setMissingFileName(); + } else { + //Replace all non-alphanumeric characters with underscores + var fileNameWithoutExt = this.get("fileName").substring( + 0, + this.get("fileName").lastIndexOf("."), + ), + extension = this.get("fileName").substring( + this.get("fileName").lastIndexOf("."), + this.get("fileName").length, + ); + this.set( + "fileName", + fileNameWithoutExt.replace(/[^a-zA-Z0-9]/g, "_") + extension, + ); + } - //Save the identifier as the id attribute - sysMetaValues.id = sysMetaValues.identifier; + if (!this.hasUpdates()) { + this.set("uploadStatus", null); + return; + } - //Parse the Access Policy - if( this.get("accessPolicy") && AccessPolicy.prototype.isPrototypeOf(this.get("accessPolicy")) ){ - this.get("accessPolicy").parse($(systemMetadata).find("accesspolicy")); - sysMetaValues.accessPolicy = this.get("accessPolicy"); - } - else{ - //Create a new AccessPolicy collection, if there isn't one already. - sysMetaValues.accessPolicy = this.createAccessPolicy($(systemMetadata).find("accesspolicy")); - } + //Set the upload transfer as in progress + this.set("uploadProgress", 2); + this.set("uploadStatus", "p"); + + //Check if the checksum has been calculated yet. + if (!this.get("checksum")) { + //When it is calculated, restart this function + this.on("checksumCalculated", this.save); + //Calculate the checksum for this file + this.calculateChecksum(); + //Exit this function until the checksum is done + return; + } - return sysMetaValues; + //Create a FormData object to send data with our XHR + var formData = new FormData(); + + //If this is not a new object, update the id. New DataONEObjects will have an id + // created during initialize. + if (!this.isNew()) { + this.updateID(); + formData.append("pid", this.get("oldPid")); + formData.append("newPid", this.get("id")); + } else { + //Create an ID if there isn't one + if (!this.get("id")) { + this.set("id", "urn:uuid:" + uuid.v4()); + } - // If the response is a list of Solr docs - } else if (( typeof response === "object") && (response.response && response.response.docs)){ + //Add the identifier to the XHR data + formData.append("pid", this.get("id")); + } - //If no objects were found in the index, mark as notFound and exit - if(!response.response.docs.length){ - this.set("notFound", true); - this.trigger("notFound"); - return; - } + //Create the system metadata XML + var sysMetaXML = this.serializeSysMeta(); - //Get the Solr document (there should be only one) - var doc = response.response.docs[0]; + //Send the system metadata as a Blob + var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" }); + //Add the system metadata XML to the XHR data + formData.append("sysmeta", xmlBlob, "sysmeta.xml"); - //Take out any empty values - _.each(Object.keys(doc), function(field){ - if( !doc[field] && doc[field] !== 0 ) - delete doc[field]; - }); + // Create the new object (MN.create()) + formData.append("object", this.get("uploadFile"), this.get("fileName")); - //Remove any erroneous white space from fields - this.removeWhiteSpaceFromSolrFields(doc); + var model = this; - return doc; + // On create(), add to the package and the metadata + // Note: This should be added to the parent collection + // but for now we are using the root collection + _.each( + this.get("collections"), + function (collection) { + if (collection.type == "DataPackage") { + this.off("successSaving", collection.addNewModel); + this.once("successSaving", collection.addNewModel, collection); } - else - // Default to returning the raw response - return response; }, + this, + ); + + //Put together the AJAX and Backbone.save() options + var requestSettings = { + url: this.url(), + cache: false, + contentType: false, + dataType: "text", + processData: false, + data: formData, + parse: false, + xhr: function () { + var xhr = new window.XMLHttpRequest(); + + //Upload progress + xhr.upload.addEventListener( + "progress", + function (evt) { + if (evt.lengthComputable) { + var percentComplete = (evt.loaded / evt.total) * 100; + + model.set("uploadProgress", percentComplete); + } + }, + false, + ); - /** A utility function for converting XML to JSON */ - toJson: function(xml) { - - // Create the return object - var obj = {}; - - // do children - if (xml.hasChildNodes()) { + return xhr; + }, + success: this.onSuccessfulSave, + error: function (model, response, xhr) { + //Reset the identifier changes + model.resetID(); + //Reset the checksum, if this is a model that needs to be serialized with each save. + if (model.serialize) { + model.set("checksum", model.defaults().checksum); + } - for(var i = 0; i < xml.childNodes.length; i++) { - var item = xml.childNodes.item(i); + model.set("numSaveAttempts", model.get("numSaveAttempts") + 1); + var numSaveAttempts = model.get("numSaveAttempts"); + + if ( + numSaveAttempts < 3 && + (response.status == 408 || response.status == 0) + ) { + //Try saving again in 10, 40, and 90 seconds + setTimeout( + function () { + model.save.call(model); + }, + numSaveAttempts * numSaveAttempts * 10000, + ); + } else { + model.set("numSaveAttempts", 0); - //If it's an empty text node, skip it - if((item.nodeType == 3) && (!item.nodeValue.trim())) - continue; + var parsedResponse = $(response.responseText) + .not("style, title") + .text(); - //Get the node name - var nodeName = item.localName; + //When there is no network connection (status == 0), there will be no response text + if (!parsedResponse) + parsedResponse = + "There was a network issue that prevented this file from uploading. " + + "Make sure you are connected to a reliable internet connection."; - //If it's a new container node, convert it to JSON and add as a new object attribute - if((typeof(obj[nodeName]) == "undefined") && (item.nodeType == 1)) { - obj[nodeName] = this.toJson(item); - } - //If it's a new text node, just store the text value and add as a new object attribute - else if((typeof(obj[nodeName]) == "undefined") && (item.nodeType == 3)){ - obj = item.nodeValue == "false" ? false : item.nodeValue == "true" ? true : item.nodeValue; - } - //If this node name is already stored as an object attribute... - else if(typeof(obj[nodeName]) != "undefined"){ - - //Cache what we have now - var old = obj[nodeName]; - if(!Array.isArray(old)) - old = [old]; - - //Create a new object to store this node info - var newNode = {}; - - //Add the new node info to the existing array we have now - if(item.nodeType == 1){ - newNode = this.toJson(item); - var newArray = old.concat(newNode); - } - else if(item.nodeType == 3){ - newNode = item.nodeValue; - var newArray = old.concat(newNode); - } - - //Store the attributes for this node - _.each(item.attributes, function(attr){ - newNode[attr.localName] = attr.nodeValue; - }); - - //Replace the old array with the updated one - obj[nodeName] = newArray; - - //Exit - continue; - } + model.set("errorMessage", parsedResponse); - //Store the attributes for this node - /*_.each(item.attributes, function(attr){ - obj[nodeName][attr.localName] = attr.nodeValue; - });*/ + //Set the model status as e for error + model.set("uploadStatus", "e"); - } + //Trigger a custom event for the model save error + model.trigger("errorSaving", parsedResponse); + // Track this error in our analytics + MetacatUI.analytics?.trackException( + `DataONEObject save error: ${parsedResponse}`, + model.get("id"), + true, + ); } - return obj; }, + }; - /** - Serialize the DataONE object JSON to XML - @param {object} json - the JSON object to convert to XML - @param {Element} containerNode - an HTML element to insertt the resulting XML into - @returns {Element} The updated HTML Element - */ - toXML: function(json, containerNode){ - - if(typeof json == "string"){ - containerNode.textContent = json; - return containerNode; - } + //Add the user settings + requestSettings = _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ); - for(var i=0; i<Object.keys(json).length; i++){ - var key = Object.keys(json)[i], - contents = json[key] || json[key]; + //Send the Save request + Backbone.Model.prototype.save.call(this, null, requestSettings); + }, - var node = document.createElement(key); + /** + * This function is executed when the XHR that saves this DataONEObject has + * successfully completed. It can be called directly if a DataONEObject is saved + * without directly using the DataONEObject.save() function. + * @param {DataONEObject} [model] A reference to this DataONEObject model + * @param {XMLHttpRequest.response} [response] The XHR response object + * @param {XMLHttpRequest} [xhr] The XHR that was just completed successfully + */ + onSuccessfulSave: function (model, response, xhr) { + if (typeof model == "undefined") { + var model = this; + } - //Skip this attribute if it is not populated - if(!contents || (Array.isArray(contents) && !contents.length)) - continue; + model.set("numSaveAttempts", 0); + model.set("uploadStatus", "c"); + model.set("isNew", false); + model.trigger("successSaving", model); - //If it's a simple text node - if(typeof contents == "string"){ - containerNode.textContent = contents; - return containerNode; - } - else if(Array.isArray(contents)){ - var allNewNodes = []; + // Get the newest sysmeta set by the MN + model.fetch({ + merge: true, + systemMetadataOnly: true, + }); - for(var ii=0; ii<contents.length; ii++){ - allNewNodes.push(this.toXML(contents[ii], $(node).clone()[0])); - } + // Reset the content changes status + model.set("hasContentChanges", false); - if(allNewNodes.length) - node = allNewNodes; - } - else if(typeof contents == "object"){ - $(node).append(this.toXML(contents, node)); - var attributeNames = _.without(Object.keys(json[key]), "content"); - } + //Reset the model isNew attribute + model.set("isNew", false); - $(containerNode).append(node); - } + // Reset oldPid so we can replace again + model.set("oldPid", null); - return containerNode; - }, + //Set the last-calculated checksum as the original checksum + model.set("originalChecksum", model.get("checksum")); + model.set("checksum", model.defaults().checksum); + }, - /** - * Saves the DataONEObject System Metadata to the server - */ - save: function(attributes, options){ + /** + * Updates the DataONEObject System Metadata to the server + */ + updateSysMeta: function () { + //Update the upload status to "p" for "in progress" + this.set("uploadStatus", "p"); + //Update the system metadata upload status to "p" as well, so the app + // knows that the system metadata, specifically, is being updated. + this.set("sysMetaUploadStatus", "p"); + + var formData = new FormData(); + + //Add the identifier to the XHR data + formData.append("pid", this.get("id")); + + var sysMetaXML = this.serializeSysMeta(); + + //Send the system metadata as a Blob + var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" }); + //Add the system metadata XML to the XHR data + formData.append("sysmeta", xmlBlob, "sysmeta.xml"); + + var model = this; + + var baseUrl = "", + activeAltRepo = MetacatUI.appModel.getActiveAltRepo(); + //Use the meta service URL from the alt repo + if (activeAltRepo) { + baseUrl = activeAltRepo.metaServiceUrl; + } + //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel + else { + baseUrl = MetacatUI.appModel.get("metaServiceUrl"); + } - // Set missing file names before saving - if ( ! this.get("fileName") ) { - this.setMissingFileName(); - } - else{ - //Replace all non-alphanumeric characters with underscores - var fileNameWithoutExt = this.get("fileName").substring(0, this.get("fileName").lastIndexOf(".")), - extension = this.get("fileName").substring(this.get("fileName").lastIndexOf("."), this.get("fileName").length); - this.set("fileName", fileNameWithoutExt.replace(/[^a-zA-Z0-9]/g, "_") + extension); - } + var requestSettings = { + url: baseUrl + encodeURIComponent(this.get("id")), + cache: false, + contentType: false, + dataType: "text", + type: "PUT", + processData: false, + data: formData, + parse: false, + success: function () { + model.set("numSaveAttempts", 0); + + //Fetch the system metadata from the server so we have a fresh copy of the newest sys meta. + model.fetch({ systemMetadataOnly: true }); + + model.set("sysMetaErrorCode", null); + + //Update the upload status to "c" for "complete" + model.set("uploadStatus", "c"); + model.set("sysMetaUploadStatus", "c"); + + //Trigger a custom event that the sys meta was updated + model.trigger("sysMetaUpdated"); + }, + error: function (xhr, status, statusCode) { + model.set("numSaveAttempts", model.get("numSaveAttempts") + 1); + var numSaveAttempts = model.get("numSaveAttempts"); + + if (numSaveAttempts < 3 && (statusCode == 408 || statusCode == 0)) { + //Try saving again in 10, 40, and 90 seconds + setTimeout( + function () { + model.updateSysMeta.call(model); + }, + numSaveAttempts * numSaveAttempts * 10000, + ); + } else { + model.set("numSaveAttempts", 0); - if ( !this.hasUpdates() ) { - this.set("uploadStatus", null); - return; - } + var parsedResponse = $(xhr.responseText) + .not("style, title") + .text(); - //Set the upload transfer as in progress - this.set("uploadProgress", 2); - this.set("uploadStatus", "p"); - - //Check if the checksum has been calculated yet. - if( !this.get("checksum") ){ - //When it is calculated, restart this function - this.on("checksumCalculated", this.save); - //Calculate the checksum for this file - this.calculateChecksum(); - //Exit this function until the checksum is done - return; - } + //When there is no network connection (status == 0), there will be no response text + if (!parsedResponse) + parsedResponse = + "There was a network issue that prevented this file from updating. " + + "Make sure you are connected to a reliable internet connection."; - //Create a FormData object to send data with our XHR - var formData = new FormData(); + model.set("errorMessage", parsedResponse); - //If this is not a new object, update the id. New DataONEObjects will have an id - // created during initialize. - if( !this.isNew() ){ - this.updateID(); - formData.append("pid", this.get("oldPid")); - formData.append("newPid", this.get("id")); - } - else{ - //Create an ID if there isn't one - if( !this.get("id") ){ - this.set("id", "urn:uuid:" + uuid.v4()); - } + model.set("sysMetaErrorCode", statusCode); - //Add the identifier to the XHR data - formData.append("pid", this.get("id")); - } + model.set("uploadStatus", "e"); + model.set("sysMetaUploadStatus", "e"); - //Create the system metadata XML - var sysMetaXML = this.serializeSysMeta(); + // Trigger a custom event for the sysmeta update that + // errored + model.trigger("sysMetaUpdateError"); - //Send the system metadata as a Blob - var xmlBlob = new Blob([sysMetaXML], {type : 'application/xml'}); - //Add the system metadata XML to the XHR data - formData.append("sysmeta", xmlBlob, "sysmeta.xml"); + // Track this error in our analytics + MetacatUI.analytics?.trackException( + `DataONEObject update system metadata ` + + `error: ${parsedResponse}`, + model.get("id"), + true, + ); + } + }, + }; - // Create the new object (MN.create()) - formData.append("object", this.get("uploadFile"), this.get("fileName")); + //Add the user settings + requestSettings = _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ); - var model = this; + //Send the XHR + $.ajax(requestSettings); + }, - // On create(), add to the package and the metadata - // Note: This should be added to the parent collection - // but for now we are using the root collection - _.each(this.get("collections"), function(collection){ + /** + * Check if the current user is authorized to perform an action on this object. This function doesn't return + * the result of the check, but it sends an XHR, updates this model, and triggers a change event. + * @param {string} [action=changePermission] - The action (read, write, or changePermission) to check + * if the current user has authorization to perform. By default checks for the highest level of permission. + * @param {object} [options] Additional options for this function. See the properties below. + * @property {function} options.onSuccess - A function to execute when the checkAuthority API is successfully completed + * @property {function} options.onError - A function to execute when the checkAuthority API returns an error, or when no PID or SID can be found for this object. + * @return {boolean} + */ + checkAuthority: function (action = "changePermission", options) { + try { + // return false - if neither PID nor SID is present to check the authority + if (this.get("id") == null && this.get("seriesId") == null) { + return false; + } - if(collection.type == "DataPackage"){ - this.off("successSaving", collection.addNewModel); - this.once("successSaving", collection.addNewModel, collection); - } + if (typeof options == "undefined") { + var options = {}; + } - }, this); - - - //Put together the AJAX and Backbone.save() options - var requestSettings = { - url: this.url(), - cache: false, - contentType: false, - dataType: "text", - processData: false, - data: formData, - parse: false, - xhr: function(){ - var xhr = new window.XMLHttpRequest(); - - //Upload progress - xhr.upload.addEventListener("progress", function(evt){ - if (evt.lengthComputable) { - var percentComplete = evt.loaded / evt.total * 100; - - model.set("uploadProgress", percentComplete); - } - }, false); - - return xhr; - }, - success: this.onSuccessfulSave, - error: function(model, response, xhr){ - - //Reset the identifier changes - model.resetID(); - //Reset the checksum, if this is a model that needs to be serialized with each save. - if( model.serialize ){ - model.set("checksum", model.defaults().checksum); - } - - model.set("numSaveAttempts", model.get("numSaveAttempts") + 1); - var numSaveAttempts = model.get("numSaveAttempts"); - - if( numSaveAttempts < 3 && (response.status == 408 || response.status == 0) ){ - - //Try saving again in 10, 40, and 90 seconds - setTimeout(function(){ - model.save.call(model); - }, - (numSaveAttempts * numSaveAttempts) * 10000); - } - else{ - model.set("numSaveAttempts", 0); - - var parsedResponse = $(response.responseText).not("style, title").text(); - - //When there is no network connection (status == 0), there will be no response text - if(!parsedResponse) - parsedResponse = "There was a network issue that prevented this file from uploading. " + - "Make sure you are connected to a reliable internet connection."; - - model.set("errorMessage", parsedResponse); - - //Set the model status as e for error - model.set("uploadStatus", "e"); - - //Trigger a custom event for the model save error - model.trigger("errorSaving", parsedResponse); - - // Track this error in our analytics - MetacatUI.analytics?.trackException( - `DataONEObject save error: ${parsedResponse}`, - model.get("id"), - true - ); - } - } - }; + // If onError or onSuccess options were provided by the user, + // check that they are functions first, so we don't try to use + // some other type of variable as a function later on. + ["onError", "onSuccess"].forEach(function (userFunction) { + if (typeof options[userFunction] !== "function") { + options[userFunction] = null; + } + }); - //Add the user settings - requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()); + // If PID is not present - check authority with seriesId + var identifier = this.get("id"); + if (identifier == null) { + identifier = this.get("seriesId"); + } - //Send the Save request - Backbone.Model.prototype.save.call(this, null, requestSettings); - }, + //If there are alt repositories configured, find the possible authoritative + // Member Node for this DataONEObject. + if (MetacatUI.appModel.get("alternateRepositories").length) { + //Get the array of possible authoritative MNs + var possibleAuthMNs = this.get("possibleAuthMNs"); - /** - * This function is executed when the XHR that saves this DataONEObject has - * successfully completed. It can be called directly if a DataONEObject is saved - * without directly using the DataONEObject.save() function. - * @param {DataONEObject} [model] A reference to this DataONEObject model - * @param {XMLHttpRequest.response} [response] The XHR response object - * @param {XMLHttpRequest} [xhr] The XHR that was just completed successfully - */ - onSuccessfulSave: function(model, response, xhr){ + //If there are no possible authoritative MNs, use the auth service URL from the AppModel + if (!possibleAuthMNs.length) { + baseUrl = MetacatUI.appModel.get("authServiceUrl"); + } else { + //Use the auth service URL from the top possible auth MN + baseUrl = possibleAuthMNs[0].authServiceUrl; + } + } else { + //Get the auth service URL from the AppModel + baseUrl = MetacatUI.appModel.get("authServiceUrl"); + } - if(typeof model == "undefined"){ - var model = this; + if (!baseUrl) { + return false; } - model.set("numSaveAttempts", 0); - model.set("uploadStatus", "c"); - model.set("isNew", false); - model.trigger("successSaving", model); + var onSuccess = + options.onSuccess || + function (data, textStatus, xhr) { + model.set("isAuthorized_" + action, true); + model.set("isAuthorized", true); + model.trigger("change:isAuthorized"); + }, + onError = + options.onError || + function (xhr, textStatus, errorThrown) { + if (errorThrown == 404) { + var possibleAuthMNs = model.get("possibleAuthMNs"); + if (possibleAuthMNs.length) { + //Remove the first MN from the array, since it didn't contain the object, so it's not the auth MN + possibleAuthMNs.shift(); + } - // Get the newest sysmeta set by the MN - model.fetch({ - merge: true, - systemMetadataOnly: true - }); + //If there are no other possible auth MNs to check, trigger this model as Not Found. + if (possibleAuthMNs.length == 0 || !possibleAuthMNs) { + model.set("notFound", true); + model.trigger("notFound"); + } + //If there's more MNs to check, try again + else { + model.checkAuthority(action, options); + } + } else { + model.set("isAuthorized_" + action, false); + model.set("isAuthorized", false); + } + }; - // Reset the content changes status - model.set("hasContentChanges", false); + var model = this; + var requestSettings = { + url: baseUrl + encodeURIComponent(identifier) + "?action=" + action, + type: "GET", + success: onSuccess, + error: onError, + }; + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + } catch (e) { + //Log an error to the console + console.error("Couldn't check the authority for this user: ", e); + + // Track this error in our analytics + const name = MetacatUI.appModel.get("username"); + MetacatUI.analytics?.trackException( + `Couldn't check the authority for the user ${name}: ${e}`, + this.get("id"), + true, + ); + + //Set the user as unauthorized + model.set("isAuthorized_" + action, false); + model.set("isAuthorized", false); + return false; + } + }, - //Reset the model isNew attribute - model.set("isNew", false); + /** + * Using the attributes set on this DataONEObject model, serializes the system metadata XML + * @returns {string} + */ + serializeSysMeta: function () { + //Get the system metadata XML that currently exists in the system + var sysMetaXML = this.get("sysMetaXML"), // sysmeta as string + xml, // sysmeta as DOM object + accessPolicyXML, // The generated access policy XML + previousSiblingNode, // A DOM node indicating any previous sibling + rightsHolderNode, // A DOM node for the rights holder field + accessPolicyNode, // A DOM node for the access policy + replicationPolicyNode, // A DOM node for the replication policy + obsoletesNode, // A DOM node for the obsoletes field + obsoletedByNode, // A DOM node for the obsoletedBy field + fileNameNode, // A DOM node for the file name + xmlString, // The system metadata document as a string + nodeNameMap, // The map of camelCase to lowercase attributes + extension; // the file name extension for this object + + if (typeof sysMetaXML === "undefined" || sysMetaXML === null) { + xml = this.createSysMeta(); + } else { + xml = $($.parseHTML(sysMetaXML)); + } - // Reset oldPid so we can replace again - model.set("oldPid", null); + //Update the system metadata values + xml.find("serialversion").text(this.get("serialVersion") || "0"); + xml.find("identifier").text(this.get("newPid") || this.get("id")); + xml + .find("submitter") + .text( + this.get("submitter") || MetacatUI.appUserModel.get("username"), + ); + xml.find("formatid").text(this.get("formatId") || this.getFormatId()); + + //If there is a seriesId, add it + if (this.get("seriesId")) { + //Get the seriesId XML node + var seriesIdNode = xml.find("seriesId"); + + //If it doesn't exist, create one + if (!seriesIdNode.length) { + seriesIdNode = $(document.createElement("seriesid")); + xml.find("identifier").before(seriesIdNode); + } - //Set the last-calculated checksum as the original checksum - model.set("originalChecksum", model.get("checksum")); - model.set("checksum", model.defaults().checksum); - }, + //Add the seriesId string to the XML node + seriesIdNode.text(this.get("seriesId")); + } - /** - * Updates the DataONEObject System Metadata to the server - */ - updateSysMeta: function () { + //If there is no size, get it + if (!this.get("size") && this.get("uploadFile")) { + this.set("size", this.get("uploadFile").size); + } - //Update the upload status to "p" for "in progress" - this.set("uploadStatus", "p"); - //Update the system metadata upload status to "p" as well, so the app - // knows that the system metadata, specifically, is being updated. - this.set("sysMetaUploadStatus", "p"); + //Get the size of the file, if there is one + if (this.get("uploadFile")) { + xml.find("size").text(this.get("uploadFile").size); + } + //Otherwise, use the last known size + else { + xml.find("size").text(this.get("size")); + } - var formData = new FormData(); + //Save the original checksum + if (!this.get("checksum") && this.get("originalChecksum")) { + xml.find("checksum").text(this.get("originalChecksum")); + } + //Update the checksum and checksum algorithm + else { + xml.find("checksum").text(this.get("checksum")); + xml.find("checksum").attr("algorithm", this.get("checksumAlgorithm")); + } - //Add the identifier to the XHR data - formData.append("pid", this.get("id")); + //Update the rightsholder + xml + .find("rightsholder") + .text( + this.get("rightsHolder") || MetacatUI.appUserModel.get("username"), + ); - var sysMetaXML = this.serializeSysMeta(); + //Write the access policy + accessPolicyXML = this.get("accessPolicy").serialize(); - //Send the system metadata as a Blob - var xmlBlob = new Blob([sysMetaXML], {type: 'application/xml'}); - //Add the system metadata XML to the XHR data - formData.append("sysmeta", xmlBlob, "sysmeta.xml"); + // Get the access policy node, if it exists + accessPolicyNode = xml.find("accesspolicy"); - var model = this; + previousSiblingNode = xml.find("rightsholder"); - var baseUrl = "", - activeAltRepo = MetacatUI.appModel.getActiveAltRepo(); - //Use the meta service URL from the alt repo - if( activeAltRepo ){ - baseUrl = activeAltRepo.metaServiceUrl; - } - //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel - else{ - baseUrl = MetacatUI.appModel.get("metaServiceUrl"); - } + // Create an access policy node if needed + if (!accessPolicyNode.length && accessPolicyXML) { + accessPolicyNode = $(document.createElement("accesspolicy")); + previousSiblingNode.after(accessPolicyNode); + } - var requestSettings = { - url: baseUrl + (encodeURIComponent(this.get("id"))), - cache: false, - contentType: false, - dataType: "text", - type: "PUT", - processData: false, - data: formData, - parse: false, - success: function() { + //Replace the old access policy with the new one if it exists + if (accessPolicyXML) { + accessPolicyNode.replaceWith(accessPolicyXML); + } else { + // Remove the node if it is empty + accessPolicyNode.remove(); + } - model.set("numSaveAttempts", 0); + // Set the obsoletes node after replPolicy or accessPolicy, or rightsHolder + replicationPolicyNode = xml.find("replicationpolicy"); + accessPolicyNode = xml.find("accesspolicy"); + rightsHolderNode = xml.find("rightsholder"); + + if (replicationPolicyNode.length) { + previousSiblingNode = replicationPolicyNode; + } else if (accessPolicyNode.length) { + previousSiblingNode = accessPolicyNode; + } else { + previousSiblingNode = rightsHolderNode; + } - //Fetch the system metadata from the server so we have a fresh copy of the newest sys meta. - model.fetch({ systemMetadataOnly: true }); + obsoletesNode = xml.find("obsoletes"); - model.set("sysMetaErrorCode", null); + if (this.get("obsoletes")) { + if (obsoletesNode.length) { + obsoletesNode.text(this.get("obsoletes")); + } else { + obsoletesNode = $(document.createElement("obsoletes")).text( + this.get("obsoletes"), + ); + previousSiblingNode.after(obsoletesNode); + } + } else { + if (obsoletesNode) { + obsoletesNode.remove(); + } + } - //Update the upload status to "c" for "complete" - model.set("uploadStatus", "c"); - model.set("sysMetaUploadStatus", "c"); + if (obsoletesNode) { + previousSiblingNode = obsoletesNode; + } - //Trigger a custom event that the sys meta was updated - model.trigger("sysMetaUpdated"); - }, - error: function (xhr, status, statusCode) { + obsoletedByNode = xml.find("obsoletedby"); - model.set("numSaveAttempts", model.get("numSaveAttempts") + 1); - var numSaveAttempts = model.get("numSaveAttempts"); + //remove the obsoletedBy node if it exists + // TODO: Verify this is what we want to do + if (obsoletedByNode) { + obsoletedByNode.remove(); + } - if (numSaveAttempts < 3 && (statusCode == 408 || statusCode == 0)) { + xml.find("archived").text(this.get("archived") || "false"); + xml + .find("dateuploaded") + .text(this.get("dateUploaded") || new Date().toISOString()); - //Try saving again in 10, 40, and 90 seconds - setTimeout(function () { - model.updateSysMeta.call(model); - }, - (numSaveAttempts * numSaveAttempts) * 10000); - } else { - model.set("numSaveAttempts", 0); + //Get the filename node + fileNameNode = xml.find("filename"); - var parsedResponse = $(xhr.responseText).not("style, title").text(); + //If the filename node doesn't exist, then create one + if (!fileNameNode.length) { + fileNameNode = $(document.createElement("filename")); + xml.find("dateuploaded").after(fileNameNode); + } - //When there is no network connection (status == 0), there will be no response text - if (!parsedResponse) - parsedResponse = "There was a network issue that prevented this file from updating. " + - "Make sure you are connected to a reliable internet connection."; + //Set the object file name + $(fileNameNode).text(this.get("fileName")); - model.set("errorMessage", parsedResponse); + xmlString = $(document.createElement("div")).append(xml.clone()).html(); - model.set("sysMetaErrorCode", statusCode); + //Now camel case the nodes + nodeNameMap = this.nodeNameMap(); - model.set("uploadStatus", "e"); - model.set("sysMetaUploadStatus", "e"); + _.each( + Object.keys(nodeNameMap), + function (name, i) { + var originalXMLString = xmlString; - // Trigger a custom event for the sysmeta update that - // errored - model.trigger("sysMetaUpdateError"); + //Camel case node names + var regEx = new RegExp("<" + name, "g"); + xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name]); + var regEx = new RegExp(name + ">", "g"); + xmlString = xmlString.replace(regEx, nodeNameMap[name] + ">"); - // Track this error in our analytics - MetacatUI.analytics?.trackException( - `DataONEObject update system metadata ` + - `error: ${parsedResponse}`, - model.get("id"), - true - ); - } - } + //If node names haven't been changed, then find an attribute + if (xmlString == originalXMLString) { + var regEx = new RegExp(" " + name + "=", "g"); + xmlString = xmlString.replace( + regEx, + " " + nodeNameMap[name] + "=", + ); } - - //Add the user settings - requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()); - - //Send the XHR - $.ajax(requestSettings); }, + this, + ); - /** - * Check if the current user is authorized to perform an action on this object. This function doesn't return - * the result of the check, but it sends an XHR, updates this model, and triggers a change event. - * @param {string} [action=changePermission] - The action (read, write, or changePermission) to check - * if the current user has authorization to perform. By default checks for the highest level of permission. - * @param {object} [options] Additional options for this function. See the properties below. - * @property {function} options.onSuccess - A function to execute when the checkAuthority API is successfully completed - * @property {function} options.onError - A function to execute when the checkAuthority API returns an error, or when no PID or SID can be found for this object. - * @return {boolean} - */ - checkAuthority: function(action = "changePermission", options){ - - try{ - // return false - if neither PID nor SID is present to check the authority - if ( (this.get("id") == null) && (this.get("seriesId") == null) ) { - return false; - } + xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata"); - if( typeof options == "undefined" ){ - var options = {}; - } + return xmlString; + }, - // If onError or onSuccess options were provided by the user, - // check that they are functions first, so we don't try to use - // some other type of variable as a function later on. - ["onError", "onSuccess"].forEach(function(userFunction){ - if(typeof options[userFunction] !== "function"){ - options[userFunction] = null; - } - }); + /** + * Get the object format identifier for this object + */ + getFormatId: function () { + var formatId = "application/octet-stream", // default to untyped data + objectFormats = { + mediaTypes: [], // The list of potential formatIds based on mediaType matches + extensions: [], // The list of possible formatIds based onextension matches + }, + fileName = this.get("fileName"), // the fileName for this object + ext; // The extension of the filename for this object + + objectFormats["mediaTypes"] = MetacatUI.objectFormats.where({ + formatId: this.get("mediaType"), + }); + if ( + typeof fileName !== "undefined" && + fileName !== null && + fileName.length > 1 + ) { + ext = fileName.substring( + fileName.lastIndexOf(".") + 1, + fileName.length, + ); + objectFormats["extensions"] = MetacatUI.objectFormats.where({ + extension: ext, + }); + } - // If PID is not present - check authority with seriesId - var identifier = this.get("id"); - if ( (identifier == null) ) { - identifier = this.get("seriesId"); - } + if ( + objectFormats["mediaTypes"].length > 0 && + objectFormats["extensions"].length > 0 + ) { + var firstMediaType = objectFormats["mediaTypes"][0].get("formatId"); + var firstExtension = objectFormats["extensions"][0].get("formatId"); + // Check if they're equal + if (firstMediaType === firstExtension) { + formatId = firstMediaType; + return formatId; + } + // Handle mismatched mediaType and extension cases - additional cases can be added below + if ( + firstMediaType === "application/vnd.ms-excel" && + firstExtension === "text/csv" + ) { + formatId = firstExtension; + return formatId; + } + } - //If there are alt repositories configured, find the possible authoritative - // Member Node for this DataONEObject. - if( MetacatUI.appModel.get("alternateRepositories").length ){ + if (objectFormats["mediaTypes"].length > 0) { + formatId = objectFormats["mediaTypes"][0].get("formatId"); + console.log("returning default mediaType"); + console.log(formatId); + return formatId; + } - //Get the array of possible authoritative MNs - var possibleAuthMNs = this.get("possibleAuthMNs"); + if (objectFormats["extensions"].length > 0) { + //If this is a "nc" file, assume it is a netCDF-3 file. + if (ext == "nc") { + formatId = "netCDF-3"; + } else { + formatId = objectFormats["extensions"][0].get("formatId"); + } + return formatId; + } - //If there are no possible authoritative MNs, use the auth service URL from the AppModel - if( !possibleAuthMNs.length ){ - baseUrl = MetacatUI.appModel.get("authServiceUrl"); - } - else{ - //Use the auth service URL from the top possible auth MN - baseUrl = possibleAuthMNs[0].authServiceUrl; - } + return formatId; + }, - } - else{ - //Get the auth service URL from the AppModel - baseUrl = MetacatUI.appModel.get("authServiceUrl"); - } + /** + * Looks up human readable format of the DataONE Object + * @returns format String + * @since 2.28.0 + */ + getFormat: function () { + var formatMap = { + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + "Microsoft Excel OpenXML", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + "Microsoft Word OpenXML", + "application/vnd.ms-excel.sheet.binary.macroEnabled.12": + "Microsoft Office Excel 2007 binary workbooks", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": + "Microsoft Office OpenXML Presentation", + "application/vnd.ms-excel": "Microsoft Excel", + "application/msword": "Microsoft Word", + "application/vnd.ms-powerpoint": "Microsoft Powerpoint", + "text/html": "HTML", + "text/plain": "plain text (.txt)", + "video/avi": "Microsoft AVI file", + "video/x-ms-wmv": "Windows Media Video (.wmv)", + "audio/x-ms-wma": "Windows Media Audio (.wma)", + "application/vnd.google-earth.kml xml": + "Google Earth Keyhole Markup Language (KML)", + "http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html": + "annotation", + "application/mathematica": "Mathematica Notebook", + "application/postscript": "Postscript", + "application/rtf": "Rich Text Format (RTF)", + "application/xml": "XML Application", + "text/xml": "XML", + "application/x-fasta": "FASTA sequence file", + "nexus/1997": "NEXUS File Format for Systematic Information", + "anvl/erc-v02": + "Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13", + "http://purl.org/dryad/terms/": + "Dryad Metadata Application Profile Version 3.0", + "http://datadryad.org/profile/v3.1": + "Dryad Metadata Application Profile Version 3.1", + "application/pdf": "PDF", + "application/zip": "ZIP file", + "http://www.w3.org/TR/rdf-syntax-grammar": "RDF/XML", + "http://www.w3.org/TR/rdfa-syntax": "RDFa", + "application/rdf xml": "RDF", + "text/turtle": "TURTLE", + "text/n3": "N3", + "application/x-gzip": "GZIP Format", + "application/x-python": "Python script", + "http://www.w3.org/2005/Atom": "ATOM-1.0", + "application/octet-stream": "octet stream (application file)", + "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd": + "Darwin Core, v2.0", + "http://rs.tdwg.org/dwc/xsd/simpledarwincore/": "Simple Darwin Core", + "eml://ecoinformatics.org/eml-2.1.0": "EML v2.1.0", + "eml://ecoinformatics.org/eml-2.1.1": "EML v2.1.1", + "eml://ecoinformatics.org/eml-2.0.1": "EML v2.0.1", + "eml://ecoinformatics.org/eml-2.0.0": "EML v2.0.0", + "https://eml.ecoinformatics.org/eml-2.2.0": "EML v2.2.0", + }; + + return formatMap[this.get("formatId")] || this.get("formatId"); + }, - if( !baseUrl ){ - return false; - } + /** + * Build a fresh system metadata document for this object when it is new + * Return it as a DOM object + */ + createSysMeta: function () { + var sysmetaDOM, // The DOM + sysmetaXML = []; // The document as a string array + + sysmetaXML.push( + //'<?xml version="1.0" encoding="UTF-8"?>', + "<d1_v2.0:systemmetadata", + ' xmlns:d1_v2.0="http://ns.dataone.org/service/types/v2.0"', + ' xmlns:d1="http://ns.dataone.org/service/types/v1">', + " <serialversion />", + " <identifier />", + " <formatid />", + " <size />", + " <checksum />", + " <submitter />", + " <rightsholder />", + " <filename />", + "</d1_v2.0:systemmetadata>", + ); + + sysmetaDOM = $($.parseHTML(sysmetaXML.join(""))); + return sysmetaDOM; + }, - var onSuccess = options.onSuccess || function(data, textStatus, xhr) { - model.set("isAuthorized_" + action, true); - model.set("isAuthorized", true); - model.trigger("change:isAuthorized"); - }, - onError = options.onError || function(xhr, textStatus, errorThrown){ - if(errorThrown == 404){ - var possibleAuthMNs = model.get("possibleAuthMNs"); - if( possibleAuthMNs.length ){ - //Remove the first MN from the array, since it didn't contain the object, so it's not the auth MN - possibleAuthMNs.shift(); - } - - //If there are no other possible auth MNs to check, trigger this model as Not Found. - if( possibleAuthMNs.length == 0 || !possibleAuthMNs ){ - model.set("notFound", true); - model.trigger("notFound"); - } - //If there's more MNs to check, try again - else{ - model.checkAuthority(action, options); - } - } - else{ - model.set("isAuthorized_" + action, false); - model.set("isAuthorized", false); - } - }; - - var model = this; - var requestSettings = { - url: baseUrl + encodeURIComponent(identifier) + "?action=" + action, - type: "GET", - success: onSuccess, - error: onError + /** + * Create an access policy for this DataONEObject using the default access + * policy set in the AppModel. + * + * @param {Element} [accessPolicyXML] - An <accessPolicy> XML node + * that contains a list of access rules. + * @return {AccessPolicy} - an AccessPolicy collection that represents the + * given XML or the default policy set in the AppModel. + */ + createAccessPolicy: function (accessPolicyXML) { + //Create a new AccessPolicy collection + var accessPolicy = new AccessPolicy(); + + accessPolicy.dataONEObject = this; + + //If there is no access policy XML sent, + if (this.isNew() && !accessPolicyXML) { + try { + //If the app is configured to inherit the access policy from the parent metadata, + // then get the parent metadata and copy it's AccessPolicy + let scienceMetadata = this.get("isDocumentedByModels"); + if ( + MetacatUI.appModel.get("inheritAccessPolicy") && + scienceMetadata && + scienceMetadata.length + ) { + let sciMetaAccessPolicy = scienceMetadata[0].get("accessPolicy"); + + if (sciMetaAccessPolicy) { + accessPolicy.copyAccessPolicy(sciMetaAccessPolicy); + } else { + accessPolicy.createDefaultPolicy(); } - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); } - catch(e){ - //Log an error to the console - console.error("Couldn't check the authority for this user: ", e); + //Otherwise, set the default access policy using the AppModel configuration + else { + accessPolicy.createDefaultPolicy(); + } + } catch (e) { + console.error( + "Could create access policy, so defaulting to default", + e, + ); + accessPolicy.createDefaultPolicy(); + } + } else { + //Parse the access policy XML to create AccessRule models from the XML + accessPolicy.parse(accessPolicyXML); + } - // Track this error in our analytics - const name = MetacatUI.appModel.get('username') - MetacatUI.analytics?.trackException( - `Couldn't check the authority for the user ${name}: ${e}`, - this.get("id"), - true - ); + //Listen to changes on the collection and trigger a change on this model + var self = this; + this.listenTo(accessPolicy, "change update", function () { + self.trigger("change"); + this.addToUploadQueue(); + }); - //Set the user as unauthorized - model.set("isAuthorized_" + action, false); - model.set("isAuthorized", false); - return false; + return accessPolicy; + }, - } + /** + * Update identifiers for this object + * + * @param {string} id - Optional identifier to update with. Generated + * automatically when not given. + * + * Note that this method caches the objects attributes prior to + * updating so this.resetID() can be called in case of a failure + * state. + * + * Also note that this method won't run if theh oldPid attribute is + * set. This enables knowing before this.save is called what the next + * PID will be such as the case where we want to update a matching + * EML entity when replacing files. + */ + updateID: function (id) { + // Only run once until oldPid is reset + if (this.get("oldPid")) { + return; + } - }, + //Save the attributes so we can reset the ID later + this.attributeCache = this.toJSON(); - /** - * Using the attributes set on this DataONEObject model, serializes the system metadata XML - * @returns {string} - */ - serializeSysMeta: function(){ - //Get the system metadata XML that currently exists in the system - var sysMetaXML = this.get("sysMetaXML"), // sysmeta as string - xml, // sysmeta as DOM object - accessPolicyXML, // The generated access policy XML - previousSiblingNode, // A DOM node indicating any previous sibling - rightsHolderNode, // A DOM node for the rights holder field - accessPolicyNode, // A DOM node for the access policy - replicationPolicyNode, // A DOM node for the replication policy - obsoletesNode, // A DOM node for the obsoletes field - obsoletedByNode, // A DOM node for the obsoletedBy field - fileNameNode, // A DOM node for the file name - xmlString, // The system metadata document as a string - nodeNameMap, // The map of camelCase to lowercase attributes - extension; // the file name extension for this object - - if ( typeof sysMetaXML === "undefined" || sysMetaXML === null ) { - xml = this.createSysMeta(); - } else { - xml = $($.parseHTML(sysMetaXML)); - } + //Set the old identifier + var oldPid = this.get("id"), + selfDocuments, + selfDocumentedBy, + documentedModels, + documentedModel, + index; - //Update the system metadata values - xml.find("serialversion").text(this.get("serialVersion") || "0"); - xml.find("identifier").text((this.get("newPid") || this.get("id"))); - xml.find("submitter").text(this.get("submitter") || MetacatUI.appUserModel.get("username")); - xml.find("formatid").text(this.get("formatId") || this.getFormatId()); - - //If there is a seriesId, add it - if( this.get("seriesId") ){ - //Get the seriesId XML node - var seriesIdNode = xml.find("seriesId"); - - //If it doesn't exist, create one - if( !seriesIdNode.length ){ - seriesIdNode = $(document.createElement("seriesid")); - xml.find("identifier").before(seriesIdNode); - } + //Save the current id as the old pid + this.set("oldPid", oldPid); - //Add the seriesId string to the XML node - seriesIdNode.text( this.get("seriesId") ); - } + //Create a new seriesId, if there isn't one, and if this model specifies that one is required + if (!this.get("seriesId") && this.get("createSeriesId")) { + this.set("seriesId", "urn:uuid:" + uuid.v4()); + } - //If there is no size, get it - if( !this.get("size") && this.get("uploadFile")){ - this.set("size", this.get("uploadFile").size); - } + // Check to see if the old pid documents or is documented by itself + selfDocuments = _.contains(this.get("documents"), oldPid); + selfDocumentedBy = _.contains(this.get("isDocumentedBy"), oldPid); + + //Set the new identifier + if (id) { + this.set("id", id); + } else { + if (this.get("type") == "DataPackage") { + this.set("id", "resource_map_urn:uuid:" + uuid.v4()); + } else { + this.set("id", "urn:uuid:" + uuid.v4()); + } + } - //Get the size of the file, if there is one - if( this.get("uploadFile") ){ - xml.find("size").text( this.get("uploadFile").size ); - } - //Otherwise, use the last known size - else{ - xml.find("size").text(this.get("size")); - } + // Remove the old pid from the documents list if present + if (selfDocuments) { + index = this.get("documents").indexOf(oldPid); + if (index > -1) { + this.get("documents").splice(index, 1); + } + // And add the new pid in + this.get("documents").push(this.get("id")); + } - //Save the original checksum - if( !this.get("checksum") && this.get("originalChecksum") ){ - xml.find("checksum").text(this.get("originalChecksum")); - } - //Update the checksum and checksum algorithm - else{ - xml.find("checksum").text(this.get("checksum")); - xml.find("checksum").attr("algorithm", this.get("checksumAlgorithm")); - } - - //Update the rightsholder - xml.find("rightsholder").text(this.get("rightsHolder") || MetacatUI.appUserModel.get("username")); - - //Write the access policy - accessPolicyXML = this.get("accessPolicy").serialize(); - - // Get the access policy node, if it exists - accessPolicyNode = xml.find("accesspolicy"); - - previousSiblingNode = xml.find("rightsholder"); - - // Create an access policy node if needed - if ( (! accessPolicyNode.length) && accessPolicyXML ) { - accessPolicyNode = $(document.createElement("accesspolicy")); - previousSiblingNode.after(accessPolicyNode); - - } - - //Replace the old access policy with the new one if it exists - if ( accessPolicyXML ) { - accessPolicyNode.replaceWith(accessPolicyXML); - } else { - // Remove the node if it is empty - accessPolicyNode.remove(); - } - - // Set the obsoletes node after replPolicy or accessPolicy, or rightsHolder - replicationPolicyNode = xml.find("replicationpolicy"); - accessPolicyNode = xml.find("accesspolicy"); - rightsHolderNode = xml.find("rightsholder"); - - if ( replicationPolicyNode.length ) { - previousSiblingNode = replicationPolicyNode; - } else if ( accessPolicyNode.length ) { - previousSiblingNode = accessPolicyNode; - } else { - previousSiblingNode = rightsHolderNode; - } - - obsoletesNode = xml.find("obsoletes"); - - if( this.get("obsoletes") ){ - if( obsoletesNode.length ) { - obsoletesNode.text(this.get("obsoletes")); - } - else { - obsoletesNode = $(document.createElement("obsoletes")).text(this.get("obsoletes")); - previousSiblingNode.after(obsoletesNode); - } - } - else { - if ( obsoletesNode ) { - obsoletesNode.remove(); - } - } - - if ( obsoletesNode ) { - previousSiblingNode = obsoletesNode; - } - - obsoletedByNode = xml.find("obsoletedby"); - - //remove the obsoletedBy node if it exists - // TODO: Verify this is what we want to do - if ( obsoletedByNode ) { - obsoletedByNode.remove(); - } - - xml.find("archived").text(this.get("archived") || "false"); - xml.find("dateuploaded").text(this.get("dateUploaded") || new Date().toISOString()); - - //Get the filename node - fileNameNode = xml.find("filename"); - - //If the filename node doesn't exist, then create one - if( ! fileNameNode.length ){ - fileNameNode = $(document.createElement("filename")); - xml.find("dateuploaded").after(fileNameNode); - } - - //Set the object file name - $(fileNameNode).text(this.get("fileName")); - - xmlString = $(document.createElement("div")).append(xml.clone()).html(); - - //Now camel case the nodes - nodeNameMap = this.nodeNameMap(); - - _.each(Object.keys(nodeNameMap), function(name, i){ - var originalXMLString = xmlString; - - //Camel case node names - var regEx = new RegExp("<" + name, "g"); - xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name]); - var regEx = new RegExp(name + ">", "g"); - xmlString = xmlString.replace(regEx, nodeNameMap[name] + ">"); - - //If node names haven't been changed, then find an attribute - if(xmlString == originalXMLString){ - var regEx = new RegExp(" " + name + "=", "g"); - xmlString = xmlString.replace(regEx, " " + nodeNameMap[name] + "="); - } - }, this); - - xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata"); - - return xmlString; - }, - - /** - * Get the object format identifier for this object - */ - getFormatId: function() { - var formatId = "application/octet-stream", // default to untyped data - objectFormats = { - "mediaTypes": [], // The list of potential formatIds based on mediaType matches - "extensions": [] // The list of possible formatIds based onextension matches - }, - fileName = this.get("fileName"), // the fileName for this object - ext; // The extension of the filename for this object - - objectFormats["mediaTypes"] = MetacatUI.objectFormats.where({formatId: this.get("mediaType")}); - if ( typeof fileName !== "undefined" && fileName !== null && fileName.length > 1) { - ext = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length); - objectFormats["extensions"] = MetacatUI.objectFormats.where({extension: ext}); - } - - if (objectFormats["mediaTypes"].length > 0 && objectFormats["extensions"].length > 0) { - var firstMediaType = objectFormats["mediaTypes"][0].get("formatId"); - var firstExtension = objectFormats["extensions"][0].get("formatId"); - // Check if they're equal - if (firstMediaType === firstExtension) { - formatId = firstMediaType; - return formatId; - } - // Handle mismatched mediaType and extension cases - additional cases can be added below - if (firstMediaType === 'application/vnd.ms-excel' && firstExtension === 'text/csv') { - formatId = firstExtension; - return formatId; - } - } - - if (objectFormats["mediaTypes"].length > 0) { - formatId = objectFormats["mediaTypes"][0].get("formatId"); - console.log('returning default mediaType'); - console.log(formatId); - return formatId; - } - - if (objectFormats["extensions"].length > 0 ) { - //If this is a "nc" file, assume it is a netCDF-3 file. - if (ext == "nc") { - formatId = "netCDF-3"; - } else { - formatId = objectFormats["extensions"][0].get("formatId"); - } - return formatId; - } - - return formatId; - - }, + // Remove the old pid from the isDocumentedBy list if present + if (selfDocumentedBy) { + index = this.get("isDocumentedBy").indexOf(oldPid); + if (index > -1) { + this.get("isDocumentedBy").splice(index, 1); + } + // And add the new pid in + this.get("isDocumentedBy").push(this.get("id")); + } - /** - * Looks up human readable format of the DataONE Object - * @returns format String - * @since 2.28.0 - */ - getFormat: function(){ - var formatMap = { - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : "Microsoft Excel OpenXML", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "Microsoft Word OpenXML", - "application/vnd.ms-excel.sheet.binary.macroEnabled.12" : "Microsoft Office Excel 2007 binary workbooks", - "application/vnd.openxmlformats-officedocument.presentationml.presentation" : "Microsoft Office OpenXML Presentation", - "application/vnd.ms-excel" : "Microsoft Excel", - "application/msword" : "Microsoft Word", - "application/vnd.ms-powerpoint" : "Microsoft Powerpoint", - "text/html" : "HTML", - "text/plain": "plain text (.txt)", - "video/avi" : "Microsoft AVI file", - "video/x-ms-wmv" : "Windows Media Video (.wmv)", - "audio/x-ms-wma" : "Windows Media Audio (.wma)", - "application/vnd.google-earth.kml xml" : "Google Earth Keyhole Markup Language (KML)", - "http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html" : "annotation", - "application/mathematica" : "Mathematica Notebook", - "application/postscript" : "Postscript", - "application/rtf" : "Rich Text Format (RTF)", - "application/xml" : "XML Application", - "text/xml" : "XML", - "application/x-fasta" : "FASTA sequence file", - "nexus/1997" : "NEXUS File Format for Systematic Information", - "anvl/erc-v02" : "Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13", - "http://purl.org/dryad/terms/" : "Dryad Metadata Application Profile Version 3.0", - "http://datadryad.org/profile/v3.1" : "Dryad Metadata Application Profile Version 3.1", - "application/pdf" : "PDF", - "application/zip" : "ZIP file", - "http://www.w3.org/TR/rdf-syntax-grammar" : "RDF/XML", - "http://www.w3.org/TR/rdfa-syntax" : "RDFa", - "application/rdf xml" : "RDF", - "text/turtle" : "TURTLE", - "text/n3" : "N3", - "application/x-gzip" : "GZIP Format", - "application/x-python" : "Python script", - "http://www.w3.org/2005/Atom" : "ATOM-1.0", - "application/octet-stream" : "octet stream (application file)", - "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd" : "Darwin Core, v2.0", - "http://rs.tdwg.org/dwc/xsd/simpledarwincore/" : "Simple Darwin Core", - "eml://ecoinformatics.org/eml-2.1.0" : "EML v2.1.0", - "eml://ecoinformatics.org/eml-2.1.1" : "EML v2.1.1", - "eml://ecoinformatics.org/eml-2.0.1" : "EML v2.0.1", - "eml://ecoinformatics.org/eml-2.0.0" : "EML v2.0.0", - "https://eml.ecoinformatics.org/eml-2.2.0" : "EML v2.2.0", + // Update all models documented by this pid with the new id + _.each( + this.get("documents"), + function (id) { + (documentedModels = MetacatUI.rootDataPackage.where({ id: id })), + documentedModel; + if (documentedModels.length > 0) { + documentedModel = documentedModels[0]; } - - return formatMap[this.get("formatId")] || this.get("formatId"); - }, - - /** - * Build a fresh system metadata document for this object when it is new - * Return it as a DOM object - */ - createSysMeta: function() { - var sysmetaDOM, // The DOM - sysmetaXML = []; // The document as a string array - - sysmetaXML.push( - //'<?xml version="1.0" encoding="UTF-8"?>', - '<d1_v2.0:systemmetadata', - ' xmlns:d1_v2.0="http://ns.dataone.org/service/types/v2.0"', - ' xmlns:d1="http://ns.dataone.org/service/types/v1">', - ' <serialversion />', - ' <identifier />', - ' <formatid />', - ' <size />', - ' <checksum />', - ' <submitter />', - ' <rightsholder />', - ' <filename />', - '</d1_v2.0:systemmetadata>' - ); - - sysmetaDOM = $($.parseHTML(sysmetaXML.join(""))); - return sysmetaDOM; - }, - - /** - * Create an access policy for this DataONEObject using the default access - * policy set in the AppModel. - * - * @param {Element} [accessPolicyXML] - An <accessPolicy> XML node - * that contains a list of access rules. - * @return {AccessPolicy} - an AccessPolicy collection that represents the - * given XML or the default policy set in the AppModel. - */ - createAccessPolicy: function(accessPolicyXML){ - //Create a new AccessPolicy collection - var accessPolicy = new AccessPolicy(); - - accessPolicy.dataONEObject = this; - - //If there is no access policy XML sent, - if( this.isNew() && !accessPolicyXML ){ - - try{ - //If the app is configured to inherit the access policy from the parent metadata, - // then get the parent metadata and copy it's AccessPolicy - let scienceMetadata = this.get("isDocumentedByModels"); - if( MetacatUI.appModel.get("inheritAccessPolicy") && scienceMetadata && scienceMetadata.length ){ - let sciMetaAccessPolicy = scienceMetadata[0].get("accessPolicy"); - - if( sciMetaAccessPolicy ){ - accessPolicy.copyAccessPolicy(sciMetaAccessPolicy); - } - else{ - accessPolicy.createDefaultPolicy(); - } + if (typeof documentedModel !== "undefined") { + // Find the oldPid in the array + if (Array.isArray(documentedModel.get("isDocumentedBy"))) { + index = documentedModel.get("isDocumentedBy").indexOf("oldPid"); + + if (index > -1) { + // Remove it + documentedModel.get("isDocumentedBy").splice(index, 1); } - //Otherwise, set the default access policy using the AppModel configuration - else{ - accessPolicy.createDefaultPolicy(); - } - } - catch(e){ - console.error("Could create access policy, so defaulting to default", e); - accessPolicy.createDefaultPolicy(); + // And add the new pid in + documentedModel.get("isDocumentedBy").push(this.get("id")); } } - else{ - //Parse the access policy XML to create AccessRule models from the XML - accessPolicy.parse(accessPolicyXML); - } - - //Listen to changes on the collection and trigger a change on this model - var self = this; - this.listenTo(accessPolicy, "change update", function(){ - self.trigger("change"); - this.addToUploadQueue(); - - }); - - return accessPolicy; }, + this, + ); - /** - * Update identifiers for this object - * - * @param {string} id - Optional identifier to update with. Generated - * automatically when not given. - * - * Note that this method caches the objects attributes prior to - * updating so this.resetID() can be called in case of a failure - * state. - * - * Also note that this method won't run if theh oldPid attribute is - * set. This enables knowing before this.save is called what the next - * PID will be such as the case where we want to update a matching - * EML entity when replacing files. - */ - updateID: function(id){ - // Only run once until oldPid is reset - if (this.get("oldPid")) { - return; - } + this.trigger("change:id"); - //Save the attributes so we can reset the ID later - this.attributeCache = this.toJSON(); + //Update the obsoletes and obsoletedBy + this.set("obsoletes", oldPid); + this.set("obsoletedBy", null); - //Set the old identifier - var oldPid = this.get("id"), - selfDocuments, - selfDocumentedBy, - documentedModels, - documentedModel, - index; + // Update the latest version of this object + this.set("latestVersion", this.get("id")); - //Save the current id as the old pid - this.set("oldPid", oldPid); + //Set the archived option to false + this.set("archived", false); + }, - //Create a new seriesId, if there isn't one, and if this model specifies that one is required - if( !this.get("seriesId") && this.get("createSeriesId") ){ - this.set("seriesId", "urn:uuid:" + uuid.v4()); - } - - // Check to see if the old pid documents or is documented by itself - selfDocuments = _.contains(this.get("documents"), oldPid); - selfDocumentedBy = _.contains(this.get("isDocumentedBy"), oldPid); - - //Set the new identifier - if( id ) { - this.set("id", id); - - } else { - if( this.get("type") == "DataPackage" ){ - this.set("id", "resource_map_urn:uuid:" + uuid.v4()); - } - else{ - this.set("id", "urn:uuid:" + uuid.v4()); - } - } - - // Remove the old pid from the documents list if present - if ( selfDocuments ) { - index = this.get("documents").indexOf(oldPid); - if ( index > -1 ) { - this.get("documents").splice(index, 1); - - } - // And add the new pid in - this.get("documents").push(this.get("id")); - - } - - // Remove the old pid from the isDocumentedBy list if present - if ( selfDocumentedBy ) { - index = this.get("isDocumentedBy").indexOf(oldPid); - if ( index > -1 ) { - this.get("isDocumentedBy").splice(index, 1); - - } - // And add the new pid in - this.get("isDocumentedBy").push(this.get("id")); - - } - - // Update all models documented by this pid with the new id - _.each(this.get("documents"), function(id) { - documentedModels = MetacatUI.rootDataPackage.where({id: id}), - documentedModel; - - if ( documentedModels.length > 0 ) { - documentedModel = documentedModels[0]; - } - if ( typeof documentedModel !== "undefined" ) { - // Find the oldPid in the array - if( Array.isArray(documentedModel.get("isDocumentedBy")) ){ - index = documentedModel.get("isDocumentedBy").indexOf("oldPid"); - - if ( index > -1 ) { - // Remove it - documentedModel.get("isDocumentedBy").splice(index, 1); - - } - // And add the new pid in - documentedModel.get("isDocumentedBy").push(this.get("id")); - } - } - }, this); - - this.trigger("change:id") - - //Update the obsoletes and obsoletedBy - this.set("obsoletes", oldPid); - this.set("obsoletedBy", null); - - // Update the latest version of this object - this.set("latestVersion", this.get("id")); - - //Set the archived option to false - this.set("archived", false); - }, - - /** - * Resets the identifier for this model. This undos all of the changes made in {DataONEObject#updateID} - */ - resetID: function(){ - if(!this.attributeCache) return false; - - this.set("oldPid", this.attributeCache.oldPid, {silent:true}); - this.set("id", this.attributeCache.id, {silent: true}); - this.set("obsoletes", this.attributeCache.obsoletes, {silent: true}); - this.set("obsoletedBy", this.attributeCache.obsoletedBy, {silent: true}); - this.set("archived", this.attributeCache.archived, {silent: true}); - this.set("latestVersion", this.attributeCache.latestVersion, {silent: true}); - - //Reset the attribute cache - this.attributeCache = {}; - }, + /** + * Resets the identifier for this model. This undos all of the changes made in {DataONEObject#updateID} + */ + resetID: function () { + if (!this.attributeCache) return false; + + this.set("oldPid", this.attributeCache.oldPid, { silent: true }); + this.set("id", this.attributeCache.id, { silent: true }); + this.set("obsoletes", this.attributeCache.obsoletes, { silent: true }); + this.set("obsoletedBy", this.attributeCache.obsoletedBy, { + silent: true, + }); + this.set("archived", this.attributeCache.archived, { silent: true }); + this.set("latestVersion", this.attributeCache.latestVersion, { + silent: true, + }); + + //Reset the attribute cache + this.attributeCache = {}; + }, - /** - * Checks if this system metadata XML has updates that need to be synced with the server. - * @returns {boolean} - */ - hasUpdates: function(){ - if(this.isNew()) return true; + /** + * Checks if this system metadata XML has updates that need to be synced with the server. + * @returns {boolean} + */ + hasUpdates: function () { + if (this.isNew()) return true; - // Compare the new system metadata XML to the old system metadata XML + // Compare the new system metadata XML to the old system metadata XML - //Check if there is system metadata first - if( !this.get("sysMetaXML") ){ - return false; - } + //Check if there is system metadata first + if (!this.get("sysMetaXML")) { + return false; + } - var D1ObjectClone = this.clone(), - // Make sure we are using the parse function in the DataONEObject model. - // Sometimes hasUpdates is called from extensions of the D1Object model, - // (e.g. from the portal model), and the parse function is overwritten - oldSysMetaAttrs = new DataONEObject().parse(D1ObjectClone.get("sysMetaXML")); + var D1ObjectClone = this.clone(), + // Make sure we are using the parse function in the DataONEObject model. + // Sometimes hasUpdates is called from extensions of the D1Object model, + // (e.g. from the portal model), and the parse function is overwritten + oldSysMetaAttrs = new DataONEObject().parse( + D1ObjectClone.get("sysMetaXML"), + ); - D1ObjectClone.set(oldSysMetaAttrs); + D1ObjectClone.set(oldSysMetaAttrs); - var oldSysMeta = D1ObjectClone.serializeSysMeta(); - var newSysMeta = this.serializeSysMeta(); + var oldSysMeta = D1ObjectClone.serializeSysMeta(); + var newSysMeta = this.serializeSysMeta(); - if ( oldSysMeta === "" ) return false; + if (oldSysMeta === "") return false; - return !(newSysMeta == oldSysMeta); - }, + return !(newSysMeta == oldSysMeta); + }, - /** + /** Set the changed flag on any system metadata or content attribute changes, and set the hasContentChanges flag on content changes only @param {DataONEObject} [model] @param {object} options Furhter options for this function @property {boolean} options.force If true, a change will be handled regardless if the attribute actually changed */ - handleChange: function(model, options) { - if(!model) var model = this; - - var sysMetaAttrs = ["serialVersion", "identifier", "formatId", "formatType", "size", "checksum", - "checksumAlgorithm", "submitter", "rightsHolder", "accessPolicy", "replicationAllowed", - "replicationPolicy", "obsoletes", "obsoletedBy", "archived", "dateUploaded", "dateSysMetadataModified", - "originMemberNode", "authoritativeMemberNode", "replica", "seriesId", "mediaType", "fileName"], - nonSysMetaNonContentAttrs = _.difference(model.get("originalAttrs"), sysMetaAttrs), - allChangedAttrs = Object.keys(model.changedAttributes()), - changedSysMetaOrContentAttrs = [], //sysmeta or content attributes that have changed - changedContentAttrs = []; // attributes from sub classes like ScienceMetadata or EML211 ... - - // Get a list of all changed sysmeta and content attributes - changedSysMetaOrContentAttrs = _.difference(allChangedAttrs, nonSysMetaNonContentAttrs); - if ( changedSysMetaOrContentAttrs.length > 0 ) { - // For any sysmeta or content change, set the package dirty flag - if ( MetacatUI.rootDataPackage && - MetacatUI.rootDataPackage.packageModel && - ! MetacatUI.rootDataPackage.packageModel.get("changed") && - model.get("synced") ) { - - MetacatUI.rootDataPackage.packageModel.set("changed", true); - } - } - - // And get a list of all changed content attributes - changedContentAttrs = _.difference(changedSysMetaOrContentAttrs, sysMetaAttrs); - - if ( (changedContentAttrs.length > 0 && !this.get("hasContentChanges") && model.get("synced")) || - (options && options.force)) { - this.set("hasContentChanges", true); - this.addToUploadQueue(); - } - - }, - - /** - * Returns true if this DataONE object is new. A DataONE object is new - * if there is no upload date and it's been synced (i.e. been fetched) - * @return {boolean} - */ - isNew: function(){ - - //If the model is explicitly marked as not new, return false - if( this.get("isNew") === false ){ - return false; - } - //If the model is explicitly marked as new, return true - else if( this.get("isNew") === true ){ - return true; - } - - //Check if there is an upload date that was retrieved from the server - return ( this.get("dateUploaded") === this.defaults().dateUploaded && - this.get("synced") ); - }, - - /** - * Updates the upload status attribute on this model and marks the collection as changed - */ - addToUploadQueue: function(){ - - if( !this.get("synced") ){ - return; - } - - //Add this item to the queue - if((this.get("uploadStatus") == "c") || (this.get("uploadStatus") == "e") || !this.get("uploadStatus")){ - this.set("uploadStatus", "q"); - - //Mark each DataPackage collection this model is in as changed - _.each(this.get("collections"), function(collection){ - if(collection.packageModel) - collection.packageModel.set("changed", true); - }, this); - } - }, + handleChange: function (model, options) { + if (!model) var model = this; + + var sysMetaAttrs = [ + "serialVersion", + "identifier", + "formatId", + "formatType", + "size", + "checksum", + "checksumAlgorithm", + "submitter", + "rightsHolder", + "accessPolicy", + "replicationAllowed", + "replicationPolicy", + "obsoletes", + "obsoletedBy", + "archived", + "dateUploaded", + "dateSysMetadataModified", + "originMemberNode", + "authoritativeMemberNode", + "replica", + "seriesId", + "mediaType", + "fileName", + ], + nonSysMetaNonContentAttrs = _.difference( + model.get("originalAttrs"), + sysMetaAttrs, + ), + allChangedAttrs = Object.keys(model.changedAttributes()), + changedSysMetaOrContentAttrs = [], //sysmeta or content attributes that have changed + changedContentAttrs = []; // attributes from sub classes like ScienceMetadata or EML211 ... + + // Get a list of all changed sysmeta and content attributes + changedSysMetaOrContentAttrs = _.difference( + allChangedAttrs, + nonSysMetaNonContentAttrs, + ); + if (changedSysMetaOrContentAttrs.length > 0) { + // For any sysmeta or content change, set the package dirty flag + if ( + MetacatUI.rootDataPackage && + MetacatUI.rootDataPackage.packageModel && + !MetacatUI.rootDataPackage.packageModel.get("changed") && + model.get("synced") + ) { + MetacatUI.rootDataPackage.packageModel.set("changed", true); + } + } - /** - * Updates the progress percentage when the model is getting uploaded - * @param {ProgressEvent} e - The ProgressEvent when this file is being uploaded - */ - updateProgress: function(e){ - if(e.lengthComputable){ - var max = e.total; - var current = e.loaded; + // And get a list of all changed content attributes + changedContentAttrs = _.difference( + changedSysMetaOrContentAttrs, + sysMetaAttrs, + ); + + if ( + (changedContentAttrs.length > 0 && + !this.get("hasContentChanges") && + model.get("synced")) || + (options && options.force) + ) { + this.set("hasContentChanges", true); + this.addToUploadQueue(); + } + }, - var Percentage = (current * 100)/max; + /** + * Returns true if this DataONE object is new. A DataONE object is new + * if there is no upload date and it's been synced (i.e. been fetched) + * @return {boolean} + */ + isNew: function () { + //If the model is explicitly marked as not new, return false + if (this.get("isNew") === false) { + return false; + } + //If the model is explicitly marked as new, return true + else if (this.get("isNew") === true) { + return true; + } + //Check if there is an upload date that was retrieved from the server + return ( + this.get("dateUploaded") === this.defaults().dateUploaded && + this.get("synced") + ); + }, - if(Percentage >= 100) - { - // process completed - } - } - }, + /** + * Updates the upload status attribute on this model and marks the collection as changed + */ + addToUploadQueue: function () { + if (!this.get("synced")) { + return; + } - /** - * Updates the relationships with other models when this model has been updated - */ - updateRelationships: function(){ - _.each(this.get("collections"), function(collection){ - //Get the old id for this model - var oldId = this.get("oldPid"); + //Add this item to the queue + if ( + this.get("uploadStatus") == "c" || + this.get("uploadStatus") == "e" || + !this.get("uploadStatus") + ) { + this.set("uploadStatus", "q"); + + //Mark each DataPackage collection this model is in as changed + _.each( + this.get("collections"), + function (collection) { + if (collection.packageModel) + collection.packageModel.set("changed", true); + }, + this, + ); + } + }, - if(!oldId) return; + /** + * Updates the progress percentage when the model is getting uploaded + * @param {ProgressEvent} e - The ProgressEvent when this file is being uploaded + */ + updateProgress: function (e) { + if (e.lengthComputable) { + var max = e.total; + var current = e.loaded; + + var Percentage = (current * 100) / max; + + if (Percentage >= 100) { + // process completed + } + } + }, - //Find references to the old id in the documents relationship - var outdatedModels = collection.filter(function(m){ - return _.contains(m.get("documents"), oldId); - }); + /** + * Updates the relationships with other models when this model has been updated + */ + updateRelationships: function () { + _.each( + this.get("collections"), + function (collection) { + //Get the old id for this model + var oldId = this.get("oldPid"); + + if (!oldId) return; + + //Find references to the old id in the documents relationship + var outdatedModels = collection.filter(function (m) { + return _.contains(m.get("documents"), oldId); + }); - //Update the documents array in each model - _.each(outdatedModels, function(model){ + //Update the documents array in each model + _.each( + outdatedModels, + function (model) { var updatedDocuments = _.without(model.get("documents"), oldId); updatedDocuments.push(this.get("id")); model.set("documents", updatedDocuments); - }, this); - - }, this); + }, + this, + ); }, + this, + ); + }, - /** + /** * Finds the latest version of this object by travesing the obsolescence chain * @param {string} [latestVersion] - The identifier of the latest known object in the version chain. If not supplied, this model's `id` will be used. * @param {string} [possiblyNewer] - The identifier of the object that obsoletes the latestVersion. It's "possibly" newer, because it may be private/inaccessible */ - findLatestVersion: function(latestVersion, possiblyNewer){ - var baseUrl = "", - activeAltRepo = MetacatUI.appModel.getActiveAltRepo(); - //Use the meta service URL from the alt repo - if( activeAltRepo ){ - baseUrl = activeAltRepo.metaServiceUrl; - } - //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel - else{ - baseUrl = MetacatUI.appModel.get("metaServiceUrl"); - } - - if( !baseUrl ){ - return; - } - - //If there is no system metadata, then retrieve it first - if(!this.get("sysMetaXML")){ - this.once("sync", this.findLatestVersion); - this.once("systemMetadataSync", this.findLatestVersion); - this.fetch({ - url: baseUrl + encodeURIComponent(this.get("id")), - dataType: "text", - systemMetadataOnly: true - }); - return; - } + findLatestVersion: function (latestVersion, possiblyNewer) { + var baseUrl = "", + activeAltRepo = MetacatUI.appModel.getActiveAltRepo(); + //Use the meta service URL from the alt repo + if (activeAltRepo) { + baseUrl = activeAltRepo.metaServiceUrl; + } + //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel + else { + baseUrl = MetacatUI.appModel.get("metaServiceUrl"); + } - //If no pid was supplied, use this model's id - if(!latestVersion || typeof latestVersion != "string"){ - var latestVersion = this.get("id"); - var possiblyNewer = this.get("obsoletedBy"); - } + if (!baseUrl) { + return; + } - //If this isn't obsoleted by anything, then there is no newer version - if(!possiblyNewer || typeof latestVersion != "string"){ - this.set("latestVersion", latestVersion); + //If there is no system metadata, then retrieve it first + if (!this.get("sysMetaXML")) { + this.once("sync", this.findLatestVersion); + this.once("systemMetadataSync", this.findLatestVersion); + this.fetch({ + url: baseUrl + encodeURIComponent(this.get("id")), + dataType: "text", + systemMetadataOnly: true, + }); + return; + } - //Trigger an event that will fire whether or not the latestVersion - // attribute was actually changed - this.trigger("latestVersionFound", this); + //If no pid was supplied, use this model's id + if (!latestVersion || typeof latestVersion != "string") { + var latestVersion = this.get("id"); + var possiblyNewer = this.get("obsoletedBy"); + } - //Remove the listeners now that we found the latest version - this.stopListening("sync", this.findLatestVersion); - this.stopListening("systemMetadataSync", this.findLatestVersion); + //If this isn't obsoleted by anything, then there is no newer version + if (!possiblyNewer || typeof latestVersion != "string") { + this.set("latestVersion", latestVersion); - return; - } + //Trigger an event that will fire whether or not the latestVersion + // attribute was actually changed + this.trigger("latestVersionFound", this); - var model = this; + //Remove the listeners now that we found the latest version + this.stopListening("sync", this.findLatestVersion); + this.stopListening("systemMetadataSync", this.findLatestVersion); - //Get the system metadata for the possibly newer version - var requestSettings = { - url: baseUrl + encodeURIComponent(possiblyNewer), - type: "GET", - success: function(data) { + return; + } - // the response may have an obsoletedBy element - var obsoletedBy = $(data).find("obsoletedBy").text(); + var model = this; - //If there is an even newer version, then get it and rerun this function - if(obsoletedBy){ - model.findLatestVersion(possiblyNewer, obsoletedBy); - } - //If there isn't a newer version, then this is it - else{ - model.set("latestVersion", possiblyNewer); - model.trigger("latestVersionFound", model); - - //Remove the listeners now that we found the latest version - model.stopListening("sync", model.findLatestVersion); - model.stopListening("systemMetadataSync", model.findLatestVersion); - } + //Get the system metadata for the possibly newer version + var requestSettings = { + url: baseUrl + encodeURIComponent(possiblyNewer), + type: "GET", + success: function (data) { + // the response may have an obsoletedBy element + var obsoletedBy = $(data).find("obsoletedBy").text(); - }, - error: function(xhr){ - //If this newer version isn't accessible, link to the latest version that is - if(xhr.status == "401"){ - model.set("latestVersion", latestVersion); - model.trigger("latestVersionFound", model); - } - } + //If there is an even newer version, then get it and rerun this function + if (obsoletedBy) { + model.findLatestVersion(possiblyNewer, obsoletedBy); } + //If there isn't a newer version, then this is it + else { + model.set("latestVersion", possiblyNewer); + model.trigger("latestVersionFound", model); - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); + //Remove the listeners now that we found the latest version + model.stopListening("sync", model.findLatestVersion); + model.stopListening( + "systemMetadataSync", + model.findLatestVersion, + ); + } }, - - /** - * A utility function that will format an XML string or XML nodes by camel-casing the node names, as necessary - * @param {string|Element} xml - The XML to format - * @returns {string} The formatted XML string - */ - formatXML: function(xml){ - var nodeNameMap = this.nodeNameMap(), - xmlString = ""; - - //XML must be provided for this function - if(!xml) - return ""; - //Support XML strings - else if(typeof xml == "string") - xmlString = xml; - //Support DOMs - else if(typeof xml == "object" && xml.nodeType){ - //XML comments should be formatted with start and end carets - if(xml.nodeType == 8) - xmlString = "<" + xml.nodeValue + ">"; - //XML nodes have the entire XML string available in the outerHTML attribute - else if(xml.nodeType == 1) - xmlString = xml.outerHTML; - //Text node types are left as-is - else if(xml.nodeType == 3) - return xml.nodeValue; + error: function (xhr) { + //If this newer version isn't accessible, link to the latest version that is + if (xhr.status == "401") { + model.set("latestVersion", latestVersion); + model.trigger("latestVersionFound", model); } - - //Return empty strings if something went wrong - if(!xmlString) - return ""; - - _.each(Object.keys(nodeNameMap), function(name, i){ - var originalXMLString = xmlString; - - //Check for this node name whe it's an opening XML node, e.g. `<name>` - var regEx = new RegExp("<" + name + ">", "g"); - xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + ">"); - - //Check for this node name when it's an opening XML node, e.g. `<name ` - regEx = new RegExp("<" + name + " ", "g"); - xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + " "); - - //Check for this node name when it's preceeded by a namespace, e.g. `:name ` - regEx = new RegExp(":" + name + " ", "g"); - xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + " "); - - //Check for this node name when it's a closing tag preceeded by a namespace, e.g. `:name>` - regEx = new RegExp(":" + name + ">", "g"); - xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + ">"); - - //Check for this node name when it's a closing XML tag, e.g. `</name>` - regEx = new RegExp("</" + name + ">", "g"); - xmlString = xmlString.replace(regEx, "</" + nodeNameMap[name] + ">"); - - //If node names haven't been changed, then find an attribute, e.g. ` name=` - if(xmlString == originalXMLString){ - regEx = new RegExp(" " + name + "=", "g"); - xmlString = xmlString.replace(regEx, " " + nodeNameMap[name] + "="); - } - - }, this); - - //Take each XML node text value and decode any XML entities - var regEx = new RegExp("\&[0-9a-zA-Z]+\;", "g"); - xmlString = xmlString.replace(regEx, function(match){ return he.encode(he.decode(match)); }); - - return xmlString; }, + }; - /** - * Converts the number of bytes into a human readable format and - * updates the `sizeStr` attribute - * @returns: None - * - */ - bytesToSize: function(){ - var kibibyte = 1024; - var mebibyte = kibibyte * 1024; - var gibibyte = mebibyte * 1024; - var tebibyte = gibibyte * 1024; - var precision = 0; - - var bytes = this.get("size"); - - if ((bytes >= 0) && (bytes < kibibyte)) { - this.set("sizeStr", bytes + ' B'); - - } else if ((bytes >= kibibyte) && (bytes < mebibyte)) { - this.set("sizeStr", (bytes / kibibyte).toFixed(precision) + ' KiB'); - - } else if ((bytes >= mebibyte) && (bytes < gibibyte)) { - precision = 2; - this.set("sizeStr", (bytes / mebibyte).toFixed(precision) + ' MiB'); - - } else if ((bytes >= gibibyte) && (bytes < tebibyte)) { - precision = 2; - this.set("sizeStr", (bytes / gibibyte).toFixed(precision) + ' GiB'); - - } else if (bytes >= tebibyte) { - precision = 2; - this.set("sizeStr", (bytes / tebibyte).toFixed(precision) + ' TiB'); + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + }, - } else { - this.set("sizeStr", bytes + ' B'); + /** + * A utility function that will format an XML string or XML nodes by camel-casing the node names, as necessary + * @param {string|Element} xml - The XML to format + * @returns {string} The formatted XML string + */ + formatXML: function (xml) { + var nodeNameMap = this.nodeNameMap(), + xmlString = ""; + + //XML must be provided for this function + if (!xml) return ""; + //Support XML strings + else if (typeof xml == "string") xmlString = xml; + //Support DOMs + else if (typeof xml == "object" && xml.nodeType) { + //XML comments should be formatted with start and end carets + if (xml.nodeType == 8) xmlString = "<" + xml.nodeValue + ">"; + //XML nodes have the entire XML string available in the outerHTML attribute + else if (xml.nodeType == 1) xmlString = xml.outerHTML; + //Text node types are left as-is + else if (xml.nodeType == 3) return xml.nodeValue; + } - } + //Return empty strings if something went wrong + if (!xmlString) return ""; + + _.each( + Object.keys(nodeNameMap), + function (name, i) { + var originalXMLString = xmlString; + + //Check for this node name whe it's an opening XML node, e.g. `<name>` + var regEx = new RegExp("<" + name + ">", "g"); + xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + ">"); + + //Check for this node name when it's an opening XML node, e.g. `<name ` + regEx = new RegExp("<" + name + " ", "g"); + xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + " "); + + //Check for this node name when it's preceeded by a namespace, e.g. `:name ` + regEx = new RegExp(":" + name + " ", "g"); + xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + " "); + + //Check for this node name when it's a closing tag preceeded by a namespace, e.g. `:name>` + regEx = new RegExp(":" + name + ">", "g"); + xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + ">"); + + //Check for this node name when it's a closing XML tag, e.g. `</name>` + regEx = new RegExp("</" + name + ">", "g"); + xmlString = xmlString.replace( + regEx, + "</" + nodeNameMap[name] + ">", + ); + + //If node names haven't been changed, then find an attribute, e.g. ` name=` + if (xmlString == originalXMLString) { + regEx = new RegExp(" " + name + "=", "g"); + xmlString = xmlString.replace( + regEx, + " " + nodeNameMap[name] + "=", + ); + } }, + this, + ); - /** - * This method will download this object while - * sending the user's auth token in the request. - * @returns None - * @since: 2.28.0 - */ - downloadWithCredentials: function(){ - //if(this.get("isPublic")) return; - - //Get info about this object - var url = this.get("url"), - model = this; - - //Create an XHR - var xhr = new XMLHttpRequest(); + //Take each XML node text value and decode any XML entities + var regEx = new RegExp("&[0-9a-zA-Z]+;", "g"); + xmlString = xmlString.replace(regEx, function (match) { + return he.encode(he.decode(match)); + }); - //Open and send the request with the user's auth token - xhr.open('GET', url); + return xmlString; + }, - if(MetacatUI.appUserModel.get("loggedIn")) - xhr.withCredentials = true; + /** + * Converts the number of bytes into a human readable format and + * updates the `sizeStr` attribute + * @returns: None + * + */ + bytesToSize: function () { + var kibibyte = 1024; + var mebibyte = kibibyte * 1024; + var gibibyte = mebibyte * 1024; + var tebibyte = gibibyte * 1024; + var precision = 0; + + var bytes = this.get("size"); + + if (bytes >= 0 && bytes < kibibyte) { + this.set("sizeStr", bytes + " B"); + } else if (bytes >= kibibyte && bytes < mebibyte) { + this.set("sizeStr", (bytes / kibibyte).toFixed(precision) + " KiB"); + } else if (bytes >= mebibyte && bytes < gibibyte) { + precision = 2; + this.set("sizeStr", (bytes / mebibyte).toFixed(precision) + " MiB"); + } else if (bytes >= gibibyte && bytes < tebibyte) { + precision = 2; + this.set("sizeStr", (bytes / gibibyte).toFixed(precision) + " GiB"); + } else if (bytes >= tebibyte) { + precision = 2; + this.set("sizeStr", (bytes / tebibyte).toFixed(precision) + " TiB"); + } else { + this.set("sizeStr", bytes + " B"); + } + }, - //When the XHR is ready, create a link with the raw data (Blob) and click the link to download - xhr.onload = function(){ + /** + * This method will download this object while + * sending the user's auth token in the request. + * @returns None + * @since: 2.28.0 + */ + downloadWithCredentials: function () { + //if(this.get("isPublic")) return; + + //Get info about this object + var url = this.get("url"), + model = this; + + //Create an XHR + var xhr = new XMLHttpRequest(); + + //Open and send the request with the user's auth token + xhr.open("GET", url); + + if (MetacatUI.appUserModel.get("loggedIn")) xhr.withCredentials = true; + + //When the XHR is ready, create a link with the raw data (Blob) and click the link to download + xhr.onload = function () { + if (this.status == 404) { + this.onerror.call(this); + return; + } - if( this.status == 404 ){ - this.onerror.call(this); - return; - } + //Get the file name to save this file as + var filename = xhr.getResponseHeader("Content-Disposition"); + + if (!filename) { + filename = + model.get("fileName") || + model.get("title") || + model.get("id") || + "download"; + } else + filename = filename + .substring(filename.indexOf("filename=") + 9) + .replace(/"/g, ""); + + //Replace any whitespaces + filename = filename.trim().replace(/ /g, "_"); + + //For IE, we need to use the navigator API + if (navigator && navigator.msSaveOrOpenBlob) { + navigator.msSaveOrOpenBlob(xhr.response, filename); + } + //Other browsers can download it via a link + else { + var a = document.createElement("a"); + a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob + + // Set the file name. + a.download = filename; + + a.style.display = "none"; + document.body.appendChild(a); + a.click(); + a.remove(); + } - //Get the file name to save this file as - var filename = xhr.getResponseHeader('Content-Disposition'); + model.trigger("downloadComplete"); + + // Track this event + MetacatUI.analytics?.trackEvent( + "download", + "Download DataONEObject", + model.get("id"), + ); + }; + + xhr.onerror = function (e) { + model.trigger("downloadError"); + + // Track the error + MetacatUI.analytics?.trackException( + `Download DataONEObject error: ${e || ""}`, + model.get("id"), + true, + ); + }; + + xhr.onprogress = function (e) { + if (e.lengthComputable) { + var percent = (e.loaded / e.total) * 100; + model.set("downloadPercent", percent); + } + }; - if(!filename){ - filename = model.get("fileName") || model.get("title") || model.get("id") || "download"; - } - else - filename = filename.substring(filename.indexOf("filename=")+9).replace(/"/g, ""); + xhr.responseType = "blob"; - //Replace any whitespaces - filename = filename.trim().replace(/ /g, "_"); + if (MetacatUI.appUserModel.get("loggedIn")) + xhr.setRequestHeader( + "Authorization", + "Bearer " + MetacatUI.appUserModel.get("token"), + ); - //For IE, we need to use the navigator API - if (navigator && navigator.msSaveOrOpenBlob) { - navigator.msSaveOrOpenBlob(xhr.response, filename); - } - //Other browsers can download it via a link - else{ - var a = document.createElement('a'); - a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob - - // Set the file name. - a.download = filename - - a.style.display = 'none'; - document.body.appendChild(a); - a.click(); - a.remove(); - } + xhr.send(); + }, - model.trigger("downloadComplete"); + /** + * Creates a file name for this DataONEObject and updates the `fileName` attribute + */ + setMissingFileName: function () { + var objectFormats, filename, extension; + + objectFormats = MetacatUI.objectFormats.where({ + formatId: this.get("formatId"), + }); + if (objectFormats.length > 0) { + extension = objectFormats[0].get("extension"); + } - // Track this event - MetacatUI.analytics?.trackEvent( - "download", - "Download DataONEObject", - model.get("id") - ); - }; + //Science metadata file names will use the title + if (this.get("type") == "Metadata") { + filename = + Array.isArray(this.get("title")) && this.get("title").length + ? this.get("title")[0] + : this.get("id"); + } + //Resource maps will use a "resource_map_" prefix + else if (this.get("type") == "DataPackage") { + filename = "resource_map_" + this.get("id"); + extension = ".rdf.xml"; + } + //All other object types will just use the id + else { + filename = this.get("id"); + } - xhr.onerror = function(e){ - model.trigger("downloadError"); + //Replace all non-alphanumeric characters with underscores + filename = filename.replace(/[^a-zA-Z0-9]/g, "_"); - // Track the error - MetacatUI.analytics?.trackException( - `Download DataONEObject error: ${e || ""}`, model.get("id"), true - ); - }; + if (typeof extension !== "undefined") { + filename = filename + "." + extension; + } - xhr.onprogress = function(e){ - if (e.lengthComputable){ - var percent = (e.loaded / e.total) * 100; - model.set("downloadPercent", percent); - } - }; + this.set("fileName", filename); + }, - xhr.responseType = "blob"; + /** + * Creates a URL for viewing more information about this object + * @return {string} + */ + createViewURL: function () { + return ( + MetacatUI.root + + "/view/" + + encodeURIComponent(this.get("seriesId") || this.get("id")) + ); + }, - if(MetacatUI.appUserModel.get("loggedIn")) - xhr.setRequestHeader("Authorization", "Bearer " + MetacatUI.appUserModel.get("token")); + /** + * Check if the seriesID or PID matches a DOI regex, and if so, return + * a canonical IRI for the DOI. + * @return {string|null} - The canonical IRI for the DOI, or null if + * neither the seriesId nor the PID match a DOI regex. + * @since 2.26.0 + */ + getCanonicalDOIIRI: function () { + const id = this.get("id"); + const seriesId = this.get("seriesId"); + let DOI = null; + if (this.isDOI(seriesId)) DOI = seriesId; + else if (this.isDOI(id)) DOI = id; + return MetacatUI.appModel.DOItoURL(DOI); + }, - xhr.send(); - }, + /** + * Converts the identifier string to a string safe to use in an XML id attribute + * @param {string} [id] - The ID string + * @return {string} - The XML-safe string + */ + getXMLSafeID: function (id) { + if (typeof id == "undefined") { + var id = this.get("id"); + } - /** - * Creates a file name for this DataONEObject and updates the `fileName` attribute - */ - setMissingFileName: function() { - var objectFormats, filename, extension; + //Replace XML id attribute invalid characters and patterns in the identifier + id = id + .replace(/</g, "-") + .replace(/:/g, "-") + .replace(/&[a-zA-Z0-9]+;/g); - objectFormats = MetacatUI.objectFormats.where({formatId: this.get("formatId")}); - if ( objectFormats.length > 0 ) { - extension = objectFormats[0].get("extension"); - } + return id; + }, - //Science metadata file names will use the title - if( this.get("type") == "Metadata" ){ - filename = (Array.isArray(this.get("title")) && this.get("title").length)? this.get("title")[0] : this.get("id"); - } - //Resource maps will use a "resource_map_" prefix - else if( this.get("type") == "DataPackage" ){ - filename = "resource_map_" + this.get("id"); - extension = ".rdf.xml"; - } - //All other object types will just use the id - else{ - filename = this.get("id"); - } + /**** Provenance-related functions ****/ + /** + * Returns true if this provenance field points to a source of this data or metadata object + * @param {string} field + * @returns {boolean} + */ + isSourceField: function (field) { + if (typeof field == "undefined" || !field) return false; + // Is the field we are checking a prov field? + if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field)) + return false; + + if ( + field == "prov_generatedByExecution" || + field == "prov_generatedByProgram" || + field == "prov_used" || + field == "prov_wasDerivedFrom" || + field == "prov_wasInformedBy" + ) + return true; + else return false; + }, - //Replace all non-alphanumeric characters with underscores - filename = filename.replace(/[^a-zA-Z0-9]/g, "_"); + /** + * Returns true if this provenance field points to a derivation of this data or metadata object + * @param {string} field + * @returns {boolean} + */ + isDerivationField: function (field) { + if (typeof field == "undefined" || !field) return false; + if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field)) + return false; + + if ( + field == "prov_usedByExecution" || + field == "prov_usedByProgram" || + field == "prov_hasDerivations" || + field == "prov_generated" + ) + return true; + else return false; + }, - if ( typeof extension !== "undefined" ) { - filename = filename + "." + extension; - } + /** + * Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data + */ + getType: function () { + //The list of formatIds that are images + + //The list of formatIds that are images + var pdfIds = ["application/pdf"]; + var annotationIds = [ + "http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html", + ]; + + // Type has already been set, use that. + if (this.get("type").toLowerCase() == "metadata") return "metadata"; + + //Determine the type via provONE + var instanceOfClass = this.get("prov_instanceOfClass"); + if ( + typeof instanceOfClass !== "undefined" && + Array.isArray(instanceOfClass) && + instanceOfClass.length + ) { + var programClass = _.filter(instanceOfClass, function (className) { + return className.indexOf("#Program") > -1; + }); + if (typeof programClass !== "undefined" && programClass.length) + return "program"; + } else { + if (this.get("prov_generated").length || this.get("prov_used").length) + return "program"; + } - this.set("fileName", filename); - }, + //Determine the type via file format + if (this.isSoftware()) return "program"; + if (this.isData()) return "data"; - /** - * Creates a URL for viewing more information about this object - * @return {string} - */ - createViewURL: function(){ - return MetacatUI.root + "/view/" + encodeURIComponent((this.get("seriesId") || this.get("id"))); - }, - - /** - * Check if the seriesID or PID matches a DOI regex, and if so, return - * a canonical IRI for the DOI. - * @return {string|null} - The canonical IRI for the DOI, or null if - * neither the seriesId nor the PID match a DOI regex. - * @since 2.26.0 - */ - getCanonicalDOIIRI: function () { - const id = this.get("id"); - const seriesId = this.get("seriesId"); - let DOI = null; - if (this.isDOI(seriesId)) DOI = seriesId; - else if (this.isDOI(id)) DOI = id; - return MetacatUI.appModel.DOItoURL(DOI); - }, + if (this.get("type").toLowerCase() == "metadata") return "metadata"; + if (this.isImage()) return "image"; + if (_.contains(pdfIds, this.get("formatId"))) return "PDF"; + if (_.contains(annotationIds, this.get("formatId"))) + return "annotation"; + else return "data"; + }, - /** - * Converts the identifier string to a string safe to use in an XML id attribute - * @param {string} [id] - The ID string - * @return {string} - The XML-safe string - */ - getXMLSafeID: function(id){ + /** + * Checks the formatId of this model and determines if it is an image. + * @returns {boolean} true if this data object is an image, false if it is other + */ + isImage: function () { + //The list of formatIds that are images + var imageIds = ["image/gif", "image/jp2", "image/jpeg", "image/png"]; + + //Does this data object match one of these IDs? + if (_.indexOf(imageIds, this.get("formatId")) == -1) return false; + else return true; + }, - if(typeof id == "undefined"){ - var id = this.get("id"); - } + /** + * Checks the formatId of this model and determines if it is a data file. + * This determination is mostly used for display and the provenance editor. In the + * DataONE API, many formatIds are considered `DATA` formatTypes, but they are categorized + * as images {@link DataONEObject#isImage} or software {@link DataONEObject#isSoftware}. + * @returns {boolean} true if this data object is a data file, false if it is other + */ + isData: function () { + var dataIds = [ + "application/atom+xml", + "application/mathematica", + "application/msword", + "application/netcdf", + "application/octet-stream", + "application/pdf", + "application/postscript", + "application/rdf+xml", + "application/rtf", + "application/vnd.google-earth.kml+xml", + "application/vnd.ms-excel", + "application/vnd.ms-excel.sheet.binary.macroEnabled.12", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/x-bzip2", + "application/x-fasta", + "application/x-gzip", + "application/x-rar-compressed", + "application/x-tar", + "application/xhtml+xml", + "application/xml", + "application/zip", + "audio/mpeg", + "audio/x-ms-wma", + "audio/x-wav", + "image/svg xml", + "image/svg+xml", + "image/bmp", + "image/tiff", + "text/anvl", + "text/csv", + "text/html", + "text/n3", + "text/plain", + "text/tab-separated-values", + "text/turtle", + "text/xml", + "video/avi", + "video/mp4", + "video/mpeg", + "video/quicktime", + "video/x-ms-wmv", + ]; + + //Does this data object match one of these IDs? + if (_.indexOf(dataIds, this.get("formatId")) == -1) return false; + else return true; + }, - //Replace XML id attribute invalid characters and patterns in the identifier - id = id.replace(/</g, "-").replace(/:/g, "-").replace(/&[a-zA-Z0-9]+;/g); + /** + * Checks the formatId of this model and determines if it is a software file. + * This determination is mostly used for display and the provenance editor. In the + * DataONE API, many formatIds are considered `DATA` formatTypes, but they are categorized + * as images {@link DataONEObject#isImage} for display purposes. + * @returns {boolean} true if this data object is a software file, false if it is other + */ + isSoftware: function () { + //The list of formatIds that are programs + var softwareIds = [ + "text/x-python", + "text/x-rsrc", + "text/x-matlab", + "text/x-sas", + "application/R", + "application/x-ipynb+json", + ]; + //Does this data object match one of these IDs? + if (_.indexOf(softwareIds, this.get("formatId")) == -1) return false; + else return true; + }, - return id; - }, + /** + * Checks the formatId of this model and determines if it a PDF. + * @returns {boolean} true if this data object is a pdf, false if it is other + */ + isPDF: function () { + //The list of formatIds that are images + var ids = ["application/pdf"]; + + //Does this data object match one of these IDs? + if (_.indexOf(ids, this.get("formatId")) == -1) return false; + else return true; + }, - /**** Provenance-related functions ****/ - /** - * Returns true if this provenance field points to a source of this data or metadata object - * @param {string} field - * @returns {boolean} - */ - isSourceField: function(field){ - if((typeof field == "undefined") || !field) return false; - // Is the field we are checking a prov field? - if(!_.contains(MetacatUI.appSearchModel.getProvFields(), field)) return false; - - if(field == "prov_generatedByExecution" || - field == "prov_generatedByProgram" || - field == "prov_used" || - field == "prov_wasDerivedFrom" || - field == "prov_wasInformedBy") - return true; - else - return false; - }, - - /** - * Returns true if this provenance field points to a derivation of this data or metadata object - * @param {string} field - * @returns {boolean} - */ - isDerivationField: function(field){ - if((typeof field == "undefined") || !field) return false; - if(!_.contains(MetacatUI.appSearchModel.getProvFields(), field)) return false; - - if(field == "prov_usedByExecution" || - field == "prov_usedByProgram" || - field == "prov_hasDerivations" || - field == "prov_generated") - return true; - else - return false; - }, + /** + * Set the DataONE ProvONE provenance class + * param className - the shortened form of the actual classname value. The + * shortname will be appened to the ProvONE namespace, for example, + * the className "program" will result in the final class name + * "http://purl.dataone.org/provone/2015/01/15/ontology#Program" + * see https://github.com/DataONEorg/sem-prov-ontologies/blob/master/provenance/ProvONE/v1/provone.html + * @param {string} className + */ + setProvClass: function (className) { + className = className.toLowerCase(); + className = className.charAt(0).toUpperCase() + className.slice(1); + + /* This function is intended to be used for the ProvONE classes that are + * typically represented in DataONEObjects: "Data", "Program", and hopefully + * someday "Execution", as we don't allow the user to set the namespace + * e.g. to "PROV", so therefor we check for the currently known ProvONE classes. + */ + if ( + _.contains( + [ + "Program", + "Data", + "Visualization", + "Document", + "Execution", + "User", + ], + className, + ) + ) { + this.set("prov_instanceOfClass", [this.PROVONE + className]); + } else if ( + _.contains( + ["Entity", "Usage", "Generation", "Association"], + className, + ) + ) { + this.set("prov_instanceOfClass", [this.PROV + className]); + } else { + message = + "The given class name: " + + className + + " is not in the known ProvONE or PROV classes."; + throw new Error(message); + } + }, - /** - * Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data - */ - getType: function(){ - //The list of formatIds that are images - - //The list of formatIds that are images - var pdfIds = ["application/pdf"]; - var annotationIds = ["http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html"]; - - // Type has already been set, use that. - if(this.get("type").toLowerCase() == "metadata") - return "metadata"; - - //Determine the type via provONE - var instanceOfClass = this.get("prov_instanceOfClass"); - if(typeof instanceOfClass !== "undefined" && Array.isArray(instanceOfClass) && instanceOfClass.length){ - var programClass = _.filter(instanceOfClass, function(className){ - return (className.indexOf("#Program") > -1); - }); - if((typeof programClass !== "undefined") && programClass.length) - return "program"; + /** + * Calculate a checksum for the object + * @param {string} [algorithm] The algorithm to use, defaults to MD5 + * @return {string} A checksum plain JS object with value and algorithm attributes + */ + calculateChecksum: function (algorithm) { + var algorithm = algorithm || "MD5"; + var checksum = { algorithm: undefined, value: undefined }; + var hash; // The checksum hash + var file; // The file to be read by slicing + var reader; // The FileReader used to read each slice + var offset = 0; // Byte offset for reading slices + var sliceSize = Math.pow(2, 20); // 1MB slices + var model = this; + + // Do we have a file? + if (this.get("uploadFile") instanceof Blob) { + file = this.get("uploadFile"); + reader = new FileReader(); + /* Handle load errors */ + reader.onerror = function (event) { + console.log("Error reading: " + event); + }; + /* Show progress */ + reader.onprogress = function (event) {}; + /* Handle load finish */ + reader.onloadend = function (event) { + if (event.target.readyState == FileReader.DONE) { + hash.update(event.target.result); } - else{ - if(this.get("prov_generated").length || this.get("prov_used").length) - return "program"; + offset += sliceSize; + if (_seek()) { + model.set("checksum", hash.hex()); + model.set("checksumAlgorithm", checksum.algorithm); + model.trigger("checksumCalculated", model.attributes); } + }; + } else { + message = "The given object is not a blob or a file object."; + throw new Error(message); + } - //Determine the type via file format - if(this.isSoftware()) return "program"; - if(this.isData()) return "data"; - - if(this.get("type").toLowerCase() == "metadata") return "metadata"; - if(this.isImage()) return "image"; - if(_.contains(pdfIds, this.get("formatId"))) return "PDF"; - if(_.contains(annotationIds, this.get("formatId"))) return "annotation"; - - else return "data"; - }, - - /** - * Checks the formatId of this model and determines if it is an image. - * @returns {boolean} true if this data object is an image, false if it is other - */ - isImage: function(){ - //The list of formatIds that are images - var imageIds = ["image/gif", - "image/jp2", - "image/jpeg", - "image/png"]; - - //Does this data object match one of these IDs? - if(_.indexOf(imageIds, this.get('formatId')) == -1) return false; - else return true; - - }, - - /** - * Checks the formatId of this model and determines if it is a data file. - * This determination is mostly used for display and the provenance editor. In the - * DataONE API, many formatIds are considered `DATA` formatTypes, but they are categorized - * as images {@link DataONEObject#isImage} or software {@link DataONEObject#isSoftware}. - * @returns {boolean} true if this data object is a data file, false if it is other - */ - isData: function() { - var dataIds = ["application/atom+xml", - "application/mathematica", - "application/msword", - "application/netcdf", - "application/octet-stream", - "application/pdf", - "application/postscript", - "application/rdf+xml", - "application/rtf", - "application/vnd.google-earth.kml+xml", - "application/vnd.ms-excel", - "application/vnd.ms-excel.sheet.binary.macroEnabled.12", - "application/vnd.ms-powerpoint", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/x-bzip2", - "application/x-fasta", - "application/x-gzip", - "application/x-rar-compressed", - "application/x-tar", - "application/xhtml+xml", - "application/xml", - "application/zip", - "audio/mpeg", - "audio/x-ms-wma", - "audio/x-wav", - "image/svg xml", - "image/svg+xml", - "image/bmp", - "image/tiff", - "text/anvl", - "text/csv", - "text/html", - "text/n3", - "text/plain", - "text/tab-separated-values", - "text/turtle", - "text/xml", - "video/avi", - "video/mp4", - "video/mpeg", - "video/quicktime", - "video/x-ms-wmv"]; - - //Does this data object match one of these IDs? - if(_.indexOf(dataIds, this.get('formatId')) == -1) return false; - else return true; - }, - - /** - * Checks the formatId of this model and determines if it is a software file. - * This determination is mostly used for display and the provenance editor. In the - * DataONE API, many formatIds are considered `DATA` formatTypes, but they are categorized - * as images {@link DataONEObject#isImage} for display purposes. - * @returns {boolean} true if this data object is a software file, false if it is other - */ - isSoftware: function(){ - //The list of formatIds that are programs - var softwareIds = ["text/x-python", - "text/x-rsrc", - "text/x-matlab", - "text/x-sas", - "application/R", - "application/x-ipynb+json"]; - //Does this data object match one of these IDs? - if(_.indexOf(softwareIds, this.get('formatId')) == -1) return false; - else return true; - }, - - /** - * Checks the formatId of this model and determines if it a PDF. - * @returns {boolean} true if this data object is a pdf, false if it is other - */ - isPDF: function(){ - //The list of formatIds that are images - var ids = ["application/pdf"]; - - //Does this data object match one of these IDs? - if(_.indexOf(ids, this.get('formatId')) == -1) return false; - else return true; - }, + switch (algorithm) { + case "MD5": + checksum.algorithm = algorithm; + hash = md5.create(); + _seek(); + break; + case "SHA-1": + // TODO: Support SHA-1 + // break; + default: + message = + "The given algorithm: " + algorithm + " is not supported."; + throw new Error(message); + } - /** - * Set the DataONE ProvONE provenance class - * param className - the shortened form of the actual classname value. The - * shortname will be appened to the ProvONE namespace, for example, - * the className "program" will result in the final class name - * "http://purl.dataone.org/provone/2015/01/15/ontology#Program" - * see https://github.com/DataONEorg/sem-prov-ontologies/blob/master/provenance/ProvONE/v1/provone.html - * @param {string} className - */ - setProvClass: function(className) { - className = className.toLowerCase(); - className = className.charAt(0).toUpperCase() + className.slice(1) - - /* This function is intended to be used for the ProvONE classes that are - * typically represented in DataONEObjects: "Data", "Program", and hopefully - * someday "Execution", as we don't allow the user to set the namespace - * e.g. to "PROV", so therefor we check for the currently known ProvONE classes. - */ - if (_.contains(['Program', 'Data', 'Visualization', 'Document', 'Execution', 'User'], className)) { - this.set("prov_instanceOfClass", [this.PROVONE + className]); - } else if (_.contains(['Entity', 'Usage', 'Generation', 'Association'], className)) { - this.set("prov_instanceOfClass", [this.PROV + className]); - } else { - message = "The given class name: " + className + " is not in the known ProvONE or PROV classes." - throw new Error(message); - } - }, + /* + * A helper function internal to calculateChecksum() used to slice + * the file at the next offset by slice size + */ + function _seek() { + var calculated = false; + var slice; + // Digest the checksum when we're done calculating + if (offset >= file.size) { + hash.digest(); + calculated = true; + return calculated; + } + // slice the file and read the slice + slice = file.slice(offset, offset + sliceSize); + reader.readAsArrayBuffer(slice); + return calculated; + } + }, - /** - * Calculate a checksum for the object - * @param {string} [algorithm] The algorithm to use, defaults to MD5 - * @return {string} A checksum plain JS object with value and algorithm attributes - */ - calculateChecksum: function(algorithm) { - var algorithm = algorithm || "MD5"; - var checksum = {algorithm: undefined, value: undefined}; - var hash; // The checksum hash - var file; // The file to be read by slicing - var reader; // The FileReader used to read each slice - var offset = 0; // Byte offset for reading slices - var sliceSize = Math.pow(2,20) // 1MB slices - var model = this; - - // Do we have a file? - if (this.get("uploadFile") instanceof Blob) { - file = this.get("uploadFile"); - reader = new FileReader(); - /* Handle load errors */ - reader.onerror = function(event) { - console.log("Error reading: " + event); - }; - /* Show progress */ - reader.onprogress = function(event) { - }; - /* Handle load finish */ - reader.onloadend = function(event) { - if (event.target.readyState == FileReader.DONE) { - hash.update(event.target.result); - } - offset += sliceSize; - if ( _seek() ) { - model.set("checksum", hash.hex()); - model.set("checksumAlgorithm", checksum.algorithm); - model.trigger("checksumCalculated", model.attributes); - }; - }; - } else { - message = "The given object is not a blob or a file object." - throw new Error(message); - } + /** + * Checks if the pid or sid or given string is a DOI + * + * @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model + * @returns {boolean} True if it is a DOI + */ + isDOI: function (customString) { + return ( + isDOI(customString) || + isDOI(this.get("id")) || + isDOI(this.get("seriesId")) + ); + }, - switch ( algorithm ) { - case "MD5": - checksum.algorithm = algorithm; - hash = md5.create(); - _seek(); - break; - case "SHA-1": - // TODO: Support SHA-1 - // break; - default: - message = "The given algorithm: " + algorithm + " is not supported." - throw new Error(message); + /** + * Creates an array of objects that represent Member Nodes that could possibly be this + * object's authoritative MN. This function updates the `possibleAuthMNs` attribute on this model. + */ + setPossibleAuthMNs: function () { + //Only do this for Coordinating Node MetacatUIs. + if (MetacatUI.appModel.get("alternateRepositories").length) { + //Set the possibleAuthMNs attribute + var possibleAuthMNs = []; + + //If a datasource is already found for this Portal, move that to the top of the list of auth MNs + var datasource = this.get("datasource") || ""; + if (datasource) { + //Find the MN object that matches the datasource node ID + var datasourceMN = _.findWhere( + MetacatUI.appModel.get("alternateRepositories"), + { identifier: datasource }, + ); + if (datasourceMN) { + //Clone the MN object and add it to the array + var clonedDatasourceMN = Object.assign({}, datasourceMN); + possibleAuthMNs.push(clonedDatasourceMN); } + } - /* - * A helper function internal to calculateChecksum() used to slice - * the file at the next offset by slice size - */ - function _seek() { - var calculated = false; - var slice; - // Digest the checksum when we're done calculating - if (offset >= file.size) { - hash.digest(); - calculated = true; - return calculated; - } - // slice the file and read the slice - slice = file.slice(offset, offset + sliceSize); - reader.readAsArrayBuffer(slice); - return calculated; - + //If there is an active alternate repo, move that to the top of the list of auth MNs + var activeAltRepo = + MetacatUI.appModel.get("activeAlternateRepositoryId") || ""; + if (activeAltRepo) { + var activeAltRepoMN = _.findWhere( + MetacatUI.appModel.get("alternateRepositories"), + { identifier: activeAltRepo }, + ); + if (activeAltRepoMN) { + //Clone the MN object and add it to the array + var clonedActiveAltRepoMN = Object.assign({}, activeAltRepoMN); + possibleAuthMNs.push(clonedActiveAltRepoMN); } - }, + } - /** - * Checks if the pid or sid or given string is a DOI - * - * @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model - * @returns {boolean} True if it is a DOI - */ - isDOI: function(customString) { - return isDOI(customString) || - isDOI(this.get("id")) || - isDOI(this.get("seriesId")); - }, + //Add all the other alternate repositories to the list of auth MNs + var otherPossibleAuthMNs = _.reject( + MetacatUI.appModel.get("alternateRepositories"), + function (mn) { + return ( + mn.identifier == datasource || mn.identifier == activeAltRepo + ); + }, + ); + //Clone each MN object and add to the array + _.each(otherPossibleAuthMNs, function (mn) { + var clonedMN = Object.assign({}, mn); + possibleAuthMNs.push(clonedMN); + }); - /** - * Creates an array of objects that represent Member Nodes that could possibly be this - * object's authoritative MN. This function updates the `possibleAuthMNs` attribute on this model. - */ - setPossibleAuthMNs: function(){ - - //Only do this for Coordinating Node MetacatUIs. - if( MetacatUI.appModel.get("alternateRepositories").length ){ - //Set the possibleAuthMNs attribute - var possibleAuthMNs = []; - - //If a datasource is already found for this Portal, move that to the top of the list of auth MNs - var datasource = this.get("datasource") || ""; - if( datasource ){ - //Find the MN object that matches the datasource node ID - var datasourceMN = _.findWhere(MetacatUI.appModel.get("alternateRepositories"), { identifier: datasource }); - if( datasourceMN ){ - //Clone the MN object and add it to the array - var clonedDatasourceMN = Object.assign({}, datasourceMN); - possibleAuthMNs.push(clonedDatasourceMN); - } - } + //Update this model + this.set("possibleAuthMNs", possibleAuthMNs); + } + }, - //If there is an active alternate repo, move that to the top of the list of auth MNs - var activeAltRepo = MetacatUI.appModel.get("activeAlternateRepositoryId") || ""; - if( activeAltRepo ){ - var activeAltRepoMN = _.findWhere(MetacatUI.appModel.get("alternateRepositories"), { identifier: activeAltRepo }); - if( activeAltRepoMN ){ - //Clone the MN object and add it to the array - var clonedActiveAltRepoMN = Object.assign({}, activeAltRepoMN); - possibleAuthMNs.push(clonedActiveAltRepoMN); - } + /** + * Removes white space from string values returned by Solr when the white space causes issues. + * For now this only effects the `resourceMap` field, which will index new line characters and spaces + * when the RDF XML has those in the `identifier` XML element content. This was causing bugs where DataONEObject + * models were created with `id`s with new line and white space characters (e.g. `\n urn:uuid:1234...`) + * @param {object} json - The Solr document as a JS Object, which will be directly altered + */ + removeWhiteSpaceFromSolrFields: function (json) { + if (typeof json.resourceMap == "string") { + json.resourceMap = json.resourceMap.trim(); + } else if (Array.isArray(json.resourceMap)) { + let newResourceMapIds = []; + _.each(json.resourceMap, function (rMapId) { + if (typeof rMapId == "string") { + newResourceMapIds.push(rMapId.trim()); } - - //Add all the other alternate repositories to the list of auth MNs - var otherPossibleAuthMNs = _.reject(MetacatUI.appModel.get("alternateRepositories"), function(mn){ - return (mn.identifier == datasource || mn.identifier == activeAltRepo); - }); - //Clone each MN object and add to the array - _.each(otherPossibleAuthMNs, function(mn){ - var clonedMN = Object.assign({}, mn); - possibleAuthMNs.push(clonedMN); - }); - - //Update this model - this.set("possibleAuthMNs", possibleAuthMNs); - - } - }, - - /** - * Removes white space from string values returned by Solr when the white space causes issues. - * For now this only effects the `resourceMap` field, which will index new line characters and spaces - * when the RDF XML has those in the `identifier` XML element content. This was causing bugs where DataONEObject - * models were created with `id`s with new line and white space characters (e.g. `\n urn:uuid:1234...`) - * @param {object} json - The Solr document as a JS Object, which will be directly altered - */ - removeWhiteSpaceFromSolrFields: function(json){ - if( typeof json.resourceMap == "string" ){ - json.resourceMap = json.resourceMap.trim(); - } - else if( Array.isArray(json.resourceMap) ){ - let newResourceMapIds = []; - _.each(json.resourceMap, function(rMapId){ - if( typeof rMapId == "string" ){ - newResourceMapIds.push(rMapId.trim()); - } - }); - json.resourceMap = newResourceMapIds; - } + }); + json.resourceMap = newResourceMapIds; } + }, }, /** @lends DataONEObject.prototype */ { /** - * Generate a unique identifier to be used as an XML id attribute - * @returns {string} The identifier string that was generated - */ - generateId: function() { - var idStr = ''; // the id to return + * Generate a unique identifier to be used as an XML id attribute + * @returns {string} The identifier string that was generated + */ + generateId: function () { + var idStr = ""; // the id to return var length = 30; // the length of the generated string - var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'.split(''); + var chars = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".split( + "", + ); for (var i = 0; i < length; i++) { idStr += chars[Math.floor(Math.random() * chars.length)]; } return idStr; - } - }); + }, + }, + ); - return DataONEObject; + return DataONEObject; });
diff --git a/docs/docs/src_js_models_LogsSearch.js.html b/docs/docs/src_js_models_LogsSearch.js.html index 5196891a2..efcec6a0f 100644 --- a/docs/docs/src_js_models_LogsSearch.js.html +++ b/docs/docs/src_js_models_LogsSearch.js.html @@ -44,199 +44,249 @@

Source: src/js/models/LogsSearch.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'models/Search'],
-	function($, _, Backbone, SearchModel) {
-	'use strict';
-
-	/**
-  * @class LogsSearch
-  * @classdesc Searches the DataONE aggregated event logs. The DataONE Metrics Service has replaced
-  * the DataONE event log service.
-  * @deprecated
-  * @classcategory Deprecated
-  */
-	var LogsSearch = SearchModel.extend(
-		/** @lends LogsSearch.prototype */{
-		// This model contains all of the search/filter terms
-		/*
-		 * Search filters can be either plain text or a filter object with the following options:
-		 * filterLabel - text that will be displayed in the filter element in the UI
-		 * label - text that will be displayed in the autocomplete  list
-		 * value - the value that will be included in the query
-		 * description - a longer text description of the filter value
-     * @classcategory Models
-		 */
-		defaults: function(){
-			return {
-				all: [],
-				dateLogged: [],
-				nodeId: MetacatUI.appModel.get("nodeId") || null,
-				id: [],
-				pid: [],
-				event: [],
-				userAgent: [],
-				dateAggregated: [],
-				inPartialRobotList: "false",
-				isRepeatVisit: "false",
-				isPublic: [],
-				entryId: [],
-				city: [],
-				region: [],
-				country: [],
-				location: [],
-				geohashes: [],
-				geohashLevel: 9,
-				geohashGroups: {},
-				username: [],
-				size: [],
-				formatId: [],
-				formatType: [],
-				exclude: [{
-					field: null,
-					value: null
-				}],
-				facets: [],
-				facetRanges: [],
-				facetRangeStart: function(){
-					var twentyYrsAgo = new Date();
-					twentyYrsAgo.setFullYear( twentyYrsAgo.getFullYear() - 20 );
-					return twentyYrsAgo.toISOString();
-				}(),
-				facetRangeEnd: function(){
-					var now = new Date();
-					return now.toISOString();
-				}(),
-				facetRangeGap: "%2B1MONTH",
-				facetMinCount: "1"
-			}
-		},
-
-		initialize: function(){
-			this.listenTo(this, "change:geohashes", this.groupGeohashes);
-		},
-
-		//Map the filter names to their index field names
-		fieldNameMap: {
-			all: "",
-			dateLogged: "dateLogged",
-			datasource: "nodeId",
-			nodeId: "nodeId",
-			id: "id",
-			pid: "pid",
-			event: "event",
-			userAgent: "userAgent",
-			dateAggregated: "dateAggregated",
-			isPublic: "isPublic",
-			entryId: "entryId",
-			city: "city",
-			region: "region",
-			country: "country",
-			location: "location",
-			size: "size",
-			username: "rightsHolder",
-			formatId: "formatId",
-			formatType: "formatType",
-			inPartialRobotList : "inPartialRobotList",
-			inFullRobotList : "inFullRobotList",
-			isRepeatVisit : "isRepeatVisit"
-		},
-
-		setNodeId: function(){
-			if(MetacatUI.nodeModel.get("currentMemberNode"))
-				this.set("nodeId", MetacatUI.nodeModel.get("currentMemberNode"));
-		},
-
-		/*
-		 * Get the query string based on the attributes set in this model
-		 */
-		getQuery: function(){
-			var query = "",
-				model = this;
-
-			var otherFilters = ["event", "formatType", "formatId", "id", "pid", "userAgent", "inPartialRobotList", "inFullRobotList", "isRepeatVisit", "dateAggregated", "dateLogged", "entryId", "city", "region", "location", "size", "username"];
-
-			//-------nodeId--------
-			//Update the Node Id
-			if(!this.get("nodeId"))
-				this.setNodeId();
-
-			if(this.filterIsAvailable("nodeId") && this.get("nodeId")){
-				var value = this.get("nodeId");
-
-				//Don't filter by nodeId when it is set to a CN
-				if((typeof value == "string") && (value.substr(value.lastIndexOf(":")+1, 2).toLowerCase() != "cn")){
-					//For multiple values
-					if(Array.isArray(value) && value.length){
-						query += "+" + model.getGroupedQuery(model.fieldNameMap["nodeId"], value, { operator: "OR", subtext: false });
-					}
-					else if(value && value.length){
-						// Does this need to be wrapped in quotes?
-						if(model.needsQuotes(value)) value = "%22" + encodeURIComponent(value) + "%22";
-						else value = model.escapeSpecialChar(encodeURIComponent(value));
-
-						query += "+" + model.fieldNameMap["nodeId"] + ":" + value;
-					}
-				}
-				else if(Array.isArray(value)){
-					query += "+" + model.getGroupedQuery(model.fieldNameMap["nodeId"], value, { operator: "OR", subtext: false });
-				}
-			}
-
-			//-----Other Filters/Basic Filters-----
-			_.each(otherFilters, function(filterName){
-				if(model.filterIsAvailable(filterName)){
-					var filterValue = null;
-					var filterValues = model.get(filterName);
-
-					//Check that this filter is set
-					if((typeof filterValues == "undefined") || !filterValues) return;
-
-					//For multiple values
-					if(Array.isArray(filterValues) && filterValues.length){
-						query += "+" + model.getGroupedQuery(model.fieldNameMap[filterName], filterValues, { operator: "OR", subtext: false });
-					}
-					else if(filterValues && filterValues.length){
-						// Does this need to be wrapped in quotes?
-						if(model.needsQuotes(filterValues)) filterValues = "%22" + encodeURIComponent(filterValues) + "%22";
-						else filterValues = model.escapeSpecialChar(encodeURIComponent(filterValues));
-
-						query += "+" + model.fieldNameMap[filterName] + ":" + filterValues;
-					}
-				}
-			});
-
-			return query;
-		},
-
-		getFacetQuery: function(){
-			var query = "&facet=true&facet.limit=-1&facet.mincount=" + this.get("facetMinCount"),
-				model = this;
-
-			if(typeof this.get("facets") == "string")
-				this.set("facets", [this.get("facets")]);
-			_.each(this.get("facets"), function(facetField, i, list){
-
-				if(model.filterIsAvailable(facetField)){
-					query += "&facet.field=" + facetField;
-				}
-			});
-
-			_.each(this.get("facetRanges"), function(facetField, i, list){
-				if(model.filterIsAvailable(facetField)){
-					  query += "&facet.range=" + facetField +
-					  			"&facet.range.start=" + model.get("facetRangeStart") +
-					  			"&facet.range.end=" + model.get("facetRangeEnd") +
-					  			"&facet.range.gap=" + model.get("facetRangeGap");
-					  return;
-				}
-			});
-
-			return query;
-		}
-
-	});
-	return LogsSearch;
+            
define(["jquery", "underscore", "backbone", "models/Search"], function (
+  $,
+  _,
+  Backbone,
+  SearchModel,
+) {
+  "use strict";
+
+  /**
+   * @class LogsSearch
+   * @classdesc Searches the DataONE aggregated event logs. The DataONE Metrics Service has replaced
+   * the DataONE event log service.
+   * @deprecated
+   * @classcategory Deprecated
+   */
+  var LogsSearch = SearchModel.extend(
+    /** @lends LogsSearch.prototype */ {
+      // This model contains all of the search/filter terms
+      /*
+       * Search filters can be either plain text or a filter object with the following options:
+       * filterLabel - text that will be displayed in the filter element in the UI
+       * label - text that will be displayed in the autocomplete  list
+       * value - the value that will be included in the query
+       * description - a longer text description of the filter value
+       * @classcategory Models
+       */
+      defaults: function () {
+        return {
+          all: [],
+          dateLogged: [],
+          nodeId: MetacatUI.appModel.get("nodeId") || null,
+          id: [],
+          pid: [],
+          event: [],
+          userAgent: [],
+          dateAggregated: [],
+          inPartialRobotList: "false",
+          isRepeatVisit: "false",
+          isPublic: [],
+          entryId: [],
+          city: [],
+          region: [],
+          country: [],
+          location: [],
+          geohashes: [],
+          geohashLevel: 9,
+          geohashGroups: {},
+          username: [],
+          size: [],
+          formatId: [],
+          formatType: [],
+          exclude: [
+            {
+              field: null,
+              value: null,
+            },
+          ],
+          facets: [],
+          facetRanges: [],
+          facetRangeStart: (function () {
+            var twentyYrsAgo = new Date();
+            twentyYrsAgo.setFullYear(twentyYrsAgo.getFullYear() - 20);
+            return twentyYrsAgo.toISOString();
+          })(),
+          facetRangeEnd: (function () {
+            var now = new Date();
+            return now.toISOString();
+          })(),
+          facetRangeGap: "%2B1MONTH",
+          facetMinCount: "1",
+        };
+      },
+
+      initialize: function () {
+        this.listenTo(this, "change:geohashes", this.groupGeohashes);
+      },
+
+      //Map the filter names to their index field names
+      fieldNameMap: {
+        all: "",
+        dateLogged: "dateLogged",
+        datasource: "nodeId",
+        nodeId: "nodeId",
+        id: "id",
+        pid: "pid",
+        event: "event",
+        userAgent: "userAgent",
+        dateAggregated: "dateAggregated",
+        isPublic: "isPublic",
+        entryId: "entryId",
+        city: "city",
+        region: "region",
+        country: "country",
+        location: "location",
+        size: "size",
+        username: "rightsHolder",
+        formatId: "formatId",
+        formatType: "formatType",
+        inPartialRobotList: "inPartialRobotList",
+        inFullRobotList: "inFullRobotList",
+        isRepeatVisit: "isRepeatVisit",
+      },
+
+      setNodeId: function () {
+        if (MetacatUI.nodeModel.get("currentMemberNode"))
+          this.set("nodeId", MetacatUI.nodeModel.get("currentMemberNode"));
+      },
+
+      /*
+       * Get the query string based on the attributes set in this model
+       */
+      getQuery: function () {
+        var query = "",
+          model = this;
+
+        var otherFilters = [
+          "event",
+          "formatType",
+          "formatId",
+          "id",
+          "pid",
+          "userAgent",
+          "inPartialRobotList",
+          "inFullRobotList",
+          "isRepeatVisit",
+          "dateAggregated",
+          "dateLogged",
+          "entryId",
+          "city",
+          "region",
+          "location",
+          "size",
+          "username",
+        ];
+
+        //-------nodeId--------
+        //Update the Node Id
+        if (!this.get("nodeId")) this.setNodeId();
+
+        if (this.filterIsAvailable("nodeId") && this.get("nodeId")) {
+          var value = this.get("nodeId");
+
+          //Don't filter by nodeId when it is set to a CN
+          if (
+            typeof value == "string" &&
+            value.substr(value.lastIndexOf(":") + 1, 2).toLowerCase() != "cn"
+          ) {
+            //For multiple values
+            if (Array.isArray(value) && value.length) {
+              query +=
+                "+" +
+                model.getGroupedQuery(model.fieldNameMap["nodeId"], value, {
+                  operator: "OR",
+                  subtext: false,
+                });
+            } else if (value && value.length) {
+              // Does this need to be wrapped in quotes?
+              if (model.needsQuotes(value))
+                value = "%22" + encodeURIComponent(value) + "%22";
+              else value = model.escapeSpecialChar(encodeURIComponent(value));
+
+              query += "+" + model.fieldNameMap["nodeId"] + ":" + value;
+            }
+          } else if (Array.isArray(value)) {
+            query +=
+              "+" +
+              model.getGroupedQuery(model.fieldNameMap["nodeId"], value, {
+                operator: "OR",
+                subtext: false,
+              });
+          }
+        }
+
+        //-----Other Filters/Basic Filters-----
+        _.each(otherFilters, function (filterName) {
+          if (model.filterIsAvailable(filterName)) {
+            var filterValue = null;
+            var filterValues = model.get(filterName);
+
+            //Check that this filter is set
+            if (typeof filterValues == "undefined" || !filterValues) return;
+
+            //For multiple values
+            if (Array.isArray(filterValues) && filterValues.length) {
+              query +=
+                "+" +
+                model.getGroupedQuery(
+                  model.fieldNameMap[filterName],
+                  filterValues,
+                  { operator: "OR", subtext: false },
+                );
+            } else if (filterValues && filterValues.length) {
+              // Does this need to be wrapped in quotes?
+              if (model.needsQuotes(filterValues))
+                filterValues = "%22" + encodeURIComponent(filterValues) + "%22";
+              else
+                filterValues = model.escapeSpecialChar(
+                  encodeURIComponent(filterValues),
+                );
+
+              query +=
+                "+" + model.fieldNameMap[filterName] + ":" + filterValues;
+            }
+          }
+        });
+
+        return query;
+      },
+
+      getFacetQuery: function () {
+        var query =
+            "&facet=true&facet.limit=-1&facet.mincount=" +
+            this.get("facetMinCount"),
+          model = this;
+
+        if (typeof this.get("facets") == "string")
+          this.set("facets", [this.get("facets")]);
+        _.each(this.get("facets"), function (facetField, i, list) {
+          if (model.filterIsAvailable(facetField)) {
+            query += "&facet.field=" + facetField;
+          }
+        });
+
+        _.each(this.get("facetRanges"), function (facetField, i, list) {
+          if (model.filterIsAvailable(facetField)) {
+            query +=
+              "&facet.range=" +
+              facetField +
+              "&facet.range.start=" +
+              model.get("facetRangeStart") +
+              "&facet.range.end=" +
+              model.get("facetRangeEnd") +
+              "&facet.range.gap=" +
+              model.get("facetRangeGap");
+            return;
+          }
+        });
+
+        return query;
+      },
+    },
+  );
+  return LogsSearch;
 });
 
diff --git a/docs/docs/src_js_models_LookupModel.js.html b/docs/docs/src_js_models_LookupModel.js.html index df464f356..1f7b5a452 100644 --- a/docs/docs/src_js_models_LookupModel.js.html +++ b/docs/docs/src_js_models_LookupModel.js.html @@ -44,12 +44,11 @@

Source: src/js/models/LookupModel.js

-
/*global define */
-define(["jquery", "jqueryui", "underscore", "backbone"], function (
+            
define(["jquery", "jqueryui", "underscore", "backbone"], function (
   $,
   $ui,
   _,
-  Backbone
+  Backbone,
 ) {
   "use strict";
 
@@ -262,7 +261,7 @@ 

Source: src/js/models/LookupModel.js

item["class"] = uri; item["ontology"] = "http://data.bioontology.org/ontologies/ECSO"; batchData["http://www.w3.org/2002/07/owl#Class"]["collection"].push( - item + item, ); }); @@ -436,7 +435,7 @@

Source: src/js/models/LookupModel.js

request, response, beforeRequest, - afterRequest + afterRequest, ) { // Handle errors in this function or in the findGrants function function handleError(error) { @@ -546,7 +545,7 @@

Source: src/js/models/LookupModel.js

//If the parsing XML failed, exit now console.error( "The accounts service did not return valid XML.", - e + e, ); return; } @@ -587,14 +586,14 @@

Source: src/js/models/LookupModel.js

")", type: type, }); - } + }, ); var term = $.ui.autocomplete.escapeRegex(request.term), startsWithMatcher = new RegExp("^" + term, "i"), startsWith = $.grep(list, function (value) { return startsWithMatcher.test( - value.label || value.value || value + value.label || value.value || value, ); }), containsMatcher = new RegExp(term, "i"), @@ -646,7 +645,7 @@

Source: src/js/models/LookupModel.js

}, success: function (data) { var sizeOfQueue = parseInt( - $(data).find("status > index > sizeOfQueue").text() + $(data).find("status > index > sizeOfQueue").text(), ); if (sizeOfQueue > 0 || sizeOfQueue == 0) { @@ -667,8 +666,8 @@

Source: src/js/models/LookupModel.js

$.ajax( _.extend( requestSettings, - MetacatUI.appUserModel.createAjaxSettings() - ) + MetacatUI.appUserModel.createAjaxSettings(), + ), ); } catch (e) { console.error(e); @@ -678,7 +677,7 @@

Source: src/js/models/LookupModel.js

} } }, - } + }, ); return LookupModel; }); diff --git a/docs/docs/src_js_models_Map.js.html b/docs/docs/src_js_models_Map.js.html index 1fd637ae2..7af3126bc 100644 --- a/docs/docs/src_js_models_Map.js.html +++ b/docs/docs/src_js_models_Map.js.html @@ -44,203 +44,251 @@

Source: src/js/models/Map.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'gmaps'],
-	function($, _, Backbone, gmaps) {
-	'use strict';
+            
define(["jquery", "underscore", "backbone", "gmaps"], function (
+  $,
+  _,
+  Backbone,
+  gmaps,
+) {
+  "use strict";
 
   /**
-  * @class Map
-  * @classdesc The Map Model represents all of the settings and options for a Google Map.
-  * @classcategory Models
-  * @extends Backbone.Model
-  */
-	var Map = Backbone.Model.extend(
-    /** @lends Map.prototype */{
-		// This model contains all of the map settings used for searching datasets
-		defaults: function(){
-			var model = this;
-			return {
-				map: null,
-
-				//The options for the map using the Google Maps API MapOptions syntax
-				mapOptions:
-					(gmaps)?
-						{   zoom: 3,
-							minZoom: 3,
-							maxZoom: 16,
-						    center: new google.maps.LatLng(44, -103),
-							disableDefaultUI: true,
-						    zoomControl: true,
-						    zoomControlOptions: {
-							          style: google.maps.ZoomControlStyle.LARGE,
-							          position: google.maps.ControlPosition.LEFT_CENTER
-							        },
-							panControl: false,
-							scaleControl: true,
-							streetViewControl: false,
-							mapTypeControl: true,
-							mapTypeControlOptions:{
-									style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR,
-									mapTypeIds: [google.maps.MapTypeId.SATELLITE, google.maps.MapTypeId.TERRAIN],
-									position: google.maps.ControlPosition.LEFT_BOTTOM
-							},
-						    mapTypeId: google.maps.MapTypeId.TERRAIN,
-						    styles: [{"featureType":"water","stylers":[{"visibility":"on"},{"color":"#b5cbe4"}]},{"featureType":"landscape","stylers":[{"color":"#efefef"}]},{"featureType":"road.highway","elementType":"geometry","stylers":[{"color":"#83a5b0"}]},{"featureType":"road.arterial","elementType":"geometry","stylers":[{"color":"#bdcdd3"}]},{"featureType":"road.local","elementType":"geometry","stylers":[{"color":"#ffffff"}]},{"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#e3eed3"}]},{"featureType":"administrative","stylers":[{"visibility":"on"},{"lightness":33}]},{"featureType":"road"},{"featureType":"poi.park","elementType":"labels","stylers":[{"visibility":"on"},{"lightness":20}]},{},{"featureType":"road","stylers":[{"lightness":20}]}]
-						}
-					: null,
-
-				//Set to true to draw markers where tile counts are equal to 1. If set to false, a tile with the count "1" will be drawn instead.
-				drawMarkers: false,
-
-				//If this theme doesn't have an image in this location, Google maps will use their default marker image
-				markerImage: "./js/themes/" + MetacatUI.theme + "/img/map-marker.png",
-
-				//Keep track of the geohash level used to draw tiles on this map
-				tileGeohashLevel: 1,
-
-				///****** MAP TILE OPTIONS **********//
-				//The options for the tiles. Using Google Maps Web API
-				tileOptions: {
-				      fillOpacity: 0.2,
-				      strokeWeight: 1,
-				      strokePosition: (typeof google != "undefined")? google.maps.StrokePosition.INSIDE : "",
-				      strokeOpacity: 1
-				},
-
-				//The options for the tiles when they are hovered on. Using Google Maps Web API
-				tileOnHover: {
-						fillOpacity: 0.8,
-						strokeColor: "#FFFF00",
-						strokePosition: (typeof google != "undefined")? google.maps.StrokePosition.INSIDE : "",
-						strokeWeight: 1,
-						strokeOpacity: 1,
-						fillColor: "#FFFF66"
-				},
-
-				//The options for the tile text
-				tileLabelColorOnHover: '#000000',
-				tileLabelColor: '#444444',
-
-				//The tile hue - the number of the hue that will be used to color tiles
-				//Tile lightness - percent range of lightness/brightness of this tile hue
-				tileHue: MetacatUI.appModel.get("searchMapTileHue") || "192",
-				tileLightnessMax: 100,
-				tileLightnessMin: 30
-			}
-		},
-
-		initialize: function(options){
-
-		},
-
-		isMaxZoom: function(map){
-			var zoom = map.getZoom(),
-				type = map.getMapTypeId();
-
-			if(zoom >= this.get("mapOptions").maxZoom) return true;
-			else return false;
-		},
-
-		/**
-		 * This function will return the appropriate geohash level to use for mapping geohash tiles on the map at the specified zoom level.
-		 */
-		determineGeohashLevel: function(zoom){
-			var geohashLevel;
-
-			switch(zoom){
-				case 0: // The whole world zoom level
-					geohashLevel = 2;
-					break;
-				case 1:
-					geohashLevel = 2;
-					break;
-				case 2:
-					geohashLevel = 2;
-					break;
-				case 3:
-					geohashLevel = 2;
-					break;
-				case 4:
-					geohashLevel = 2;
-					break;
-				case 5:
-					geohashLevel = 3;
-					break;
-				case 6:
-					geohashLevel = 3;
-					break;
-				case 7:
-					geohashLevel = 4;
-					break;
-				case 8:
-					geohashLevel = 4;
-					break;
-				case 9:
-					geohashLevel = 4;
-					break;
-				case 10:
-					geohashLevel = 5;
-					break;
-				case 11:
-					geohashLevel = 5;
-					break;
-				case 12:
-					geohashLevel = 6;
-					break;
-				case 13:
-					geohashLevel = 6;
-					break;
-				case 14:
-					geohashLevel = 7;
-					break;
-				case 15:
-					geohashLevel = 7;
-					break;
-				case 16:
-					geohashLevel = 7;
-					break;
-				case 17:
-					geohashLevel = 8;
-					break;
-				case 18:
-					geohashLevel = 9;
-					break;
-				case 19:
-					geohashLevel = 9;
-					break;
-				case 20:
-					geohashLevel = 9;
-					break;
-				default:  //Anything over (Gmaps goes up to 19)
-					geohashLevel = 9;
-			}
-
-			return geohashLevel;
-		},
-
-		getSearchPrecision: function(zoom){
-			if(zoom <= 5) return 2;
-			else if(zoom <= 7) return 3;
-			else if (zoom <= 11) return 4;
-			else if (zoom <= 13) return 5;
-			else if (zoom <= 15) return 6;
-			else return 7;
-		},
-
-		/*
-		* Creates a LatLng Google Maps object based on the given latitude and longitude
-		*/
-		createLatLng: function(lat, long){
-			return new google.maps.LatLng(parseFloat(lat), parseFloat(long));
-		},
-
-		clear: function() {
-		    return this.set(_.clone(this.defaults()));
-		  }
-
-	});
-	return Map;
+   * @class Map
+   * @classdesc The Map Model represents all of the settings and options for a Google Map.
+   * @classcategory Models
+   * @extends Backbone.Model
+   */
+  var Map = Backbone.Model.extend(
+    /** @lends Map.prototype */ {
+      // This model contains all of the map settings used for searching datasets
+      defaults: function () {
+        var model = this;
+        return {
+          map: null,
+
+          //The options for the map using the Google Maps API MapOptions syntax
+          mapOptions: gmaps
+            ? {
+                zoom: 3,
+                minZoom: 3,
+                maxZoom: 16,
+                center: new google.maps.LatLng(44, -103),
+                disableDefaultUI: true,
+                zoomControl: true,
+                zoomControlOptions: {
+                  style: google.maps.ZoomControlStyle.LARGE,
+                  position: google.maps.ControlPosition.LEFT_CENTER,
+                },
+                panControl: false,
+                scaleControl: true,
+                streetViewControl: false,
+                mapTypeControl: true,
+                mapTypeControlOptions: {
+                  style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR,
+                  mapTypeIds: [
+                    google.maps.MapTypeId.SATELLITE,
+                    google.maps.MapTypeId.TERRAIN,
+                  ],
+                  position: google.maps.ControlPosition.LEFT_BOTTOM,
+                },
+                mapTypeId: google.maps.MapTypeId.TERRAIN,
+                styles: [
+                  {
+                    featureType: "water",
+                    stylers: [{ visibility: "on" }, { color: "#b5cbe4" }],
+                  },
+                  { featureType: "landscape", stylers: [{ color: "#efefef" }] },
+                  {
+                    featureType: "road.highway",
+                    elementType: "geometry",
+                    stylers: [{ color: "#83a5b0" }],
+                  },
+                  {
+                    featureType: "road.arterial",
+                    elementType: "geometry",
+                    stylers: [{ color: "#bdcdd3" }],
+                  },
+                  {
+                    featureType: "road.local",
+                    elementType: "geometry",
+                    stylers: [{ color: "#ffffff" }],
+                  },
+                  {
+                    featureType: "poi.park",
+                    elementType: "geometry",
+                    stylers: [{ color: "#e3eed3" }],
+                  },
+                  {
+                    featureType: "administrative",
+                    stylers: [{ visibility: "on" }, { lightness: 33 }],
+                  },
+                  { featureType: "road" },
+                  {
+                    featureType: "poi.park",
+                    elementType: "labels",
+                    stylers: [{ visibility: "on" }, { lightness: 20 }],
+                  },
+                  {},
+                  { featureType: "road", stylers: [{ lightness: 20 }] },
+                ],
+              }
+            : null,
+
+          //Set to true to draw markers where tile counts are equal to 1. If set to false, a tile with the count "1" will be drawn instead.
+          drawMarkers: false,
+
+          //If this theme doesn't have an image in this location, Google maps will use their default marker image
+          markerImage: "./js/themes/" + MetacatUI.theme + "/img/map-marker.png",
+
+          //Keep track of the geohash level used to draw tiles on this map
+          tileGeohashLevel: 1,
+
+          ///****** MAP TILE OPTIONS **********//
+          //The options for the tiles. Using Google Maps Web API
+          tileOptions: {
+            fillOpacity: 0.2,
+            strokeWeight: 1,
+            strokePosition:
+              typeof google != "undefined"
+                ? google.maps.StrokePosition.INSIDE
+                : "",
+            strokeOpacity: 1,
+          },
+
+          //The options for the tiles when they are hovered on. Using Google Maps Web API
+          tileOnHover: {
+            fillOpacity: 0.8,
+            strokeColor: "#FFFF00",
+            strokePosition:
+              typeof google != "undefined"
+                ? google.maps.StrokePosition.INSIDE
+                : "",
+            strokeWeight: 1,
+            strokeOpacity: 1,
+            fillColor: "#FFFF66",
+          },
+
+          //The options for the tile text
+          tileLabelColorOnHover: "#000000",
+          tileLabelColor: "#444444",
+
+          //The tile hue - the number of the hue that will be used to color tiles
+          //Tile lightness - percent range of lightness/brightness of this tile hue
+          tileHue: MetacatUI.appModel.get("searchMapTileHue") || "192",
+          tileLightnessMax: 100,
+          tileLightnessMin: 30,
+        };
+      },
+
+      initialize: function (options) {},
+
+      isMaxZoom: function (map) {
+        var zoom = map.getZoom(),
+          type = map.getMapTypeId();
+
+        if (zoom >= this.get("mapOptions").maxZoom) return true;
+        else return false;
+      },
+
+      /**
+       * This function will return the appropriate geohash level to use for mapping geohash tiles on the map at the specified zoom level.
+       */
+      determineGeohashLevel: function (zoom) {
+        var geohashLevel;
+
+        switch (zoom) {
+          case 0: // The whole world zoom level
+            geohashLevel = 2;
+            break;
+          case 1:
+            geohashLevel = 2;
+            break;
+          case 2:
+            geohashLevel = 2;
+            break;
+          case 3:
+            geohashLevel = 2;
+            break;
+          case 4:
+            geohashLevel = 2;
+            break;
+          case 5:
+            geohashLevel = 3;
+            break;
+          case 6:
+            geohashLevel = 3;
+            break;
+          case 7:
+            geohashLevel = 4;
+            break;
+          case 8:
+            geohashLevel = 4;
+            break;
+          case 9:
+            geohashLevel = 4;
+            break;
+          case 10:
+            geohashLevel = 5;
+            break;
+          case 11:
+            geohashLevel = 5;
+            break;
+          case 12:
+            geohashLevel = 6;
+            break;
+          case 13:
+            geohashLevel = 6;
+            break;
+          case 14:
+            geohashLevel = 7;
+            break;
+          case 15:
+            geohashLevel = 7;
+            break;
+          case 16:
+            geohashLevel = 7;
+            break;
+          case 17:
+            geohashLevel = 8;
+            break;
+          case 18:
+            geohashLevel = 9;
+            break;
+          case 19:
+            geohashLevel = 9;
+            break;
+          case 20:
+            geohashLevel = 9;
+            break;
+          default: //Anything over (Gmaps goes up to 19)
+            geohashLevel = 9;
+        }
+
+        return geohashLevel;
+      },
+
+      getSearchPrecision: function (zoom) {
+        if (zoom <= 5) return 2;
+        else if (zoom <= 7) return 3;
+        else if (zoom <= 11) return 4;
+        else if (zoom <= 13) return 5;
+        else if (zoom <= 15) return 6;
+        else return 7;
+      },
+
+      /*
+       * Creates a LatLng Google Maps object based on the given latitude and longitude
+       */
+      createLatLng: function (lat, long) {
+        return new google.maps.LatLng(parseFloat(lat), parseFloat(long));
+      },
+
+      clear: function () {
+        return this.set(_.clone(this.defaults()));
+      },
+    },
+  );
+  return Map;
 });
 
diff --git a/docs/docs/src_js_models_MetricsModel.js.html b/docs/docs/src_js_models_MetricsModel.js.html index fab214dee..0ccd28ac0 100644 --- a/docs/docs/src_js_models_MetricsModel.js.html +++ b/docs/docs/src_js_models_MetricsModel.js.html @@ -44,294 +44,302 @@

Source: src/js/models/MetricsModel.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone'],
-    function($, _, Backbone) {
-    'use strict';
-
-    /**
-     * @class Metrics
-     * @classdesc A single result from the DataONE Metrics Service
-     * @classcategory Models
-     * @extends Backbone.Model
-     * @constructor
-     */
-    var Metrics = Backbone.Model.extend(
-      /** @lends Metrics.prototype */{
-
-        /**
-        * The name of this Model
-        * @type {string}
-        */
-        type: "Metrics",
-
-        defaults: {
-            metricRequest: null,
-            startDate: null,
-            endDate: null,
-            results: null,
-            resultDetails: null,
-            pid_list: null,
-            url: null,
-            filterType: null,
-
-            // metrics and metric Facets returned as response from the user
-            // datatype: array
-            citations: null,
-            views: null,
-            downloads: null,
-            months: null,
-            country: null,
-            years: null,
-            repository: null,
-            award: null,
-            datasets: null,
-
-
-            // Total counts for metrics
-            totalCitations: null,
-            totalViews: null,
-            totalDownloads: null,
-
-            // flag to send POST request to the metrics service
-            useMetricsPost: false,
-
-            // colelctionQuery for Portal Objects
-            filterQueryObject: null,
-            forwardCollectionQuery: false,
-
-            metricsRequiredFields: {
-                metricName: true,
-                pid_list: true
-            }
-
-        },
-
-        metricRequest: {
-            "metricsPage": {
-                "total": 0,
-                "start": 0,
-                "count": 0
-            },
-            "metrics": [
-                "citations",
-                "downloads",
-                "views"
-            ],
-            "filterBy": [
-                {
-                    "filterType": "",
-                    "values": [],
-                    "interpretAs": "list"
-                },
-                {
-                    "filterType": "month",
-                    "values": [],
-                    "interpretAs": "range"
-                }
-            ],
-            "groupBy": [
-                "month"
-            ]
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  "use strict";
+
+  /**
+   * @class Metrics
+   * @classdesc A single result from the DataONE Metrics Service
+   * @classcategory Models
+   * @extends Backbone.Model
+   * @constructor
+   */
+  var Metrics = Backbone.Model.extend(
+    /** @lends Metrics.prototype */ {
+      /**
+       * The name of this Model
+       * @type {string}
+       */
+      type: "Metrics",
+
+      defaults: {
+        metricRequest: null,
+        startDate: null,
+        endDate: null,
+        results: null,
+        resultDetails: null,
+        pid_list: null,
+        url: null,
+        filterType: null,
+
+        // metrics and metric Facets returned as response from the user
+        // datatype: array
+        citations: null,
+        views: null,
+        downloads: null,
+        months: null,
+        country: null,
+        years: null,
+        repository: null,
+        award: null,
+        datasets: null,
+
+        // Total counts for metrics
+        totalCitations: null,
+        totalViews: null,
+        totalDownloads: null,
+
+        // flag to send POST request to the metrics service
+        useMetricsPost: false,
+
+        // colelctionQuery for Portal Objects
+        filterQueryObject: null,
+        forwardCollectionQuery: false,
+
+        metricsRequiredFields: {
+          metricName: true,
+          pid_list: true,
         },
+      },
 
-        /**
-        * Initializing the Model objects pid and the metricName variables.
-        * @param {object} options
-        */
-        initialize: function(options) {
-            if((options) && options.pid_list !== 'undefined') {
-                this.set("pid_list", options.pid_list);
-                this.set("filterType", options.type);
-            }
-            this.set("startDate", "01/01/2012");
-
-            // overwrite forwardCollectionQuery flag
-            this.set("forwardCollectionQuery", MetacatUI.appModel.get("metricsForwardCollectionQuery"));
-
-            // url for the model that is used to for the fetch() call
-            this.url = MetacatUI.appModel.get("metricsUrl");
+      metricRequest: {
+        metricsPage: {
+          total: 0,
+          start: 0,
+          count: 0,
         },
-
-        /**
-        * Overriding the Model's fetch function.
-        */
-        fetch: function(){
-          var fetchOptions = {};
-          this.metricRequest.filterBy[0].filterType = this.get("filterType");
-          this.metricRequest.filterBy[0].values = this.get("pid_list");
-
-          // TODO: Set the startDate and endDate based on the datePublished and current date
-          // respctively.
-          this.metricRequest.filterBy[1].values = [];
-          this.metricRequest.filterBy[1].values.push(this.get("startDate"));
-          this.metricRequest.filterBy[1].values.push(this.getCurrentDate());
-
-          // set custom request settings if we're forwarding a CollectionQuery for a portal
-          if ( this.get("forwardCollectionQuery") && this.get("filterType") === "portal"
-                                        &&  this.get("filterQueryObject") != undefined
-                                        && typeof this.get("filterQueryObject") === "object") {
-            var filterQueryObject = this.get("filterQueryObject");
-            if (filterQueryObject.filterType != undefined
-                                        && filterQueryObject.filterType == "query") {
-                if (Array.isArray(filterQueryObject.values)
-                                        && filterQueryObject.values.length > 0 ) {
-
-                    // check if query object exists
-                    var queryInserted = this.metricRequest.filterBy.indexOf(filterQueryObject);
-
-                    // Performing HTTP POST?
-                    if(this.get("useMetricsPost")) {
-                        // insert query object
-                        if (queryInserted < 0) {
-                            this.metricRequest.filterBy.push(filterQueryObject);
-                        }
-
-                        fetchOptions = _.extend({
-                            data:JSON.stringify(this.metricRequest),
-                            type:"POST",
-                            timeout:300000
-                        });
-                    }
-                    else {
-                        // insert query object
-                        var collectionQuery = filterQueryObject.values[0];
-                        if (collectionQuery.length < 1000 && queryInserted < 0) {
-                            this.metricRequest.filterBy.push(filterQueryObject);
-                        }
-
-                        // set the fetch options for
-                        var model = this;
-                        fetchOptions = _.extend({
-                            data:"metricsRequest="+JSON.stringify(this.metricRequest),
-                            timeout:300000,
-                            // on error recursively call fetch, but this time use POST
-                            error: function(response){
-                                model.set("useMetricsPost", "true");
-                                model.fetch();
-                            }
-                        });
-                    }
+        metrics: ["citations", "downloads", "views"],
+        filterBy: [
+          {
+            filterType: "",
+            values: [],
+            interpretAs: "list",
+          },
+          {
+            filterType: "month",
+            values: [],
+            interpretAs: "range",
+          },
+        ],
+        groupBy: ["month"],
+      },
+
+      /**
+       * Initializing the Model objects pid and the metricName variables.
+       * @param {object} options
+       */
+      initialize: function (options) {
+        if (options && options.pid_list !== "undefined") {
+          this.set("pid_list", options.pid_list);
+          this.set("filterType", options.type);
+        }
+        this.set("startDate", "01/01/2012");
+
+        // overwrite forwardCollectionQuery flag
+        this.set(
+          "forwardCollectionQuery",
+          MetacatUI.appModel.get("metricsForwardCollectionQuery"),
+        );
+
+        // url for the model that is used to for the fetch() call
+        this.url = MetacatUI.appModel.get("metricsUrl");
+      },
+
+      /**
+       * Overriding the Model's fetch function.
+       */
+      fetch: function () {
+        var fetchOptions = {};
+        this.metricRequest.filterBy[0].filterType = this.get("filterType");
+        this.metricRequest.filterBy[0].values = this.get("pid_list");
+
+        // TODO: Set the startDate and endDate based on the datePublished and current date
+        // respctively.
+        this.metricRequest.filterBy[1].values = [];
+        this.metricRequest.filterBy[1].values.push(this.get("startDate"));
+        this.metricRequest.filterBy[1].values.push(this.getCurrentDate());
+
+        // set custom request settings if we're forwarding a CollectionQuery for a portal
+        if (
+          this.get("forwardCollectionQuery") &&
+          this.get("filterType") === "portal" &&
+          this.get("filterQueryObject") != undefined &&
+          typeof this.get("filterQueryObject") === "object"
+        ) {
+          var filterQueryObject = this.get("filterQueryObject");
+          if (
+            filterQueryObject.filterType != undefined &&
+            filterQueryObject.filterType == "query"
+          ) {
+            if (
+              Array.isArray(filterQueryObject.values) &&
+              filterQueryObject.values.length > 0
+            ) {
+              // check if query object exists
+              var queryInserted =
+                this.metricRequest.filterBy.indexOf(filterQueryObject);
+
+              // Performing HTTP POST?
+              if (this.get("useMetricsPost")) {
+                // insert query object
+                if (queryInserted < 0) {
+                  this.metricRequest.filterBy.push(filterQueryObject);
                 }
-            }
-          }
 
-          // check if we need to set fetchOptions
-          if (Object.keys(fetchOptions).length === 0) {
-            if ( this.get("useMetricsPost") ) {
                 fetchOptions = _.extend({
-                    data:JSON.stringify(this.metricRequest),
-                    type:"POST",
-                    timeout:300000
+                  data: JSON.stringify(this.metricRequest),
+                  type: "POST",
+                  timeout: 300000,
                 });
-            }
-            else {
+              } else {
+                // insert query object
+                var collectionQuery = filterQueryObject.values[0];
+                if (collectionQuery.length < 1000 && queryInserted < 0) {
+                  this.metricRequest.filterBy.push(filterQueryObject);
+                }
+
+                // set the fetch options for
+                var model = this;
                 fetchOptions = _.extend({
-                    data:"metricsRequest="+JSON.stringify(this.metricRequest),
-                    timeout:300000,
+                  data: "metricsRequest=" + JSON.stringify(this.metricRequest),
+                  timeout: 300000,
+                  // on error recursively call fetch, but this time use POST
+                  error: function (response) {
+                    model.set("useMetricsPost", "true");
+                    model.fetch();
+                  },
                 });
+              }
             }
           }
-
-          //This calls the Backbone fetch() function but with our custom fetch options.
-          return Backbone.Model.prototype.fetch.call(this, fetchOptions);
-        },
-
-        /**
-        * Get's a string version of today's date
-        * @return {string}
-        */
-        getCurrentDate: function() {
-            var today = new Date();
-            var dd = today.getDate();
-            var mm = today.getMonth()+1; //January is 0!
-
-            var yyyy = today.getFullYear();
-            if(dd<10){
-                dd='0'+dd;
-            }
-            if(mm<10){
-                mm='0'+mm;
-            }
-            var today = mm+'/'+dd+'/'+yyyy;
-            return today;
-        },
-
-        /**
-        * Parsing the response for setting the Model's member variables.
-        */
-        parse: function(response){
-            var metricsObject = {
-                "metricRequest": response.metricsRequest,
-                "citations": response.results.citations,
-                "views": response.results.views,
-                "downloads": response.results.downloads,
-                "months": response.results.months,
-                "country": response.results.country,
-                "resultDetails": response.resultDetails,
-                "datasets": response.results.datasets
-            }
-
-            if (response.results.citations != null) {
-                metricsObject["totalCitations"] =  response.results.citations.reduce(function(acc, val) { return acc + val; }, 0)
-            }
-            else {
-                metricsObject["totalCitations"] =  0
-            }
-
-            if (response.results.downloads != null) {
-                metricsObject["totalDownloads"] =  response.results.downloads.reduce(function(acc, val) { return acc + val; }, 0)
-            }
-            else {
-                metricsObject["totalDownloads"] =  0
-            }
-
-            if (response.results.views != null) {
-                metricsObject["totalViews"] =  response.results.views.reduce(function(acc, val) { return acc + val; }, 0)
-            }
-            else {
-                metricsObject["totalViews"] =  0
-            }
-
-            //trim off the leading zeros and their corresponding months
-            if (response.results.months != null) {
-
-                // iterate all the metrics objects and remove the entry if the counts are 0
-                for (var i = 0 ; i < metricsObject["months"].length; i++) {
-
-                    if ( metricsObject["citations"] != null &&
-                        metricsObject["views"]     != null &&
-                        metricsObject["downloads"] != null ) {
-
-                        if (( metricsObject["citations"][i] == 0 ) &&
-                            ( metricsObject["views"][i]     == 0 ) &&
-                            ( metricsObject["downloads"][i] == 0 )) {
-
-                            metricsObject["months"].splice(i,1);
-                            metricsObject["citations"].splice(i,1);
-                            metricsObject["views"].splice(i,1);
-                            metricsObject["downloads"].splice(i,1);
-
-                            // if country facet was part of the request; update object;
-                            if ( metricsObject["country"] != null) {
-                                metricsObject["country"].splice(i,1)
-                            }
-
-                            // modified array size; decrement the counter;
-                            i--;
-                        }
-                        else {
-                            break;
-                        }
-                    }
+        }
+
+        // check if we need to set fetchOptions
+        if (Object.keys(fetchOptions).length === 0) {
+          if (this.get("useMetricsPost")) {
+            fetchOptions = _.extend({
+              data: JSON.stringify(this.metricRequest),
+              type: "POST",
+              timeout: 300000,
+            });
+          } else {
+            fetchOptions = _.extend({
+              data: "metricsRequest=" + JSON.stringify(this.metricRequest),
+              timeout: 300000,
+            });
+          }
+        }
+
+        //This calls the Backbone fetch() function but with our custom fetch options.
+        return Backbone.Model.prototype.fetch.call(this, fetchOptions);
+      },
+
+      /**
+       * Get's a string version of today's date
+       * @return {string}
+       */
+      getCurrentDate: function () {
+        var today = new Date();
+        var dd = today.getDate();
+        var mm = today.getMonth() + 1; //January is 0!
+
+        var yyyy = today.getFullYear();
+        if (dd < 10) {
+          dd = "0" + dd;
+        }
+        if (mm < 10) {
+          mm = "0" + mm;
+        }
+        var today = mm + "/" + dd + "/" + yyyy;
+        return today;
+      },
+
+      /**
+       * Parsing the response for setting the Model's member variables.
+       */
+      parse: function (response) {
+        var metricsObject = {
+          metricRequest: response.metricsRequest,
+          citations: response.results.citations,
+          views: response.results.views,
+          downloads: response.results.downloads,
+          months: response.results.months,
+          country: response.results.country,
+          resultDetails: response.resultDetails,
+          datasets: response.results.datasets,
+        };
+
+        if (response.results.citations != null) {
+          metricsObject["totalCitations"] = response.results.citations.reduce(
+            function (acc, val) {
+              return acc + val;
+            },
+            0,
+          );
+        } else {
+          metricsObject["totalCitations"] = 0;
+        }
+
+        if (response.results.downloads != null) {
+          metricsObject["totalDownloads"] = response.results.downloads.reduce(
+            function (acc, val) {
+              return acc + val;
+            },
+            0,
+          );
+        } else {
+          metricsObject["totalDownloads"] = 0;
+        }
+
+        if (response.results.views != null) {
+          metricsObject["totalViews"] = response.results.views.reduce(function (
+            acc,
+            val,
+          ) {
+            return acc + val;
+          }, 0);
+        } else {
+          metricsObject["totalViews"] = 0;
+        }
+
+        //trim off the leading zeros and their corresponding months
+        if (response.results.months != null) {
+          // iterate all the metrics objects and remove the entry if the counts are 0
+          for (var i = 0; i < metricsObject["months"].length; i++) {
+            if (
+              metricsObject["citations"] != null &&
+              metricsObject["views"] != null &&
+              metricsObject["downloads"] != null
+            ) {
+              if (
+                metricsObject["citations"][i] == 0 &&
+                metricsObject["views"][i] == 0 &&
+                metricsObject["downloads"][i] == 0
+              ) {
+                metricsObject["months"].splice(i, 1);
+                metricsObject["citations"].splice(i, 1);
+                metricsObject["views"].splice(i, 1);
+                metricsObject["downloads"].splice(i, 1);
+
+                // if country facet was part of the request; update object;
+                if (metricsObject["country"] != null) {
+                  metricsObject["country"].splice(i, 1);
                 }
-            }
 
-            return metricsObject;
-        },
+                // modified array size; decrement the counter;
+                i--;
+              } else {
+                break;
+              }
+            }
+          }
+        }
 
-    });
-    return Metrics;
+        return metricsObject;
+      },
+    },
+  );
+  return Metrics;
 });
 
diff --git a/docs/docs/src_js_models_PackageModel.js.html b/docs/docs/src_js_models_PackageModel.js.html index 4448a815c..b2049ed05 100644 --- a/docs/docs/src_js_models_PackageModel.js.html +++ b/docs/docs/src_js_models_PackageModel.js.html @@ -44,1484 +44,1858 @@

Source: src/js/models/PackageModel.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'uuid', 'md5', 'rdflib', 'models/SolrResult'],
-	function($, _, Backbone, uuid, md5, rdf, SolrResult) {
-
-	// Package Model
-	// ------------------
-	var PackageModel = Backbone.Model.extend(
-    /** @lends PackageModel.prototype */{
-		// This model contains information about a package/resource map
-		defaults: function(){
-			return {
-				id: null, //The id of the resource map/package itself
-				url: null, //the URL to retrieve this package
-				memberId: null, //An id of a member of the data package
-				indexDoc: null, //A SolrResult object representation of the resource map
-				size: 0, //The number of items aggregated in this package
-				totalSize: null,
-				formattedSize: "",
-				formatId: null,
-				obsoletedBy: null,
-				obsoletes: null,
-				read_count_i: null,
-				isPublic: true,
-				members: [],
-				memberIds: [],
-				sources: [],
-				derivations: [],
-				provenanceFlag: null,
-				sourcePackages: [],
-				derivationPackages: [],
-				sourceDocs: [],
-				derivationDocs: [],
-				relatedModels: [], //A condensed list of all SolrResult models related to this package in some way
-				parentPackageMetadata: null,
-				//If true, when the member objects are retrieved, archived content will be included
-				getArchivedMembers: false,
-			}
-		},
-
-		//Define the namespaces
-        namespaces: {
-			RDF:     "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
-			FOAF:    "http://xmlns.com/foaf/0.1/",
-			OWL:     "http://www.w3.org/2002/07/owl#",
-			DC:      "http://purl.org/dc/elements/1.1/",
-			ORE:     "http://www.openarchives.org/ore/terms/",
-			DCTERMS: "http://purl.org/dc/terms/",
-			CITO:    "http://purl.org/spar/cito/",
-			XML:     "http://www.w3.org/2001/XMLSchema#"
-		},
-
-		sysMetaNodeMap: {
-			accesspolicy: "accessPolicy",
-			accessrule: "accessRule",
-			authoritativemembernode: "authoritativeMemberNode",
-			dateuploaded: "dateUploaded",
-			datesysmetadatamodified: "dateSysMetadataModified",
-			dateuploaded: "dateUploaded",
-			formatid: "formatId",
-			nodereference: "nodeReference",
-			obsoletedby: "obsoletedBy",
-			originmembernode: "originMemberNode",
-			replicamembernode: "replicaMemberNode",
-			replicapolicy: "replicaPolicy",
-			replicationstatus: "replicationStatus",
-			replicaverified: "replicaVerified",
-			rightsholder: "rightsHolder",
-			serialversion: "serialVersion"
-		},
-
-		complete: false,
-
-		pending: false,
-
-		type: "Package",
-
-		// The RDF graph representing this data package
-        dataPackageGraph: null,
-
-		initialize: function(options){
-			this.setURL();
-
-			// Create an initial RDF graph
-            this.dataPackageGraph = rdf.graph();
-		},
-
-		setURL: function(){
-			if(MetacatUI.appModel.get("packageServiceUrl"))
-				this.set("url", MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(this.get("id")));
-		},
-
-		/*
-		 * Set the URL for fetch
-		 */
-		url: function(){
-			return MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(this.get("id"));
-		},
-
-		/* Retrieve the id of the resource map/package that this id belongs to */
-		getMembersByMemberID: function(id){
-			this.pending = true;
-
-			if((typeof id === "undefined") || !id) var id = this.memberId;
-
-			var model = this;
-
-			//Get the id of the resource map for this member
-			var provFlList = MetacatUI.appSearchModel.getProvFlList() + "prov_instanceOfClass,";
-			var query = 'fl=resourceMap,fileName,read:read_count_i,obsoletedBy,size,formatType,formatId,id,datasource,title,origin,pubDate,dateUploaded,isPublic,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription,' + provFlList +
-						'&rows=1' +
-						'&q=id:%22' + encodeURIComponent(id) + '%22' +
-						'&wt=json';
-
-
-			var requestSettings = {
-				url: MetacatUI.appModel.get("queryServiceUrl") + query,
-				success: function(data, textStatus, xhr) {
-					//There should be only one response since we searched by id
-					if(typeof data.response.docs !== "undefined"){
-						var doc = data.response.docs[0];
-
-						//Is this document a resource map itself?
-						if(doc.formatId == "http://www.openarchives.org/ore/terms"){
-							model.set("id", doc.id); //this is the package model ID
-							model.set("members", new Array()); //Reset the member list
-							model.getMembers();
-						}
-						//If there is no resource map, then this is the only document to in this package
-						else if((typeof doc.resourceMap === "undefined") || !doc.resourceMap){
-							model.set('id', null);
-							model.set('memberIds', new Array(doc.id));
-							model.set('members', [new SolrResult(doc)]);
-							model.trigger("change:members");
-							model.flagComplete();
-						}
-						else{
-							model.set('id', doc.resourceMap[0]);
-							model.getMembers();
-						}
-					}
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		/* Get all the members of a resource map/package based on the id attribute of this model.
-		 * Create a SolrResult model for each member and save it in the members[] attribute of this model. */
-		getMembers: function(options){
-			this.pending = true;
-
-			var model   = this,
-				members = [],
-				pids    = []; //Keep track of each object pid
-
-			//*** Find all the files that are a part of this resource map and the resource map itself
-			var provFlList = MetacatUI.appSearchModel.getProvFlList();
-			var query = 'fl=resourceMap,fileName,obsoletes,obsoletedBy,size,formatType,formatId,id,datasource,' +
-							'rightsHolder,dateUploaded,archived,title,origin,prov_instanceOfClass,isDocumentedBy,isPublic' +
-  						'&rows=1000' +
-  						'&q=%28resourceMap:%22' + encodeURIComponent(this.id) + '%22%20OR%20id:%22' + encodeURIComponent(this.id) + '%22%29' +
-  						'&wt=json';
-
-			if( this.get("getArchivedMembers") ){
-				query += "&archived=archived:*";
-			}
-
-			var requestSettings = {
-				url: MetacatUI.appModel.get("queryServiceUrl") + query,
-				success: function(data, textStatus, xhr) {
-
-					//Separate the resource maps from the data/metadata objects
-					_.each(data.response.docs, function(doc){
-						if(doc.id == model.get("id")){
-							model.set("indexDoc", doc);
-							model.set(doc);
-							if(model.get("resourceMap") && (options && options.getParentMetadata))
-								model.getParentMetadata();
-						}
-						else{
-							pids.push(doc.id);
-
-							if(doc.formatType == "RESOURCE"){
-								var newPckg = new PackageModel(doc);
-								newPckg.set("parentPackage", model);
-								members.push(newPckg);
-							}
-							else
-								members.push(new SolrResult(doc));
-						}
-					});
-
-					model.set('memberIds', _.uniq(pids));
-					model.set('members', members);
-
-					if(model.getNestedPackages().length > 0)
-						model.createNestedPackages();
-					else
-						model.flagComplete();
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-
-			return this;
-		},
-
-		/*
-		 * Send custom options to the Backbone.Model.fetch() function
-		 */
-		fetch: function(options){
-			if(!options) var options = {};
-
-			var fetchOptions = _.extend({dataType: "text"}, options);
-
-            //Add the authorization options
-            fetchOptions = _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings());
-
-
-            return Backbone.Model.prototype.fetch.call(this, fetchOptions);
-		},
-
-		/*
-         * Deserialize a Package from OAI-ORE RDF XML
-         */
-        parse: function(response, options) {
-
-            //Save the raw XML in case it needs to be used later
-            this.set("objectXML", $.parseHTML(response));
-
-            //Define the namespaces
-            var RDF =     rdf.Namespace(this.namespaces.RDF),
-                FOAF =    rdf.Namespace(this.namespaces.FOAF),
-                OWL =     rdf.Namespace(this.namespaces.OWL),
-                DC =      rdf.Namespace(this.namespaces.DC),
-                ORE =     rdf.Namespace(this.namespaces.ORE),
-                DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
-                CITO =    rdf.Namespace(this.namespaces.CITO);
-
-            var memberStatements = [],
-                memberURIParts,
-                memberPIDStr,
-                memberPID,
-                memberModel,
-                models = []; // the models returned by parse()
-
-            try {
-                rdf.parse(response, this.dataPackageGraph, MetacatUI.appModel.get("objectServiceUrl") + (encodeURIComponent(this.id) || encodeURIComponent(this.seriesid)), 'application/rdf+xml');
-
-                // List the package members
-                memberStatements = this.dataPackageGraph.statementsMatching(
-                    undefined, ORE('aggregates'), undefined, undefined);
-
-                var memberPIDs = [],
-                	members = [],
-                	currentMembers = this.get("members"),
-                	model = this;
-
-                // Get system metadata for each member to eval the formatId
-                _.each(memberStatements, function(memberStatement){
-                    memberURIParts = memberStatement.object.value.split('/');
-                    memberPIDStr = _.last(memberURIParts);
-                    memberPID = decodeURIComponent(memberPIDStr);
-
-                    if ( memberPID ){
-                    	memberPIDs.push(memberPID);
-
-                    	//Get the current model from the member list, if it exists
-                    	var existingModel = _.find(currentMembers, function(m){
-                    		return m.get("id") == decodeURIComponent(memberPID);
-                    	});
-
-                    	//Add the existing model to the new member list
-                    	if(existingModel){
-                    		members.push(existingModel);
-                    	}
-                    	//Or create a new SolrResult model
-                    	else{
-                    		members.push(new SolrResult({
-                    			id: decodeURIComponent(memberPID)
-                    		}));
-                    	}
-                    }
-
-                }, this);
-
-                //Get the documents relationships
-                var documentedByStatements = this.dataPackageGraph.statementsMatching(
-                        undefined, CITO('isDocumentedBy'), undefined, undefined),
-                    metadataPids = [];
-
-                _.each(documentedByStatements, function(statement){
-                	//Get the data object that is documentedBy metadata
-                	var dataPid = decodeURIComponent(_.last(statement.subject.value.split('/'))),
-                		dataObj = _.find(members, function(m){ return (m.get("id") == dataPid) }),
-                		metadataPid = _.last(statement.object.value.split('/'));
-
-                	//Save this as a metadata model
-                	metadataPids.push(metadataPid);
-
-                	//Set the isDocumentedBy field
-                	var isDocBy = dataObj.get("isDocumentedBy");
-                	if(isDocBy && Array.isArray(isDocBy)) isDocBy.push(metadataPid);
-                	else if(isDocBy && !Array.isArray(isDocBy)) isDocBy = [isDocBy, metadataPid];
-                	else isDocBy = [metadataPid];
-
-                	dataObj.set("isDocumentedBy", isDocBy);
-                }, this);
-
-                //Get the metadata models and mark them as metadata
-                var metadataModels = _.filter(members, function(m){ return _.contains(metadataPids, m.get("id")) });
-                _.invoke(metadataModels, "set", "formatType", "METADATA");
-
-                //Keep the pids in the collection for easy access later
-                this.set("memberIds", memberPIDs);
-                this.set("members", members);
-
-            } catch (error) {
-                console.log(error);
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "uuid",
+  "md5",
+  "rdflib",
+  "models/SolrResult",
+], function ($, _, Backbone, uuid, md5, rdf, SolrResult) {
+  // Package Model
+  // ------------------
+  var PackageModel = Backbone.Model.extend(
+    /** @lends PackageModel.prototype */ {
+      // This model contains information about a package/resource map
+      defaults: function () {
+        return {
+          id: null, //The id of the resource map/package itself
+          url: null, //the URL to retrieve this package
+          memberId: null, //An id of a member of the data package
+          indexDoc: null, //A SolrResult object representation of the resource map
+          size: 0, //The number of items aggregated in this package
+          totalSize: null,
+          formattedSize: "",
+          formatId: null,
+          obsoletedBy: null,
+          obsoletes: null,
+          read_count_i: null,
+          isPublic: true,
+          members: [],
+          memberIds: [],
+          sources: [],
+          derivations: [],
+          provenanceFlag: null,
+          sourcePackages: [],
+          derivationPackages: [],
+          sourceDocs: [],
+          derivationDocs: [],
+          relatedModels: [], //A condensed list of all SolrResult models related to this package in some way
+          parentPackageMetadata: null,
+          //If true, when the member objects are retrieved, archived content will be included
+          getArchivedMembers: false,
+        };
+      },
+
+      //Define the namespaces
+      namespaces: {
+        RDF: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+        FOAF: "http://xmlns.com/foaf/0.1/",
+        OWL: "http://www.w3.org/2002/07/owl#",
+        DC: "http://purl.org/dc/elements/1.1/",
+        ORE: "http://www.openarchives.org/ore/terms/",
+        DCTERMS: "http://purl.org/dc/terms/",
+        CITO: "http://purl.org/spar/cito/",
+        XML: "http://www.w3.org/2001/XMLSchema#",
+      },
+
+      sysMetaNodeMap: {
+        accesspolicy: "accessPolicy",
+        accessrule: "accessRule",
+        authoritativemembernode: "authoritativeMemberNode",
+        dateuploaded: "dateUploaded",
+        datesysmetadatamodified: "dateSysMetadataModified",
+        dateuploaded: "dateUploaded",
+        formatid: "formatId",
+        nodereference: "nodeReference",
+        obsoletedby: "obsoletedBy",
+        originmembernode: "originMemberNode",
+        replicamembernode: "replicaMemberNode",
+        replicapolicy: "replicaPolicy",
+        replicationstatus: "replicationStatus",
+        replicaverified: "replicaVerified",
+        rightsholder: "rightsHolder",
+        serialversion: "serialVersion",
+      },
+
+      complete: false,
+
+      pending: false,
+
+      type: "Package",
+
+      // The RDF graph representing this data package
+      dataPackageGraph: null,
+
+      initialize: function (options) {
+        this.setURL();
+
+        // Create an initial RDF graph
+        this.dataPackageGraph = rdf.graph();
+      },
+
+      setURL: function () {
+        if (MetacatUI.appModel.get("packageServiceUrl"))
+          this.set(
+            "url",
+            MetacatUI.appModel.get("packageServiceUrl") +
+              encodeURIComponent(this.get("id")),
+          );
+      },
+
+      /*
+       * Set the URL for fetch
+       */
+      url: function () {
+        return (
+          MetacatUI.appModel.get("objectServiceUrl") +
+          encodeURIComponent(this.get("id"))
+        );
+      },
+
+      /* Retrieve the id of the resource map/package that this id belongs to */
+      getMembersByMemberID: function (id) {
+        this.pending = true;
+
+        if (typeof id === "undefined" || !id) var id = this.memberId;
+
+        var model = this;
+
+        //Get the id of the resource map for this member
+        var provFlList =
+          MetacatUI.appSearchModel.getProvFlList() + "prov_instanceOfClass,";
+        var query =
+          "fl=resourceMap,fileName,read:read_count_i,obsoletedBy,size,formatType,formatId,id,datasource,title,origin,pubDate,dateUploaded,isPublic,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription," +
+          provFlList +
+          "&rows=1" +
+          "&q=id:%22" +
+          encodeURIComponent(id) +
+          "%22" +
+          "&wt=json";
+
+        var requestSettings = {
+          url: MetacatUI.appModel.get("queryServiceUrl") + query,
+          success: function (data, textStatus, xhr) {
+            //There should be only one response since we searched by id
+            if (typeof data.response.docs !== "undefined") {
+              var doc = data.response.docs[0];
+
+              //Is this document a resource map itself?
+              if (doc.formatId == "http://www.openarchives.org/ore/terms") {
+                model.set("id", doc.id); //this is the package model ID
+                model.set("members", new Array()); //Reset the member list
+                model.getMembers();
+              }
+              //If there is no resource map, then this is the only document to in this package
+              else if (
+                typeof doc.resourceMap === "undefined" ||
+                !doc.resourceMap
+              ) {
+                model.set("id", null);
+                model.set("memberIds", new Array(doc.id));
+                model.set("members", [new SolrResult(doc)]);
+                model.trigger("change:members");
+                model.flagComplete();
+              } else {
+                model.set("id", doc.resourceMap[0]);
+                model.getMembers();
+              }
             }
-            return models;
-        },
-
-
-		/*
-		 * Overwrite the Backbone.Model.save() function to set custom options
-		 */
-		save: function(attrs, options){
-			if(!options) var options = {};
-
-			//Get the system metadata first
-			if(!this.get("hasSystemMetadata")){
-				var model = this;
-				var requestSettings = {
-						url: MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(this.get("id")),
-						success: function(response){
-							model.parseSysMeta(response);
-
-							model.set("hasSystemMetadata", true);
-							model.save.call(model, null, options);
-						},
-						dataType: "text"
-				}
-				$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-				return;
-			}
-
-			//Create a new pid if we are updating the object
-			if(!options.sysMetaOnly){
-				//Set a new id
-				var oldPid = this.get("id");
-				this.set("oldPid", oldPid);
-				this.set("id", "urn:uuid:" + uuid.v4());
-				this.set("obsoletes", oldPid);
-				this.set("obsoletedBy", null);
-				this.set("archived", false);
-			}
-
-			//Create the system metadata
-			var sysMetaXML = this.serializeSysMeta();
-
-			//Send the new pid, old pid, and system metadata
-			var xmlBlob = new Blob([sysMetaXML], {type : 'application/xml'});
-			var formData = new FormData();
-			formData.append("sysmeta", xmlBlob, "sysmeta");
-
-			//Let's try updating the system metadata for now
-			if(options.sysMetaOnly){
-				formData.append("pid", this.get("id"));
-
-				var requestSettings = {
-						url: MetacatUI.appModel.get("metaServiceUrl"),
-						type: "PUT",
-						cache: false,
-					    contentType: false,
-					    processData: false,
-						data: formData,
-						success: function(response){
-						},
-						error: function(data){
-							console.log("error updating system metadata");
-						}
-				}
-				$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-			}
-			else{
-				//Add the ids to the form data
-				formData.append("newPid", this.get("id"));
-				formData.append("pid", oldPid);
-
-				//Create the resource map XML
-				var mapXML = this.serialize();
-				var mapBlob = new Blob([mapXML], {type : 'application/xml'});
-				formData.append("object", mapBlob);
-
-				//Get the size of the new resource map
-				this.set("size", mapBlob.size);
-
-				//Get the new checksum of the resource map
-				var checksum = md5(mapXML);
-				this.set("checksum", checksum);
-
-				var requestSettings = {
-						url: MetacatUI.appModel.get("objectServiceUrl"),
-						type: "PUT",
-						cache: false,
-						contentType: false,
-						processData: false,
-						data: formData,
-						success: function(response){
-						},
-						error: function(data){
-							console.log("error udpating object");
-						}
-				}
-				$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-			}
-		},
-
-		parseSysMeta: function(response){
-        	this.set("sysMetaXML", $.parseHTML(response));
-
-    		var responseDoc = $.parseHTML(response),
-    			systemMetadata,
-    			prependXML = "",
-    			appendXML = "";
-
-    		for(var i=0; i<responseDoc.length; i++){
-    			if((responseDoc[i].nodeType == 1) && (responseDoc[i].localName.indexOf("systemmetadata") > -1))
-    				systemMetadata = responseDoc[i];
-    		}
-
-    		//Parse the XML to JSON
-    		var sysMetaValues = this.toJson(systemMetadata),
-    			camelCasedValues = {};
-    		//Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code
-    		_.each(Object.keys(sysMetaValues), function(key){
-    			camelCasedValues[this.sysMetaNodeMap[key]] = sysMetaValues[key];
-    		}, this);
-
-    		//Set the values on the model
-    		this.set(camelCasedValues);
-        },
-
-        serialize: function(){
-        	//Create an RDF serializer
-        	var serializer = rdf.Serializer();
-        	serializer.store = this.dataPackageGraph;
-
-        	//Define the namespaces
-            var ORE  = rdf.Namespace(this.namespaces.ORE),
-            	CITO = rdf.Namespace(this.namespaces.CITO);
-
-        	//Get the pid of this package - depends on whether we are updating or creating a resource map
-            var pid = this.get("id"),
-            	oldPid = this.get("oldPid"),
-            	updating = oldPid? true : false;
-
-            //Update the pids in the RDF graph only if we are updating the resource map with a new pid
-            if(updating){
-
-            	//Find the identifier statement in the resource map
-				var idNode =  rdf.lit(oldPid),
-					idStatement = this.dataPackageGraph.statementsMatching(undefined, undefined, idNode);
-
-				//Get the CN Resolve Service base URL from the resource map (mostly important in dev environments where it will not always be cn.dataone.org)
-				var	cnResolveUrl = idStatement[0].subject.value.substring(0, idStatement[0].subject.value.indexOf(oldPid));
-				this.dataPackageGraph.cnResolveUrl = cnResolveUrl;
-
-				//Create variations of the resource map ID using the resolve URL so we can always find it in the RDF graph
-	            var	oldPidVariations = [oldPid, encodeURIComponent(oldPid), cnResolveUrl+ encodeURIComponent(oldPid)];
-
-            	//Get all the isAggregatedBy statements
-	            var aggregationNode =  rdf.sym(cnResolveUrl + encodeURIComponent(oldPid) + "#aggregation"),
-	            	aggByStatements = this.dataPackageGraph.statementsMatching(undefined, ORE("isAggregatedBy"));
-
-	            //Using the isAggregatedBy statements, find all the DataONE object ids in the RDF graph
-	            var idsFromXML = [];
-	            _.each(aggByStatements, function(statement){
-
-	            	//Check if the resource map ID is the old existing id, so we don't collect ids that are not about this resource map
-	            	if(_.find(oldPidVariations, function(oldPidV){ return (oldPidV + "#aggregation" == statement.object.value) })){
-	            		var statementID = statement.subject.value;
-	            		idsFromXML.push(statementID);
-
-	            		//Add variations of the ID so we make sure we account for all the ways they exist in the RDF XML
-	            		if(statementID.indexOf(cnResolveUrl) > -1)
-	            			idsFromXML.push(statementID.substring(statementID.lastIndexOf("/") + 1));
-		            	else
-		            		idsFromXML.push(cnResolveUrl + encodeURIComponent(statementID));
-	            	}
-
-	            }, this);
-
-	            //Get all the ids from this model
-            	var idsFromModel = _.invoke(this.get("members"), "get", "id");
-
-		        //Find the difference between the model IDs and the XML IDs to get a list of added members
-	            var addedIds  = _.without(_.difference(idsFromModel, idsFromXML), oldPidVariations);
-	            //Create variations of all these ids too
-	            var allMemberIds = idsFromModel;
-	            _.each(idsFromModel, function(id){
-	            	allMemberIds.push(cnResolveUrl + encodeURIComponent(id));
-	           	});
-
-            	//Remove any other isAggregatedBy statements that are not listed as members of this model
-	            _.each(aggByStatements, function(statement){
-	            	if(!_.contains(allMemberIds, statement.subject.value))
-	            		this.removeFromAggregation(statement.subject.value);
-	            	else if(_.find(oldPidVariations, function(oldPidV){ return (oldPidV + "#aggregation" == statement.object.value) }))
-	            		statement.object.value = cnResolveUrl+ encodeURIComponent(pid) + "#aggregation";
-	            }, this);
-
-            	//Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID
-	            var aggregationSubjStatements = this.dataPackageGraph.statementsMatching(aggregationNode);
-	            _.each(aggregationSubjStatements, function(statement){
-	            	statement.subject.value = cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
-	            });
-
-            	//Change all the statements in the RDF where the aggregation is the object, to reflect the new resource map ID
-	            var aggregationObjStatements = this.dataPackageGraph.statementsMatching(undefined, undefined, aggregationNode);
-	            _.each(aggregationObjStatements, function(statement){
-	            	statement.object.value = cnResolveUrl+ encodeURIComponent(pid) + "#aggregation";
-	            });
-
-				//Change all the resource map subject nodes in the RDF graph
-				var rMapNode =  rdf.sym(cnResolveUrl + encodeURIComponent(oldPid));
-			    var rMapStatements = this.dataPackageGraph.statementsMatching(rMapNode);
-			    _.each(rMapStatements, function(statement){
-			    	statement.subject.value = cnResolveUrl + encodeURIComponent(pid);
-			    });
-
-			    //Change the idDescribedBy statement
-			    var isDescribedByStatements = this.dataPackageGraph.statementsMatching(undefined, ORE("isDescribedBy"), rdf.sym(oldPid));
-			    if(isDescribedByStatements[0])
-			    	isDescribedByStatements[0].object.value = pid;
-
-            	//Add nodes for new package members
-            	_.each(addedIds, function(id){
-            		this.addToAggregation(id);
-            	}, this);
-
-			    //Change all the resource map identifier literal node in the RDF graph
-				if(idStatement[0]) idStatement[0].object.value = pid;
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      /* Get all the members of a resource map/package based on the id attribute of this model.
+       * Create a SolrResult model for each member and save it in the members[] attribute of this model. */
+      getMembers: function (options) {
+        this.pending = true;
+
+        var model = this,
+          members = [],
+          pids = []; //Keep track of each object pid
+
+        //*** Find all the files that are a part of this resource map and the resource map itself
+        var provFlList = MetacatUI.appSearchModel.getProvFlList();
+        var query =
+          "fl=resourceMap,fileName,obsoletes,obsoletedBy,size,formatType,formatId,id,datasource," +
+          "rightsHolder,dateUploaded,archived,title,origin,prov_instanceOfClass,isDocumentedBy,isPublic" +
+          "&rows=1000" +
+          "&q=%28resourceMap:%22" +
+          encodeURIComponent(this.id) +
+          "%22%20OR%20id:%22" +
+          encodeURIComponent(this.id) +
+          "%22%29" +
+          "&wt=json";
+
+        if (this.get("getArchivedMembers")) {
+          query += "&archived=archived:*";
+        }
+
+        var requestSettings = {
+          url: MetacatUI.appModel.get("queryServiceUrl") + query,
+          success: function (data, textStatus, xhr) {
+            //Separate the resource maps from the data/metadata objects
+            _.each(data.response.docs, function (doc) {
+              if (doc.id == model.get("id")) {
+                model.set("indexDoc", doc);
+                model.set(doc);
+                if (
+                  model.get("resourceMap") &&
+                  options &&
+                  options.getParentMetadata
+                )
+                  model.getParentMetadata();
+              } else {
+                pids.push(doc.id);
+
+                if (doc.formatType == "RESOURCE") {
+                  var newPckg = new PackageModel(doc);
+                  newPckg.set("parentPackage", model);
+                  members.push(newPckg);
+                } else members.push(new SolrResult(doc));
+              }
+            });
+
+            model.set("memberIds", _.uniq(pids));
+            model.set("members", members);
+
+            if (model.getNestedPackages().length > 0)
+              model.createNestedPackages();
+            else model.flagComplete();
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+
+        return this;
+      },
+
+      /*
+       * Send custom options to the Backbone.Model.fetch() function
+       */
+      fetch: function (options) {
+        if (!options) var options = {};
+
+        var fetchOptions = _.extend({ dataType: "text" }, options);
+
+        //Add the authorization options
+        fetchOptions = _.extend(
+          fetchOptions,
+          MetacatUI.appUserModel.createAjaxSettings(),
+        );
+
+        return Backbone.Model.prototype.fetch.call(this, fetchOptions);
+      },
+
+      /*
+       * Deserialize a Package from OAI-ORE RDF XML
+       */
+      parse: function (response, options) {
+        //Save the raw XML in case it needs to be used later
+        this.set("objectXML", $.parseHTML(response));
+
+        //Define the namespaces
+        var RDF = rdf.Namespace(this.namespaces.RDF),
+          FOAF = rdf.Namespace(this.namespaces.FOAF),
+          OWL = rdf.Namespace(this.namespaces.OWL),
+          DC = rdf.Namespace(this.namespaces.DC),
+          ORE = rdf.Namespace(this.namespaces.ORE),
+          DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
+          CITO = rdf.Namespace(this.namespaces.CITO);
+
+        var memberStatements = [],
+          memberURIParts,
+          memberPIDStr,
+          memberPID,
+          memberModel,
+          models = []; // the models returned by parse()
+
+        try {
+          rdf.parse(
+            response,
+            this.dataPackageGraph,
+            MetacatUI.appModel.get("objectServiceUrl") +
+              (encodeURIComponent(this.id) ||
+                encodeURIComponent(this.seriesid)),
+            "application/rdf+xml",
+          );
+
+          // List the package members
+          memberStatements = this.dataPackageGraph.statementsMatching(
+            undefined,
+            ORE("aggregates"),
+            undefined,
+            undefined,
+          );
+
+          var memberPIDs = [],
+            members = [],
+            currentMembers = this.get("members"),
+            model = this;
+
+          // Get system metadata for each member to eval the formatId
+          _.each(
+            memberStatements,
+            function (memberStatement) {
+              memberURIParts = memberStatement.object.value.split("/");
+              memberPIDStr = _.last(memberURIParts);
+              memberPID = decodeURIComponent(memberPIDStr);
+
+              if (memberPID) {
+                memberPIDs.push(memberPID);
+
+                //Get the current model from the member list, if it exists
+                var existingModel = _.find(currentMembers, function (m) {
+                  return m.get("id") == decodeURIComponent(memberPID);
+                });
+
+                //Add the existing model to the new member list
+                if (existingModel) {
+                  members.push(existingModel);
+                }
+                //Or create a new SolrResult model
+                else {
+                  members.push(
+                    new SolrResult({
+                      id: decodeURIComponent(memberPID),
+                    }),
+                  );
+                }
+              }
+            },
+            this,
+          );
+
+          //Get the documents relationships
+          var documentedByStatements = this.dataPackageGraph.statementsMatching(
+              undefined,
+              CITO("isDocumentedBy"),
+              undefined,
+              undefined,
+            ),
+            metadataPids = [];
+
+          _.each(
+            documentedByStatements,
+            function (statement) {
+              //Get the data object that is documentedBy metadata
+              var dataPid = decodeURIComponent(
+                  _.last(statement.subject.value.split("/")),
+                ),
+                dataObj = _.find(members, function (m) {
+                  return m.get("id") == dataPid;
+                }),
+                metadataPid = _.last(statement.object.value.split("/"));
+
+              //Save this as a metadata model
+              metadataPids.push(metadataPid);
+
+              //Set the isDocumentedBy field
+              var isDocBy = dataObj.get("isDocumentedBy");
+              if (isDocBy && Array.isArray(isDocBy)) isDocBy.push(metadataPid);
+              else if (isDocBy && !Array.isArray(isDocBy))
+                isDocBy = [isDocBy, metadataPid];
+              else isDocBy = [metadataPid];
+
+              dataObj.set("isDocumentedBy", isDocBy);
+            },
+            this,
+          );
+
+          //Get the metadata models and mark them as metadata
+          var metadataModels = _.filter(members, function (m) {
+            return _.contains(metadataPids, m.get("id"));
+          });
+          _.invoke(metadataModels, "set", "formatType", "METADATA");
+
+          //Keep the pids in the collection for easy access later
+          this.set("memberIds", memberPIDs);
+          this.set("members", members);
+        } catch (error) {
+          console.log(error);
+        }
+        return models;
+      },
+
+      /*
+       * Overwrite the Backbone.Model.save() function to set custom options
+       */
+      save: function (attrs, options) {
+        if (!options) var options = {};
+
+        //Get the system metadata first
+        if (!this.get("hasSystemMetadata")) {
+          var model = this;
+          var requestSettings = {
+            url:
+              MetacatUI.appModel.get("metaServiceUrl") +
+              encodeURIComponent(this.get("id")),
+            success: function (response) {
+              model.parseSysMeta(response);
+
+              model.set("hasSystemMetadata", true);
+              model.save.call(model, null, options);
+            },
+            dataType: "text",
+          };
+          $.ajax(
+            _.extend(
+              requestSettings,
+              MetacatUI.appUserModel.createAjaxSettings(),
+            ),
+          );
+          return;
+        }
+
+        //Create a new pid if we are updating the object
+        if (!options.sysMetaOnly) {
+          //Set a new id
+          var oldPid = this.get("id");
+          this.set("oldPid", oldPid);
+          this.set("id", "urn:uuid:" + uuid.v4());
+          this.set("obsoletes", oldPid);
+          this.set("obsoletedBy", null);
+          this.set("archived", false);
+        }
+
+        //Create the system metadata
+        var sysMetaXML = this.serializeSysMeta();
+
+        //Send the new pid, old pid, and system metadata
+        var xmlBlob = new Blob([sysMetaXML], { type: "application/xml" });
+        var formData = new FormData();
+        formData.append("sysmeta", xmlBlob, "sysmeta");
+
+        //Let's try updating the system metadata for now
+        if (options.sysMetaOnly) {
+          formData.append("pid", this.get("id"));
+
+          var requestSettings = {
+            url: MetacatUI.appModel.get("metaServiceUrl"),
+            type: "PUT",
+            cache: false,
+            contentType: false,
+            processData: false,
+            data: formData,
+            success: function (response) {},
+            error: function (data) {
+              console.log("error updating system metadata");
+            },
+          };
+          $.ajax(
+            _.extend(
+              requestSettings,
+              MetacatUI.appUserModel.createAjaxSettings(),
+            ),
+          );
+        } else {
+          //Add the ids to the form data
+          formData.append("newPid", this.get("id"));
+          formData.append("pid", oldPid);
+
+          //Create the resource map XML
+          var mapXML = this.serialize();
+          var mapBlob = new Blob([mapXML], { type: "application/xml" });
+          formData.append("object", mapBlob);
+
+          //Get the size of the new resource map
+          this.set("size", mapBlob.size);
+
+          //Get the new checksum of the resource map
+          var checksum = md5(mapXML);
+          this.set("checksum", checksum);
+
+          var requestSettings = {
+            url: MetacatUI.appModel.get("objectServiceUrl"),
+            type: "PUT",
+            cache: false,
+            contentType: false,
+            processData: false,
+            data: formData,
+            success: function (response) {},
+            error: function (data) {
+              console.log("error udpating object");
+            },
+          };
+          $.ajax(
+            _.extend(
+              requestSettings,
+              MetacatUI.appUserModel.createAjaxSettings(),
+            ),
+          );
+        }
+      },
+
+      parseSysMeta: function (response) {
+        this.set("sysMetaXML", $.parseHTML(response));
+
+        var responseDoc = $.parseHTML(response),
+          systemMetadata,
+          prependXML = "",
+          appendXML = "";
+
+        for (var i = 0; i < responseDoc.length; i++) {
+          if (
+            responseDoc[i].nodeType == 1 &&
+            responseDoc[i].localName.indexOf("systemmetadata") > -1
+          )
+            systemMetadata = responseDoc[i];
+        }
 
+        //Parse the XML to JSON
+        var sysMetaValues = this.toJson(systemMetadata),
+          camelCasedValues = {};
+        //Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code
+        _.each(
+          Object.keys(sysMetaValues),
+          function (key) {
+            camelCasedValues[this.sysMetaNodeMap[key]] = sysMetaValues[key];
+          },
+          this,
+        );
+
+        //Set the values on the model
+        this.set(camelCasedValues);
+      },
+
+      serialize: function () {
+        //Create an RDF serializer
+        var serializer = rdf.Serializer();
+        serializer.store = this.dataPackageGraph;
+
+        //Define the namespaces
+        var ORE = rdf.Namespace(this.namespaces.ORE),
+          CITO = rdf.Namespace(this.namespaces.CITO);
+
+        //Get the pid of this package - depends on whether we are updating or creating a resource map
+        var pid = this.get("id"),
+          oldPid = this.get("oldPid"),
+          updating = oldPid ? true : false;
+
+        //Update the pids in the RDF graph only if we are updating the resource map with a new pid
+        if (updating) {
+          //Find the identifier statement in the resource map
+          var idNode = rdf.lit(oldPid),
+            idStatement = this.dataPackageGraph.statementsMatching(
+              undefined,
+              undefined,
+              idNode,
+            );
+
+          //Get the CN Resolve Service base URL from the resource map (mostly important in dev environments where it will not always be cn.dataone.org)
+          var cnResolveUrl = idStatement[0].subject.value.substring(
+            0,
+            idStatement[0].subject.value.indexOf(oldPid),
+          );
+          this.dataPackageGraph.cnResolveUrl = cnResolveUrl;
+
+          //Create variations of the resource map ID using the resolve URL so we can always find it in the RDF graph
+          var oldPidVariations = [
+            oldPid,
+            encodeURIComponent(oldPid),
+            cnResolveUrl + encodeURIComponent(oldPid),
+          ];
+
+          //Get all the isAggregatedBy statements
+          var aggregationNode = rdf.sym(
+              cnResolveUrl + encodeURIComponent(oldPid) + "#aggregation",
+            ),
+            aggByStatements = this.dataPackageGraph.statementsMatching(
+              undefined,
+              ORE("isAggregatedBy"),
+            );
+
+          //Using the isAggregatedBy statements, find all the DataONE object ids in the RDF graph
+          var idsFromXML = [];
+          _.each(
+            aggByStatements,
+            function (statement) {
+              //Check if the resource map ID is the old existing id, so we don't collect ids that are not about this resource map
+              if (
+                _.find(oldPidVariations, function (oldPidV) {
+                  return oldPidV + "#aggregation" == statement.object.value;
+                })
+              ) {
+                var statementID = statement.subject.value;
+                idsFromXML.push(statementID);
+
+                //Add variations of the ID so we make sure we account for all the ways they exist in the RDF XML
+                if (statementID.indexOf(cnResolveUrl) > -1)
+                  idsFromXML.push(
+                    statementID.substring(statementID.lastIndexOf("/") + 1),
+                  );
+                else
+                  idsFromXML.push(
+                    cnResolveUrl + encodeURIComponent(statementID),
+                  );
+              }
+            },
+            this,
+          );
+
+          //Get all the ids from this model
+          var idsFromModel = _.invoke(this.get("members"), "get", "id");
+
+          //Find the difference between the model IDs and the XML IDs to get a list of added members
+          var addedIds = _.without(
+            _.difference(idsFromModel, idsFromXML),
+            oldPidVariations,
+          );
+          //Create variations of all these ids too
+          var allMemberIds = idsFromModel;
+          _.each(idsFromModel, function (id) {
+            allMemberIds.push(cnResolveUrl + encodeURIComponent(id));
+          });
+
+          //Remove any other isAggregatedBy statements that are not listed as members of this model
+          _.each(
+            aggByStatements,
+            function (statement) {
+              if (!_.contains(allMemberIds, statement.subject.value))
+                this.removeFromAggregation(statement.subject.value);
+              else if (
+                _.find(oldPidVariations, function (oldPidV) {
+                  return oldPidV + "#aggregation" == statement.object.value;
+                })
+              )
+                statement.object.value =
+                  cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
+            },
+            this,
+          );
+
+          //Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID
+          var aggregationSubjStatements =
+            this.dataPackageGraph.statementsMatching(aggregationNode);
+          _.each(aggregationSubjStatements, function (statement) {
+            statement.subject.value =
+              cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
+          });
+
+          //Change all the statements in the RDF where the aggregation is the object, to reflect the new resource map ID
+          var aggregationObjStatements =
+            this.dataPackageGraph.statementsMatching(
+              undefined,
+              undefined,
+              aggregationNode,
+            );
+          _.each(aggregationObjStatements, function (statement) {
+            statement.object.value =
+              cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
+          });
+
+          //Change all the resource map subject nodes in the RDF graph
+          var rMapNode = rdf.sym(cnResolveUrl + encodeURIComponent(oldPid));
+          var rMapStatements =
+            this.dataPackageGraph.statementsMatching(rMapNode);
+          _.each(rMapStatements, function (statement) {
+            statement.subject.value = cnResolveUrl + encodeURIComponent(pid);
+          });
+
+          //Change the idDescribedBy statement
+          var isDescribedByStatements =
+            this.dataPackageGraph.statementsMatching(
+              undefined,
+              ORE("isDescribedBy"),
+              rdf.sym(oldPid),
+            );
+          if (isDescribedByStatements[0])
+            isDescribedByStatements[0].object.value = pid;
+
+          //Add nodes for new package members
+          _.each(
+            addedIds,
+            function (id) {
+              this.addToAggregation(id);
+            },
+            this,
+          );
+
+          //Change all the resource map identifier literal node in the RDF graph
+          if (idStatement[0]) idStatement[0].object.value = pid;
+        }
+
+        //Now serialize the RDF XML
+        var serializer = rdf.Serializer();
+        serializer.store = this.dataPackageGraph;
+
+        var xmlString = serializer.statementsToXML(
+          this.dataPackageGraph.statements,
+        );
+
+        return xmlString;
+      },
+
+      serializeSysMeta: function () {
+        //Get the system metadata XML that currently exists in the system
+        var xml = $(this.get("sysMetaXML"));
+
+        //Update the system metadata values
+        xml.find("serialversion").text(this.get("serialVersion") || "0");
+        xml.find("identifier").text(this.get("newPid") || this.get("id"));
+        xml.find("formatid").text(this.get("formatId"));
+        xml.find("size").text(this.get("size"));
+        xml.find("checksum").text(this.get("checksum"));
+        xml
+          .find("submitter")
+          .text(
+            this.get("submitter") || MetacatUI.appUserModel.get("username"),
+          );
+        xml
+          .find("rightsholder")
+          .text(
+            this.get("rightsHolder") || MetacatUI.appUserModel.get("username"),
+          );
+        xml.find("archived").text(this.get("archived"));
+        xml
+          .find("dateuploaded")
+          .text(this.get("dateUploaded") || new Date().toISOString());
+        xml
+          .find("datesysmetadatamodified")
+          .text(
+            this.get("dateSysMetadataModified") || new Date().toISOString(),
+          );
+        xml
+          .find("originmembernode")
+          .text(
+            this.get("originMemberNode") ||
+              MetacatUI.nodeModel.get("currentMemberNode"),
+          );
+        xml
+          .find("authoritativemembernode")
+          .text(
+            this.get("authoritativeMemberNode") ||
+              MetacatUI.nodeModel.get("currentMemberNode"),
+          );
+
+        if (this.get("obsoletes"))
+          xml.find("obsoletes").text(this.get("obsoletes"));
+        else xml.find("obsoletes").remove();
+
+        if (this.get("obsoletedBy"))
+          xml.find("obsoletedby").text(this.get("obsoletedBy"));
+        else xml.find("obsoletedby").remove();
+
+        //Write the access policy
+        var accessPolicyXML = "<accessPolicy>\n";
+        _.each(this.get("accesspolicy"), function (policy, policyType, all) {
+          var fullPolicy = all[policyType];
+
+          _.each(fullPolicy, function (policyPart) {
+            accessPolicyXML += "\t<" + policyType + ">\n";
+
+            accessPolicyXML +=
+              "\t\t<subject>" + policyPart.subject + "</subject>\n";
+
+            var permissions = Array.isArray(policyPart.permission)
+              ? policyPart.permission
+              : [policyPart.permission];
+            _.each(permissions, function (perm) {
+              accessPolicyXML += "\t\t<permission>" + perm + "</permission>\n";
+            });
+
+            accessPolicyXML += "\t</" + policyType + ">\n";
+          });
+        });
+        accessPolicyXML += "</accessPolicy>";
+
+        //Replace the old access policy with the new one
+        xml.find("accesspolicy").replaceWith(accessPolicyXML);
+
+        var xmlString = $(document.createElement("div"))
+          .append(xml.clone())
+          .html();
+
+        //Now camel case the nodes
+        _.each(
+          Object.keys(this.sysMetaNodeMap),
+          function (name, i, allNodeNames) {
+            var regEx = new RegExp("<" + name, "g");
+            xmlString = xmlString.replace(
+              regEx,
+              "<" + this.sysMetaNodeMap[name],
+            );
+            var regEx = new RegExp(name + ">", "g");
+            xmlString = xmlString.replace(
+              regEx,
+              this.sysMetaNodeMap[name] + ">",
+            );
+          },
+          this,
+        );
+
+        xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata");
+
+        return xmlString;
+      },
+
+      //Adds a new object to the resource map RDF graph
+      addToAggregation: function (id) {
+        if (id.indexOf(this.dataPackageGraph.cnResolveUrl) < 0)
+          var fullID =
+            this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
+        else {
+          var fullID = id;
+          id = id.substring(
+            this.dataPackageGraph.cnResolveUrl.lastIndexOf("/") + 1,
+          );
+        }
+
+        //Initialize the namespaces
+        var ORE = rdf.Namespace(this.namespaces.ORE),
+          DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
+          XML = rdf.Namespace(this.namespaces.XML),
+          CITO = rdf.Namespace(this.namespaces.CITO);
+
+        //Create a node for this object, the identifier, the resource map, and the aggregation
+        var objectNode = rdf.sym(fullID),
+          mapNode = rdf.sym(
+            this.dataPackageGraph.cnResolveUrl +
+              encodeURIComponent(this.get("id")),
+          ),
+          aggNode = rdf.sym(
+            this.dataPackageGraph.cnResolveUrl +
+              encodeURIComponent(this.get("id")) +
+              "#aggregation",
+          ),
+          idNode = rdf.literal(id, undefined, XML("string"));
+
+        //Add the statement: this object isAggregatedBy the resource map aggregation
+        this.dataPackageGraph.addStatement(
+          rdf.st(objectNode, ORE("isAggregatedBy"), aggNode),
+        );
+        //Add the statement: The resource map aggregation aggregates this object
+        this.dataPackageGraph.addStatement(
+          rdf.st(aggNode, ORE("aggregates"), objectNode),
+        );
+        //Add the statement: This object has the identifier {id}
+        this.dataPackageGraph.addStatement(
+          rdf.st(objectNode, DCTERMS("identifier"), idNode),
+        );
+
+        //Find the metadata doc that describes this object
+        var model = _.find(this.get("members"), function (m) {
+            return m.get("id") == id;
+          }),
+          isDocBy = model.get("isDocumentedBy");
+
+        //If this object is documented by any metadata...
+        if (isDocBy) {
+          //Get the ids of all the metadata objects in this package
+          var metadataInPackage = _.compact(
+            _.map(this.get("members"), function (m) {
+              if (m.get("formatType") == "METADATA") return m.get("id");
+            }),
+          );
+          //Find the metadata IDs that are in this package that also documents this data object
+          var metadataIds = Array.isArray(isDocBy)
+            ? _.intersection(metadataInPackage, isDocBy)
+            : _.intersection(metadataInPackage, [isDocBy]);
+
+          //For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement
+          _.each(
+            metadataIds,
+            function (metaId) {
+              //Create the named nodes and statements
+              var memberNode = rdf.sym(
+                  this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id),
+                ),
+                metadataNode = rdf.sym(
+                  this.dataPackageGraph.cnResolveUrl +
+                    encodeURIComponent(metaId),
+                ),
+                isDocByStatement = rdf.st(
+                  memberNode,
+                  CITO("isDocumentedBy"),
+                  metadataNode,
+                ),
+                documentsStatement = rdf.st(
+                  metadataNode,
+                  CITO("documents"),
+                  memberNode,
+                );
+              //Add the statements
+              this.dataPackageGraph.addStatement(isDocByStatement);
+              this.dataPackageGraph.addStatement(documentsStatement);
+            },
+            this,
+          );
+        }
+      },
+
+      removeFromAggregation: function (id) {
+        if (!id.indexOf(this.dataPackageGraph.cnResolveUrl))
+          id = this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
+
+        var removedObjNode = rdf.sym(id),
+          statements = _.union(
+            this.dataPackageGraph.statementsMatching(
+              undefined,
+              undefined,
+              removedObjNode,
+            ),
+            this.dataPackageGraph.statementsMatching(removedObjNode),
+          );
+
+        this.dataPackageGraph.removeStatements(statements);
+      },
+
+      getParentMetadata: function () {
+        var rMapIds = this.get("resourceMap");
+
+        //Create a query that searches for any resourceMap with an id matching one of the parents OR an id that matches one of the parents.
+        //This will return all members of the parent resource maps AND the parent resource maps themselves
+        var rMapQuery = "",
+          idQuery = "";
+        if (Array.isArray(rMapIds) && rMapIds.length > 1) {
+          _.each(rMapIds, function (id, i, ids) {
+            //At the begininng of the list of ids
+            if (rMapQuery.length == 0) {
+              rMapQuery += "resourceMap:(";
+              idQuery += "id:(";
             }
 
-            //Now serialize the RDF XML
-            var serializer = rdf.Serializer();
-            serializer.store = this.dataPackageGraph;
-
-            var xmlString = serializer.statementsToXML(this.dataPackageGraph.statements);
-
-        	return xmlString;
-        },
-
-        serializeSysMeta: function(){
-        	//Get the system metadata XML that currently exists in the system
-        	var xml = $(this.get("sysMetaXML"));
-
-        	//Update the system metadata values
-        	xml.find("serialversion").text(this.get("serialVersion") || "0");
-        	xml.find("identifier").text((this.get("newPid") || this.get("id")));
-        	xml.find("formatid").text(this.get("formatId"));
-        	xml.find("size").text(this.get("size"));
-        	xml.find("checksum").text(this.get("checksum"));
-        	xml.find("submitter").text(this.get("submitter") || MetacatUI.appUserModel.get("username"));
-        	xml.find("rightsholder").text(this.get("rightsHolder") || MetacatUI.appUserModel.get("username"));
-        	xml.find("archived").text(this.get("archived"));
-        	xml.find("dateuploaded").text(this.get("dateUploaded") || new Date().toISOString());
-        	xml.find("datesysmetadatamodified").text(this.get("dateSysMetadataModified") || new Date().toISOString());
-        	xml.find("originmembernode").text(this.get("originMemberNode") || MetacatUI.nodeModel.get("currentMemberNode"));
-        	xml.find("authoritativemembernode").text(this.get("authoritativeMemberNode") || MetacatUI.nodeModel.get("currentMemberNode"));
-
-        	if(this.get("obsoletes"))
-        		xml.find("obsoletes").text(this.get("obsoletes"));
-        	else
-        		xml.find("obsoletes").remove();
-
-        	if(this.get("obsoletedBy"))
-        		xml.find("obsoletedby").text(this.get("obsoletedBy"));
-        	else
-        		xml.find("obsoletedby").remove();
-
-        	//Write the access policy
-        	var accessPolicyXML = '<accessPolicy>\n';
-        	_.each(this.get("accesspolicy"), function(policy, policyType, all){
-    			var fullPolicy = all[policyType];
-
-    			_.each(fullPolicy, function(policyPart){
-    				accessPolicyXML += '\t<' + policyType + '>\n';
-
-        			accessPolicyXML += '\t\t<subject>' + policyPart.subject + '</subject>\n';
-
-        			var permissions = Array.isArray(policyPart.permission)? policyPart.permission : [policyPart.permission];
-        			_.each(permissions, function(perm){
-        				accessPolicyXML += '\t\t<permission>' + perm + '</permission>\n';
-            		});
-
-        			accessPolicyXML += '\t</' + policyType + '>\n';
-    			});
-        	});
-        	accessPolicyXML += '</accessPolicy>';
-
-        	//Replace the old access policy with the new one
-        	xml.find("accesspolicy").replaceWith(accessPolicyXML);
-
-        	var xmlString = $(document.createElement("div")).append(xml.clone()).html();
-
-        	//Now camel case the nodes
-        	_.each(Object.keys(this.sysMetaNodeMap), function(name, i, allNodeNames){
-        		var regEx = new RegExp("<" + name, "g");
-        		xmlString = xmlString.replace(regEx, "<" + this.sysMetaNodeMap[name]);
-        		var regEx = new RegExp(name + ">", "g");
-        		xmlString = xmlString.replace(regEx, this.sysMetaNodeMap[name] + ">");
-        	}, this);
-
-        	xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata");
-
-        	return xmlString;
-        },
-
-        //Adds a new object to the resource map RDF graph
-        addToAggregation: function(id){
-        	if(id.indexOf(this.dataPackageGraph.cnResolveUrl) < 0)
-        		var fullID = this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
-        	else{
-        		var fullID = id;
-        		id = id.substring(this.dataPackageGraph.cnResolveUrl.lastIndexOf("/") + 1);
-        	}
-
-        	//Initialize the namespaces
-        	var ORE     = rdf.Namespace(this.namespaces.ORE),
-        		DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
-        		XML     = rdf.Namespace(this.namespaces.XML),
-        		CITO    = rdf.Namespace(this.namespaces.CITO);
-
-        	//Create a node for this object, the identifier, the resource map, and the aggregation
-        	var objectNode = rdf.sym(fullID),
-        		mapNode    = rdf.sym(this.dataPackageGraph.cnResolveUrl + encodeURIComponent(this.get("id"))),
-        		aggNode    = rdf.sym(this.dataPackageGraph.cnResolveUrl + encodeURIComponent(this.get("id")) + "#aggregation"),
-        		idNode     = rdf.literal(id, undefined, XML("string"));
-
-        	//Add the statement: this object isAggregatedBy the resource map aggregation
-			this.dataPackageGraph.addStatement(rdf.st(objectNode, ORE("isAggregatedBy"), aggNode));
-			//Add the statement: The resource map aggregation aggregates this object
-			this.dataPackageGraph.addStatement(rdf.st(aggNode, ORE("aggregates"), objectNode));
-			//Add the statement: This object has the identifier {id}
-			this.dataPackageGraph.addStatement(rdf.st(objectNode, DCTERMS("identifier"), idNode));
-
-			//Find the metadata doc that describes this object
-			var model   = _.find(this.get("members"), function(m){ return m.get("id") == id }),
-				isDocBy = model.get("isDocumentedBy");
-
-			//If this object is documented by any metadata...
-			if(isDocBy){
-				//Get the ids of all the metadata objects in this package
-				var	metadataInPackage = _.compact(_.map(this.get("members"), function(m){ if(m.get("formatType") == "METADATA") return m.get("id"); }));
-				//Find the metadata IDs that are in this package that also documents this data object
-				var metadataIds = Array.isArray(isDocBy)? _.intersection(metadataInPackage, isDocBy) : _.intersection(metadataInPackage, [isDocBy]);
-
-				//For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement
-				_.each(metadataIds, function(metaId){
-					//Create the named nodes and statements
-					var memberNode       = rdf.sym(this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id)),
-						metadataNode     = rdf.sym(this.dataPackageGraph.cnResolveUrl + encodeURIComponent(metaId)),
-						isDocByStatement = rdf.st(memberNode, CITO("isDocumentedBy"), metadataNode),
-						documentsStatement = rdf.st(metadataNode, CITO("documents"), memberNode);
-					//Add the statements
-					this.dataPackageGraph.addStatement(isDocByStatement);
-					this.dataPackageGraph.addStatement(documentsStatement);
-				}, this);
-			}
-        },
-
-        removeFromAggregation: function(id){
-        	if(!id.indexOf(this.dataPackageGraph.cnResolveUrl)) id = this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
-
-        	var removedObjNode = rdf.sym(id),
-        		statements = _.union(this.dataPackageGraph.statementsMatching(undefined, undefined, removedObjNode),
-        						this.dataPackageGraph.statementsMatching(removedObjNode));
-
-			this.dataPackageGraph.removeStatements(statements);
-        },
-
-		getParentMetadata: function(){
-			var rMapIds = this.get("resourceMap");
-
-			//Create a query that searches for any resourceMap with an id matching one of the parents OR an id that matches one of the parents.
-			//This will return all members of the parent resource maps AND the parent resource maps themselves
-			var rMapQuery = "",
-				idQuery = "";
-			if(Array.isArray(rMapIds) && (rMapIds.length > 1)){
-				_.each(rMapIds, function(id, i, ids){
-
-					//At the begininng of the list of ids
-					if(rMapQuery.length == 0){
-						rMapQuery += "resourceMap:(";
-						idQuery += "id:(";
-					}
-
-					//The id
-					rMapQuery += "%22" + encodeURIComponent(id) + "%22";
-					idQuery   += "%22" + encodeURIComponent(id) + "%22";
-
-					//At the end of the list of ids
-					if(i+1 == ids.length){
-						rMapQuery += ")";
-						idQuery += ")";
-					}
-					//In-between each id
-					else{
-						rMapQuery += " OR ";
-						idQuery += " OR ";
-					}
-				});
-			}
-			else{
-				//When there is just one parent, the query is simple
-				var rMapId = Array.isArray(rMapIds)? rMapIds[0] : rMapIds;
-				rMapQuery += "resourceMap:%22" + encodeURIComponent(rMapId) + "%22";
-				idQuery   += "id:%22" + encodeURIComponent(rMapId) + "%22";
-			}
-			var query = "fl=title,id,obsoletedBy,resourceMap" +
-						"&wt=json" +
-						"&group=true&group.field=formatType&group.limit=-1" +
-						"&q=((formatType:METADATA AND " + rMapQuery + ") OR " + idQuery + ")";
-
-			var model = this;
-			var requestSettings = {
-				url: MetacatUI.appModel.get("queryServiceUrl") + query,
-				success: function(data, textStatus, xhr) {
-					var results = data.grouped.formatType.groups,
-							resourceMapGroup = _.where(results, { groupValue: "RESOURCE" })[0],
-						rMapList = resourceMapGroup? resourceMapGroup.doclist : null,
-						rMaps = rMapList? rMapList.docs : [],
-						rMapIds = _.pluck(rMaps, "id"),
-						parents = [],
-						parentIds = [];
-
-					//As long as this map isn't obsoleted by another map in our results list, we will show it
-					_.each(rMaps, function(map){
-						if(! (map.obsoletedBy && _.contains(rMapIds, map.obsoletedBy))){
-							parents.push(map);
-							parentIds.push(map.id);
-						}
-					});
-
-					var metadataList =  _.where(results, {groupValue: "METADATA"})[0],
-						metadata = (metadataList && metadataList.doclist)? metadataList.doclist.docs : [],
-						metadataModels = [];
+            //The id
+            rMapQuery += "%22" + encodeURIComponent(id) + "%22";
+            idQuery += "%22" + encodeURIComponent(id) + "%22";
+
+            //At the end of the list of ids
+            if (i + 1 == ids.length) {
+              rMapQuery += ")";
+              idQuery += ")";
+            }
+            //In-between each id
+            else {
+              rMapQuery += " OR ";
+              idQuery += " OR ";
+            }
+          });
+        } else {
+          //When there is just one parent, the query is simple
+          var rMapId = Array.isArray(rMapIds) ? rMapIds[0] : rMapIds;
+          rMapQuery += "resourceMap:%22" + encodeURIComponent(rMapId) + "%22";
+          idQuery += "id:%22" + encodeURIComponent(rMapId) + "%22";
+        }
+        var query =
+          "fl=title,id,obsoletedBy,resourceMap" +
+          "&wt=json" +
+          "&group=true&group.field=formatType&group.limit=-1" +
+          "&q=((formatType:METADATA AND " +
+          rMapQuery +
+          ") OR " +
+          idQuery +
+          ")";
+
+        var model = this;
+        var requestSettings = {
+          url: MetacatUI.appModel.get("queryServiceUrl") + query,
+          success: function (data, textStatus, xhr) {
+            var results = data.grouped.formatType.groups,
+              resourceMapGroup = _.where(results, {
+                groupValue: "RESOURCE",
+              })[0],
+              rMapList = resourceMapGroup ? resourceMapGroup.doclist : null,
+              rMaps = rMapList ? rMapList.docs : [],
+              rMapIds = _.pluck(rMaps, "id"),
+              parents = [],
+              parentIds = [];
 
             //As long as this map isn't obsoleted by another map in our results list, we will show it
-  					_.each(metadata, function(m){
+            _.each(rMaps, function (map) {
+              if (!(map.obsoletedBy && _.contains(rMapIds, map.obsoletedBy))) {
+                parents.push(map);
+                parentIds.push(map.id);
+              }
+            });
+
+            var metadataList = _.where(results, { groupValue: "METADATA" })[0],
+              metadata =
+                metadataList && metadataList.doclist
+                  ? metadataList.doclist.docs
+                  : [],
+              metadataModels = [];
 
+            //As long as this map isn't obsoleted by another map in our results list, we will show it
+            _.each(metadata, function (m) {
               //Find the metadata doc that obsoletes this one
               var isObsoletedBy = _.findWhere(metadata, { id: m.obsoletedBy });
 
               //If one isn't found, then this metadata doc is the most recent
-  						if(typeof isObsoletedBy == "undefined"){
+              if (typeof isObsoletedBy == "undefined") {
                 //If this metadata doc is in one of the filtered parent resource maps
-    						if(_.intersection(parentIds, m.resourceMap).length){
+                if (_.intersection(parentIds, m.resourceMap).length) {
                   //Create a SolrResult model and add to an array
-    							metadataModels.push(new SolrResult(m));
+                  metadataModels.push(new SolrResult(m));
+                }
+              }
+            });
+
+            model.set("parentPackageMetadata", metadataModels);
+            model.trigger("change:parentPackageMetadata");
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      //Create the URL string that is used to download this package
+      getURL: function () {
+        var url = null;
+
+        //If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from
+        if (
+          MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") >
+            -1 &&
+          MetacatUI.nodeModel.get("members").length
+        ) {
+          var source = this.get("datasource"),
+            node = _.find(MetacatUI.nodeModel.get("members"), {
+              identifier: source,
+            });
+
+          //If this node has MNRead v2 services...
+          if (node && node.readv2)
+            url =
+              node.baseURL +
+              "/v2/packages/application%2Fbagit-097/" +
+              encodeURIComponent(this.get("id"));
+        } else if (MetacatUI.appModel.get("packageServiceUrl"))
+          url =
+            MetacatUI.appModel.get("packageServiceUrl") +
+            encodeURIComponent(this.get("id"));
+
+        this.set("url", url);
+        return url;
+      },
+
+      createNestedPackages: function () {
+        var parentPackage = this,
+          nestedPackages = this.getNestedPackages(),
+          numNestedPackages = nestedPackages.length,
+          numComplete = 0;
+
+        _.each(nestedPackages, function (nestedPackage, i, nestedPackages) {
+          //Flag the parent model as complete when all the nested package info is ready
+          nestedPackage.on("complete", function () {
+            numComplete++;
+
+            //This is the last package in this package - finish up details and flag as complete
+            if (numNestedPackages == numComplete) {
+              var sorted = _.sortBy(parentPackage.get("members"), function (p) {
+                return p.get("id");
+              });
+              parentPackage.set("members", sorted);
+              parentPackage.flagComplete();
+            }
+          });
+
+          //Only look one-level deep at all times to avoid going down a rabbit hole
+          if (
+            nestedPackage.get("parentPackage") &&
+            nestedPackage.get("parentPackage").get("parentPackage")
+          ) {
+            nestedPackage.flagComplete();
+            return;
+          } else {
+            //Get the members of this nested package
+            nestedPackage.getMembers();
+          }
+        });
+      },
+
+      getNestedPackages: function () {
+        return _.where(this.get("members"), { type: "Package" });
+      },
+
+      getMemberNames: function () {
+        var metadata = this.getMetadata();
+        if (!metadata) return false;
+
+        //Load the rendered metadata from the view service
+        var viewService =
+          MetacatUI.appModel.get("viewServiceUrl") +
+          encodeURIComponent(metadata.get("id"));
+        var requestSettings = {
+          url: viewService,
+          success: function (data, response, xhr) {
+            if (solrResult.get("formatType") == "METADATA")
+              entityName = solrResult.get("title");
+            else {
+              var container = viewRef.findEntityDetailsContainer(
+                solrResult.get("id"),
+              );
+              if (container && container.length > 0) {
+                var entityName = $(container)
+                  .find(".entityName")
+                  .attr("data-entity-name");
+                if (typeof entityName === "undefined" || !entityName) {
+                  entityName = $(container)
+                    .find(
+                      ".control-label:contains('Entity Name') + .controls-well",
+                    )
+                    .text();
+                  if (typeof entityName === "undefined" || !entityName)
+                    entityName = null;
                 }
-  						}
-  					});
-
-					model.set("parentPackageMetadata", metadataModels);
-					model.trigger("change:parentPackageMetadata");
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		//Create the URL string that is used to download this package
-		getURL: function(){
-			var url = null;
-
-			//If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from
-			if((MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") > -1) && MetacatUI.nodeModel.get("members").length){
-				var source = this.get("datasource"),
-					node   = _.find(MetacatUI.nodeModel.get("members"), {identifier: source});
-
-				//If this node has MNRead v2 services...
-				if(node && node.readv2)
-					url = node.baseURL + "/v2/packages/application%2Fbagit-097/" + encodeURIComponent(this.get("id"));
-			}
-			else if(MetacatUI.appModel.get("packageServiceUrl"))
-				url = MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(this.get("id"));
-
-			this.set("url", url);
-			return url;
-		},
-
-		createNestedPackages: function(){
-			var parentPackage = this,
-			    nestedPackages = this.getNestedPackages(),
-					numNestedPackages = nestedPackages.length,
-					numComplete = 0;
-
-			_.each(nestedPackages, function(nestedPackage, i, nestedPackages){
-
-				//Flag the parent model as complete when all the nested package info is ready
-				nestedPackage.on("complete", function(){
-					numComplete++;
-
-					//This is the last package in this package - finish up details and flag as complete
-					if(numNestedPackages == numComplete){
-						var sorted = _.sortBy(parentPackage.get("members"), function(p){ return p.get("id") });
-						parentPackage.set("members", sorted);
-						parentPackage.flagComplete();
-					}
-				});
-
-				//Only look one-level deep at all times to avoid going down a rabbit hole
-				if( nestedPackage.get("parentPackage") && nestedPackage.get("parentPackage").get("parentPackage") ){
-					nestedPackage.flagComplete();
-					return;
-				}
-				else{
-					//Get the members of this nested package
-					nestedPackage.getMembers();
-				}
-
-			});
-		},
-
-		getNestedPackages: function(){
-			return _.where(this.get("members"), {type: "Package"});
-		},
-
-		getMemberNames: function(){
-			var metadata = this.getMetadata();
-			if(!metadata) return false;
-
-			//Load the rendered metadata from the view service
-			var viewService = MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(metadata.get("id"));
-			var requestSettings = {
-					url: viewService,
-					success: function(data, response, xhr){
-						if(solrResult.get("formatType") == "METADATA")
-							entityName = solrResult.get("title");
-						else{
-							var container = viewRef.findEntityDetailsContainer(solrResult.get("id"));
-							if(container && container.length > 0){
-								var entityName = $(container).find(".entityName").attr("data-entity-name");
-								if((typeof entityName === "undefined") || (!entityName)){
-									entityName = $(container).find(".control-label:contains('Entity Name') + .controls-well").text();
-									if((typeof entityName === "undefined") || (!entityName))
-										entityName = null;
-								}
-							}
-							else
-								entityName = null;
-
-						}
-					}
-			}
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		/*
-		 * Will query for the derivations of this package, and sort all entities in the prov trace
-		 * into sources and derivations.
-		 */
-		getProvTrace: function(){
-			var model = this;
-
-			//See if there are any prov fields in our index before continuing
-			if(!MetacatUI.appSearchModel.getProvFields()) return this;
-
-			//Start keeping track of the sources and derivations
-			var sources 		   = new Array(),
-				derivations 	   = new Array();
-
-			//Search for derivations of this package
-			var derivationsQuery = MetacatUI.appSearchModel.getGroupedQuery("prov_wasDerivedFrom",
-					_.map(this.get("members"), function(m){ return m.get("id"); }), "OR") +
-					"%20-obsoletedBy:*";
-
-			var requestSettings = {
-					url: MetacatUI.appModel.get("queryServiceUrl") + "&q=" + derivationsQuery + "&wt=json&rows=1000" +
-						 "&fl=id,resourceMap,documents,isDocumentedBy,prov_wasDerivedFrom",
-					success: function(data){
-						_.each(data.response.docs, function(result){
-							derivations.push(result.id);
-						});
-
-						//Make arrays of unique IDs of objects that are sources or derivations of this package.
-						_.each(model.get("members"), function(member, i){
-							if(member.type == "Package") return;
-
-							if(member.hasProvTrace()){
-								sources 	= _.union(sources, member.getSources());
-								derivations = _.union(derivations, member.getDerivations());
-							}
-						});
-
-						//Save the arrays of sources and derivations
-						model.set("sources", sources);
-						model.set("derivations", derivations);
-
-						//Now get metadata about all the entities in the prov trace not in this package
-						model.getExternalProvTrace();
-					}
-			}
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		getExternalProvTrace: function(){
-			var model = this;
-
-			//Compact our list of ids that are in the prov trace by combining the sources and derivations and removing ids of members of this package
-			var externalProvEntities = _.difference(_.union(this.get("sources"), this.get("derivations")), this.get("memberIds"));
-
-			//If there are no sources or derivations, then we do not need to find resource map ids for anything
-			if(!externalProvEntities.length){
-
-				//Save this prov trace on a package-member/document/object level.
-				if(this.get("sources").length || this.get("derivations").length)
-					this.setMemberProvTrace();
-
-				//Flag that the provenance trace is complete
-				this.set("provenanceFlag", "complete");
-
-				return this;
-			}
-			else{
-				//Create a query where we retrieve the ID of the resource map of each source and derivation
-				var idQuery = MetacatUI.appSearchModel.getGroupedQuery("id", externalProvEntities, "OR");
-
-				//Create a query where we retrieve the metadata for each source and derivation
-				var metadataQuery = MetacatUI.appSearchModel.getGroupedQuery("documents", externalProvEntities, "OR");
-			}
-
-			//TODO: Find the products of programs/executions
-
-			//Make a comma-separated list of the provenance field names
-			var provFieldList = "";
-			_.each(MetacatUI.appSearchModel.getProvFields(), function(fieldName, i, list){
-				provFieldList += fieldName;
-				if(i < list.length-1) provFieldList += ",";
-			});
-
-			//Combine the two queries with an OR operator
-			if(idQuery.length && metadataQuery.length) var combinedQuery = idQuery + "%20OR%20" + metadataQuery;
-			else return this;
-
-			//the full and final query in Solr syntax
-			var query = "q=" + combinedQuery +
-						"&fl=id,resourceMap,documents,isDocumentedBy,formatType,formatId,dateUploaded,rightsHolder,datasource,prov_instanceOfClass," +
-						provFieldList +
-						"&rows=100&wt=json";
-
-			//Send the query to the query service
-			var requestSettings = {
-				url: MetacatUI.appModel.get("queryServiceUrl") + query,
-				success: function(data, textStatus, xhr){
-
-					//Do any of our docs have multiple resource maps?
-					var hasMultipleMaps = _.filter(data.response.docs, function(doc){
-						return((typeof doc.resourceMap !== "undefined") && (doc.resourceMap.length > 1))
-						});
-					//If so, we want to find the latest version of each resource map and only represent that one in the Prov Chart
-					if(typeof hasMultipleMaps !== "undefined"){
-						var allMapIDs = _.uniq(_.flatten(_.pluck(hasMultipleMaps, "resourceMap")));
-						if(allMapIDs.length){
-
-							var query = "q=+-obsoletedBy:*+" + MetacatUI.appSearchModel.getGroupedQuery("id", allMapIDs, "OR") +
-										"&fl=obsoletes,id" +
-										"&wt=json";
-							var requestSettings = {
-								url: MetacatUI.appModel.get("queryServiceUrl") + query,
-								success: function(mapData, textStatus, xhr){
-
-									//Create a list of resource maps that are not obsoleted by any other resource map retrieved
-									var resourceMaps = mapData.response.docs;
-
-									model.obsoletedResourceMaps = _.pluck(resourceMaps, "obsoletes");
-									model.latestResourceMaps    = _.difference(resourceMaps, model.obsoletedResourceMaps);
-
-									model.sortProvTrace(data.response.docs);
-								}
-							}
-							$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-						}
-						else
-							model.sortProvTrace(data.response.docs);
-					}
-					else
-						model.sortProvTrace(data.response.docs);
-
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-
-			return this;
-		},
-
-		sortProvTrace: function(docs){
-			var model = this;
-
-			//Start an array to hold the packages in the prov trace
-			var sourcePackages   = new Array(),
-				derPackages      = new Array(),
-				sourceDocs		 = new Array(),
-				derDocs	 		 = new Array(),
-				sourceIDs        = this.get("sources"),
-				derivationIDs    = this.get("derivations");
-
-			//Separate the results into derivations and sources and group by their resource map.
-			_.each(docs, function(doc, i){
-
-				var docModel = new SolrResult(doc),
-				      mapIds = docModel.get("resourceMap");
-
-
-				if(((typeof mapIds === "undefined") || !mapIds) && (docModel.get("formatType") == "DATA") && ((typeof docModel.get("isDocumentedBy") === "undefined") || !docModel.get("isDocumentedBy"))){
-					//If this object is not in a resource map and does not have metadata, it is a "naked" data doc, so save it by itself
-					if(_.contains(sourceIDs, doc.id))
-						sourceDocs.push(docModel);
-					if(_.contains(derivationIDs, doc.id))
-						derDocs.push(docModel);
-				}
-				else if(((typeof mapIds === "undefined") || !mapIds) && (docModel.get("formatType") == "DATA") && docModel.get("isDocumentedBy")){
-					//If this data doc does not have a resource map but has a metadata doc that documents it, create a blank package model and save it
-					var p = new PackageModel({
-						members: new Array(docModel)
-					});
-					//Add this package model to the sources and/or derivations packages list
-					if(_.contains(sourceIDs, docModel.get("id")))
-						sourcePackages[docModel.get("id")] = p;
-					if(_.contains(derivationIDs, docModel.get("id")))
-						derPackages[docModel.get("id")] = p;
-				}
-				else if(mapIds.length){
-					//If this doc has a resource map, create a package model and SolrResult model and store it
-					var id     = docModel.get("id");
-
-					//Some of these objects may have multiple resource maps
-					_.each(mapIds, function(mapId, i, list){
-
-						if(!_.contains(model.obsoletedResourceMaps, mapId)){
-							var documentsSource, documentsDerivation;
-							if(docModel.get("formatType") == "METADATA"){
-								if(_.intersection(docModel.get("documents"), sourceIDs).length)     documentsSource = true;
-								if(_.intersection(docModel.get("documents"), derivationIDs).length) documentsDerivation = true;
-							}
-
-							//Is this a source object or a metadata doc of a source object?
-							if(_.contains(sourceIDs, id) || documentsSource){
-								//Have we encountered this source package yet?
-								if(!sourcePackages[mapId] && (mapId != model.get("id"))){
-									//Now make a new package model for it
-									var p = new PackageModel({
-										id: mapId,
-										members: new Array(docModel)
-									});
-									//Add to the array of source packages
-									sourcePackages[mapId] = p;
-								}
-								//If so, add this member to its package model
-								else if(mapId != model.get("id")){
-									var memberList = sourcePackages[mapId].get("members");
-									memberList.push(docModel);
-									sourcePackages[mapId].set("members", memberList);
-								}
-							}
-
-							//Is this a derivation object or a metadata doc of a derivation object?
-							if(_.contains(derivationIDs, id) || documentsDerivation){
-								//Have we encountered this derivation package yet?
-								if(!derPackages[mapId] && (mapId != model.get("id"))){
-									//Now make a new package model for it
-									var p = new PackageModel({
-										id: mapId,
-										members: new Array(docModel)
-									});
-									//Add to the array of source packages
-									derPackages[mapId] = p;
-								}
-								//If so, add this member to its package model
-								else if(mapId != model.get("id")){
-									var memberList = derPackages[mapId].get("members");
-									memberList.push(docModel);
-									derPackages[mapId].set("members", memberList);
-								}
-							}
-						}
-					});
-				}
-			});
-
-			//Transform our associative array (Object) of packages into an array
-			var newArrays = new Array();
-			_.each(new Array(sourcePackages, derPackages, sourceDocs, derDocs), function(provObject){
-				var newArray = new Array(), key;
-				for(key in provObject){
-					newArray.push(provObject[key]);
-				}
-				newArrays.push(newArray);
-			});
-
-			//We now have an array of source packages and an array of derivation packages.
-			model.set("sourcePackages", newArrays[0]);
-			model.set("derivationPackages", newArrays[1]);
-			model.set("sourceDocs", newArrays[2]);
-			model.set("derivationDocs", newArrays[3]);
-
-			//Save this prov trace on a package-member/document/object level.
-			model.setMemberProvTrace();
-
-			//Flag that the provenance trace is complete
-			model.set("provenanceFlag", "complete");
-		},
-
-		setMemberProvTrace: function(){
-			var model = this,
-				relatedModels = this.get("relatedModels"),
-				relatedModelIDs = new Array();
-
-			//Now for each doc, we want to find which member it is related to
-			_.each(this.get("members"), function(member, i, members){
-				if(member.type == "Package") return;
-
-				//Get the sources and derivations of this member
-				var memberSourceIDs = member.getSources();
-				var memberDerIDs    = member.getDerivations();
-
-				//Look through each source package, derivation package, source doc, and derivation doc.
-				_.each(model.get("sourcePackages"), function(pkg, i){
-					_.each(pkg.get("members"), function(sourcePkgMember, i){
-						//Is this package member a direct source of this package member?
-						if(_.contains(memberSourceIDs, sourcePkgMember.get("id")))
-							//Save this source package member as a source of this member
-							member.set("provSources", _.union(member.get("provSources"), [sourcePkgMember]));
-
-						//Save this in the list of related models
-						if(!_.contains(relatedModelIDs, sourcePkgMember.get("id"))){
-							relatedModels.push(sourcePkgMember);
-							relatedModelIDs.push(sourcePkgMember.get("id"));
-						}
-					});
-				});
-				_.each(model.get("derivationPackages"), function(pkg, i){
-					_.each(pkg.get("members"), function(derPkgMember, i){
-						//Is this package member a direct source of this package member?
-						if(_.contains(memberDerIDs, derPkgMember.get("id")))
-							//Save this derivation package member as a derivation of this member
-							member.set("provDerivations", _.union(member.get("provDerivations"), [derPkgMember]));
-
-						//Save this in the list of related models
-						if(!_.contains(relatedModelIDs, derPkgMember.get("id"))){
-							relatedModels.push(derPkgMember);
-							relatedModelIDs.push(derPkgMember.get("id"));
-						}
-					});
-				});
-				_.each(model.get("sourceDocs"), function(doc, i){
-					//Is this package member a direct source of this package member?
-					if(_.contains(memberSourceIDs, doc.get("id")))
-						//Save this source package member as a source of this member
-						member.set("provSources", _.union(member.get("provSources"), [doc]));
-
-					//Save this in the list of related models
-					if(!_.contains(relatedModelIDs, doc.get("id"))){
-						relatedModels.push(doc);
-						relatedModelIDs.push(doc.get("id"));
-					}
-				});
-				_.each(model.get("derivationDocs"), function(doc, i){
-					//Is this package member a direct derivation of this package member?
-					if(_.contains(memberDerIDs, doc.get("id")))
-						//Save this derivation package member as a derivation of this member
-						member.set("provDerivations", _.union(member.get("provDerivations"), [doc]));
-
-					//Save this in the list of related models
-					if(!_.contains(relatedModelIDs, doc.get("id"))){
-						relatedModels.push(doc);
-						relatedModelIDs.push(doc.get("id"));
-					}
-				});
-				_.each(members, function(otherMember, i){
-					//Is this other package member a direct derivation of this package member?
-					if(_.contains(memberDerIDs, otherMember.get("id")))
-						//Save this other derivation package member as a derivation of this member
-						member.set("provDerivations", _.union(member.get("provDerivations"), [otherMember]));
-					//Is this other package member a direct source of this package member?
-					if(_.contains(memberSourceIDs, otherMember.get("id")))
-						//Save this other source package member as a source of this member
-						member.set("provSources", _.union(member.get("provSources"), [otherMember]));
-
-					//Is this other package member an indirect source or derivation?
-					if((otherMember.get("type") == "program") && (_.contains(member.get("prov_generatedByProgram"), otherMember.get("id")))){
-						var indirectSources = _.filter(members, function(m){
-							return _.contains(otherMember.getInputs(), m.get("id"));
-						});
-						indirectSourcesIds = _.each(indirectSources, function(m){ return m.get("id") });
-						member.set("prov_wasDerivedFrom", _.union(member.get("prov_wasDerivedFrom"), indirectSourcesIds));
-						//otherMember.set("prov_hasDerivations", _.union(otherMember.get("prov_hasDerivations"), [member.get("id")]));
-						member.set("provSources", _.union(member.get("provSources"), indirectSources));
-					}
-					if((otherMember.get("type") == "program") && (_.contains(member.get("prov_usedByProgram"), otherMember.get("id")))){
-						var indirectDerivations = _.filter(members, function(m){
-							return _.contains(otherMember.getOutputs(), m.get("id"));
-						});
-						indirectDerivationsIds = _.each(indirectDerivations, function(m){ return m.get("id") });
-						member.set("prov_hasDerivations", _.union(member.get("prov_hasDerivations"), indirectDerivationsIds));
-						//otherMember.set("prov_wasDerivedFrom", _.union(otherMember.get("prov_wasDerivedFrom"), [member.get("id")]));
-						member.set("provDerivations", _.union(member.get("provDerivations"), indirectDerivationsIds));
-					}
-				});
-
-				//Add this member to the list of related models
-				if(!_.contains(relatedModelIDs, member.get("id"))){
-					relatedModels.push(member);
-					relatedModelIDs.push(member.get("id"));
-				}
-
-				//Clear out any duplicates
-				member.set("provSources", _.uniq(member.get("provSources")));
-				member.set("provDerivations", _.uniq(member.get("provDerivations")));
-			});
-
-			//Update the list of related models
-			this.set("relatedModels", relatedModels);
-
-		},
-
-		downloadWithCredentials: function(){
-			//Get info about this object
-			var	url = this.get("url"),
-				  model = this;
-
-			//Create an XHR
-			var xhr = new XMLHttpRequest();
-			xhr.withCredentials = true;
-
-			//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
-			xhr.onload = function(){
-
-        //Get the file name from the Content-Disposition header
-        var filename = xhr.getResponseHeader('Content-Disposition');
-
-        //As a backup, use the system metadata file name or the id
-        if(!filename){
-          filename = model.get("filename") || model.get("id");
+              } else entityName = null;
+            }
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      /*
+       * Will query for the derivations of this package, and sort all entities in the prov trace
+       * into sources and derivations.
+       */
+      getProvTrace: function () {
+        var model = this;
+
+        //See if there are any prov fields in our index before continuing
+        if (!MetacatUI.appSearchModel.getProvFields()) return this;
+
+        //Start keeping track of the sources and derivations
+        var sources = new Array(),
+          derivations = new Array();
+
+        //Search for derivations of this package
+        var derivationsQuery =
+          MetacatUI.appSearchModel.getGroupedQuery(
+            "prov_wasDerivedFrom",
+            _.map(this.get("members"), function (m) {
+              return m.get("id");
+            }),
+            "OR",
+          ) + "%20-obsoletedBy:*";
+
+        var requestSettings = {
+          url:
+            MetacatUI.appModel.get("queryServiceUrl") +
+            "&q=" +
+            derivationsQuery +
+            "&wt=json&rows=1000" +
+            "&fl=id,resourceMap,documents,isDocumentedBy,prov_wasDerivedFrom",
+          success: function (data) {
+            _.each(data.response.docs, function (result) {
+              derivations.push(result.id);
+            });
+
+            //Make arrays of unique IDs of objects that are sources or derivations of this package.
+            _.each(model.get("members"), function (member, i) {
+              if (member.type == "Package") return;
+
+              if (member.hasProvTrace()) {
+                sources = _.union(sources, member.getSources());
+                derivations = _.union(derivations, member.getDerivations());
+              }
+            });
+
+            //Save the arrays of sources and derivations
+            model.set("sources", sources);
+            model.set("derivations", derivations);
+
+            //Now get metadata about all the entities in the prov trace not in this package
+            model.getExternalProvTrace();
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      getExternalProvTrace: function () {
+        var model = this;
+
+        //Compact our list of ids that are in the prov trace by combining the sources and derivations and removing ids of members of this package
+        var externalProvEntities = _.difference(
+          _.union(this.get("sources"), this.get("derivations")),
+          this.get("memberIds"),
+        );
+
+        //If there are no sources or derivations, then we do not need to find resource map ids for anything
+        if (!externalProvEntities.length) {
+          //Save this prov trace on a package-member/document/object level.
+          if (this.get("sources").length || this.get("derivations").length)
+            this.setMemberProvTrace();
+
+          //Flag that the provenance trace is complete
+          this.set("provenanceFlag", "complete");
+
+          return this;
+        } else {
+          //Create a query where we retrieve the ID of the resource map of each source and derivation
+          var idQuery = MetacatUI.appSearchModel.getGroupedQuery(
+            "id",
+            externalProvEntities,
+            "OR",
+          );
+
+          //Create a query where we retrieve the metadata for each source and derivation
+          var metadataQuery = MetacatUI.appSearchModel.getGroupedQuery(
+            "documents",
+            externalProvEntities,
+            "OR",
+          );
         }
 
-        //Add a ".zip" extension if it doesn't exist
-  			if( filename.indexOf(".zip") < 0 || (filename.indexOf(".zip") != (filename.length-4)) ){
-          filename += ".zip";
+        //TODO: Find the products of programs/executions
+
+        //Make a comma-separated list of the provenance field names
+        var provFieldList = "";
+        _.each(
+          MetacatUI.appSearchModel.getProvFields(),
+          function (fieldName, i, list) {
+            provFieldList += fieldName;
+            if (i < list.length - 1) provFieldList += ",";
+          },
+        );
+
+        //Combine the two queries with an OR operator
+        if (idQuery.length && metadataQuery.length)
+          var combinedQuery = idQuery + "%20OR%20" + metadataQuery;
+        else return this;
+
+        //the full and final query in Solr syntax
+        var query =
+          "q=" +
+          combinedQuery +
+          "&fl=id,resourceMap,documents,isDocumentedBy,formatType,formatId,dateUploaded,rightsHolder,datasource,prov_instanceOfClass," +
+          provFieldList +
+          "&rows=100&wt=json";
+
+        //Send the query to the query service
+        var requestSettings = {
+          url: MetacatUI.appModel.get("queryServiceUrl") + query,
+          success: function (data, textStatus, xhr) {
+            //Do any of our docs have multiple resource maps?
+            var hasMultipleMaps = _.filter(data.response.docs, function (doc) {
+              return (
+                typeof doc.resourceMap !== "undefined" &&
+                doc.resourceMap.length > 1
+              );
+            });
+            //If so, we want to find the latest version of each resource map and only represent that one in the Prov Chart
+            if (typeof hasMultipleMaps !== "undefined") {
+              var allMapIDs = _.uniq(
+                _.flatten(_.pluck(hasMultipleMaps, "resourceMap")),
+              );
+              if (allMapIDs.length) {
+                var query =
+                  "q=+-obsoletedBy:*+" +
+                  MetacatUI.appSearchModel.getGroupedQuery(
+                    "id",
+                    allMapIDs,
+                    "OR",
+                  ) +
+                  "&fl=obsoletes,id" +
+                  "&wt=json";
+                var requestSettings = {
+                  url: MetacatUI.appModel.get("queryServiceUrl") + query,
+                  success: function (mapData, textStatus, xhr) {
+                    //Create a list of resource maps that are not obsoleted by any other resource map retrieved
+                    var resourceMaps = mapData.response.docs;
+
+                    model.obsoletedResourceMaps = _.pluck(
+                      resourceMaps,
+                      "obsoletes",
+                    );
+                    model.latestResourceMaps = _.difference(
+                      resourceMaps,
+                      model.obsoletedResourceMaps,
+                    );
+
+                    model.sortProvTrace(data.response.docs);
+                  },
+                };
+                $.ajax(
+                  _.extend(
+                    requestSettings,
+                    MetacatUI.appUserModel.createAjaxSettings(),
+                  ),
+                );
+              } else model.sortProvTrace(data.response.docs);
+            } else model.sortProvTrace(data.response.docs);
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+
+        return this;
+      },
+
+      sortProvTrace: function (docs) {
+        var model = this;
+
+        //Start an array to hold the packages in the prov trace
+        var sourcePackages = new Array(),
+          derPackages = new Array(),
+          sourceDocs = new Array(),
+          derDocs = new Array(),
+          sourceIDs = this.get("sources"),
+          derivationIDs = this.get("derivations");
+
+        //Separate the results into derivations and sources and group by their resource map.
+        _.each(docs, function (doc, i) {
+          var docModel = new SolrResult(doc),
+            mapIds = docModel.get("resourceMap");
+
+          if (
+            (typeof mapIds === "undefined" || !mapIds) &&
+            docModel.get("formatType") == "DATA" &&
+            (typeof docModel.get("isDocumentedBy") === "undefined" ||
+              !docModel.get("isDocumentedBy"))
+          ) {
+            //If this object is not in a resource map and does not have metadata, it is a "naked" data doc, so save it by itself
+            if (_.contains(sourceIDs, doc.id)) sourceDocs.push(docModel);
+            if (_.contains(derivationIDs, doc.id)) derDocs.push(docModel);
+          } else if (
+            (typeof mapIds === "undefined" || !mapIds) &&
+            docModel.get("formatType") == "DATA" &&
+            docModel.get("isDocumentedBy")
+          ) {
+            //If this data doc does not have a resource map but has a metadata doc that documents it, create a blank package model and save it
+            var p = new PackageModel({
+              members: new Array(docModel),
+            });
+            //Add this package model to the sources and/or derivations packages list
+            if (_.contains(sourceIDs, docModel.get("id")))
+              sourcePackages[docModel.get("id")] = p;
+            if (_.contains(derivationIDs, docModel.get("id")))
+              derPackages[docModel.get("id")] = p;
+          } else if (mapIds.length) {
+            //If this doc has a resource map, create a package model and SolrResult model and store it
+            var id = docModel.get("id");
+
+            //Some of these objects may have multiple resource maps
+            _.each(mapIds, function (mapId, i, list) {
+              if (!_.contains(model.obsoletedResourceMaps, mapId)) {
+                var documentsSource, documentsDerivation;
+                if (docModel.get("formatType") == "METADATA") {
+                  if (
+                    _.intersection(docModel.get("documents"), sourceIDs).length
+                  )
+                    documentsSource = true;
+                  if (
+                    _.intersection(docModel.get("documents"), derivationIDs)
+                      .length
+                  )
+                    documentsDerivation = true;
+                }
+
+                //Is this a source object or a metadata doc of a source object?
+                if (_.contains(sourceIDs, id) || documentsSource) {
+                  //Have we encountered this source package yet?
+                  if (!sourcePackages[mapId] && mapId != model.get("id")) {
+                    //Now make a new package model for it
+                    var p = new PackageModel({
+                      id: mapId,
+                      members: new Array(docModel),
+                    });
+                    //Add to the array of source packages
+                    sourcePackages[mapId] = p;
+                  }
+                  //If so, add this member to its package model
+                  else if (mapId != model.get("id")) {
+                    var memberList = sourcePackages[mapId].get("members");
+                    memberList.push(docModel);
+                    sourcePackages[mapId].set("members", memberList);
+                  }
+                }
+
+                //Is this a derivation object or a metadata doc of a derivation object?
+                if (_.contains(derivationIDs, id) || documentsDerivation) {
+                  //Have we encountered this derivation package yet?
+                  if (!derPackages[mapId] && mapId != model.get("id")) {
+                    //Now make a new package model for it
+                    var p = new PackageModel({
+                      id: mapId,
+                      members: new Array(docModel),
+                    });
+                    //Add to the array of source packages
+                    derPackages[mapId] = p;
+                  }
+                  //If so, add this member to its package model
+                  else if (mapId != model.get("id")) {
+                    var memberList = derPackages[mapId].get("members");
+                    memberList.push(docModel);
+                    derPackages[mapId].set("members", memberList);
+                  }
+                }
+              }
+            });
+          }
+        });
+
+        //Transform our associative array (Object) of packages into an array
+        var newArrays = new Array();
+        _.each(
+          new Array(sourcePackages, derPackages, sourceDocs, derDocs),
+          function (provObject) {
+            var newArray = new Array(),
+              key;
+            for (key in provObject) {
+              newArray.push(provObject[key]);
+            }
+            newArrays.push(newArray);
+          },
+        );
+
+        //We now have an array of source packages and an array of derivation packages.
+        model.set("sourcePackages", newArrays[0]);
+        model.set("derivationPackages", newArrays[1]);
+        model.set("sourceDocs", newArrays[2]);
+        model.set("derivationDocs", newArrays[3]);
+
+        //Save this prov trace on a package-member/document/object level.
+        model.setMemberProvTrace();
+
+        //Flag that the provenance trace is complete
+        model.set("provenanceFlag", "complete");
+      },
+
+      setMemberProvTrace: function () {
+        var model = this,
+          relatedModels = this.get("relatedModels"),
+          relatedModelIDs = new Array();
+
+        //Now for each doc, we want to find which member it is related to
+        _.each(this.get("members"), function (member, i, members) {
+          if (member.type == "Package") return;
+
+          //Get the sources and derivations of this member
+          var memberSourceIDs = member.getSources();
+          var memberDerIDs = member.getDerivations();
+
+          //Look through each source package, derivation package, source doc, and derivation doc.
+          _.each(model.get("sourcePackages"), function (pkg, i) {
+            _.each(pkg.get("members"), function (sourcePkgMember, i) {
+              //Is this package member a direct source of this package member?
+              if (_.contains(memberSourceIDs, sourcePkgMember.get("id")))
+                //Save this source package member as a source of this member
+                member.set(
+                  "provSources",
+                  _.union(member.get("provSources"), [sourcePkgMember]),
+                );
+
+              //Save this in the list of related models
+              if (!_.contains(relatedModelIDs, sourcePkgMember.get("id"))) {
+                relatedModels.push(sourcePkgMember);
+                relatedModelIDs.push(sourcePkgMember.get("id"));
+              }
+            });
+          });
+          _.each(model.get("derivationPackages"), function (pkg, i) {
+            _.each(pkg.get("members"), function (derPkgMember, i) {
+              //Is this package member a direct source of this package member?
+              if (_.contains(memberDerIDs, derPkgMember.get("id")))
+                //Save this derivation package member as a derivation of this member
+                member.set(
+                  "provDerivations",
+                  _.union(member.get("provDerivations"), [derPkgMember]),
+                );
+
+              //Save this in the list of related models
+              if (!_.contains(relatedModelIDs, derPkgMember.get("id"))) {
+                relatedModels.push(derPkgMember);
+                relatedModelIDs.push(derPkgMember.get("id"));
+              }
+            });
+          });
+          _.each(model.get("sourceDocs"), function (doc, i) {
+            //Is this package member a direct source of this package member?
+            if (_.contains(memberSourceIDs, doc.get("id")))
+              //Save this source package member as a source of this member
+              member.set(
+                "provSources",
+                _.union(member.get("provSources"), [doc]),
+              );
+
+            //Save this in the list of related models
+            if (!_.contains(relatedModelIDs, doc.get("id"))) {
+              relatedModels.push(doc);
+              relatedModelIDs.push(doc.get("id"));
+            }
+          });
+          _.each(model.get("derivationDocs"), function (doc, i) {
+            //Is this package member a direct derivation of this package member?
+            if (_.contains(memberDerIDs, doc.get("id")))
+              //Save this derivation package member as a derivation of this member
+              member.set(
+                "provDerivations",
+                _.union(member.get("provDerivations"), [doc]),
+              );
+
+            //Save this in the list of related models
+            if (!_.contains(relatedModelIDs, doc.get("id"))) {
+              relatedModels.push(doc);
+              relatedModelIDs.push(doc.get("id"));
+            }
+          });
+          _.each(members, function (otherMember, i) {
+            //Is this other package member a direct derivation of this package member?
+            if (_.contains(memberDerIDs, otherMember.get("id")))
+              //Save this other derivation package member as a derivation of this member
+              member.set(
+                "provDerivations",
+                _.union(member.get("provDerivations"), [otherMember]),
+              );
+            //Is this other package member a direct source of this package member?
+            if (_.contains(memberSourceIDs, otherMember.get("id")))
+              //Save this other source package member as a source of this member
+              member.set(
+                "provSources",
+                _.union(member.get("provSources"), [otherMember]),
+              );
+
+            //Is this other package member an indirect source or derivation?
+            if (
+              otherMember.get("type") == "program" &&
+              _.contains(
+                member.get("prov_generatedByProgram"),
+                otherMember.get("id"),
+              )
+            ) {
+              var indirectSources = _.filter(members, function (m) {
+                return _.contains(otherMember.getInputs(), m.get("id"));
+              });
+              indirectSourcesIds = _.each(indirectSources, function (m) {
+                return m.get("id");
+              });
+              member.set(
+                "prov_wasDerivedFrom",
+                _.union(member.get("prov_wasDerivedFrom"), indirectSourcesIds),
+              );
+              //otherMember.set("prov_hasDerivations", _.union(otherMember.get("prov_hasDerivations"), [member.get("id")]));
+              member.set(
+                "provSources",
+                _.union(member.get("provSources"), indirectSources),
+              );
+            }
+            if (
+              otherMember.get("type") == "program" &&
+              _.contains(
+                member.get("prov_usedByProgram"),
+                otherMember.get("id"),
+              )
+            ) {
+              var indirectDerivations = _.filter(members, function (m) {
+                return _.contains(otherMember.getOutputs(), m.get("id"));
+              });
+              indirectDerivationsIds = _.each(
+                indirectDerivations,
+                function (m) {
+                  return m.get("id");
+                },
+              );
+              member.set(
+                "prov_hasDerivations",
+                _.union(
+                  member.get("prov_hasDerivations"),
+                  indirectDerivationsIds,
+                ),
+              );
+              //otherMember.set("prov_wasDerivedFrom", _.union(otherMember.get("prov_wasDerivedFrom"), [member.get("id")]));
+              member.set(
+                "provDerivations",
+                _.union(member.get("provDerivations"), indirectDerivationsIds),
+              );
+            }
+          });
+
+          //Add this member to the list of related models
+          if (!_.contains(relatedModelIDs, member.get("id"))) {
+            relatedModels.push(member);
+            relatedModelIDs.push(member.get("id"));
+          }
+
+          //Clear out any duplicates
+          member.set("provSources", _.uniq(member.get("provSources")));
+          member.set("provDerivations", _.uniq(member.get("provDerivations")));
+        });
+
+        //Update the list of related models
+        this.set("relatedModels", relatedModels);
+      },
+
+      downloadWithCredentials: function () {
+        //Get info about this object
+        var url = this.get("url"),
+          model = this;
+
+        //Create an XHR
+        var xhr = new XMLHttpRequest();
+        xhr.withCredentials = true;
+
+        //When the XHR is ready, create a link with the raw data (Blob) and click the link to download
+        xhr.onload = function () {
+          //Get the file name from the Content-Disposition header
+          var filename = xhr.getResponseHeader("Content-Disposition");
+
+          //As a backup, use the system metadata file name or the id
+          if (!filename) {
+            filename = model.get("filename") || model.get("id");
+          }
+
+          //Add a ".zip" extension if it doesn't exist
+          if (
+            filename.indexOf(".zip") < 0 ||
+            filename.indexOf(".zip") != filename.length - 4
+          ) {
+            filename += ".zip";
+          }
+
+          //For IE, we need to use the navigator API
+          if (navigator && navigator.msSaveOrOpenBlob) {
+            navigator.msSaveOrOpenBlob(xhr.response, filename);
+          } else {
+            var a = document.createElement("a");
+            a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
+            a.download = filename; // Set the file name.
+            a.style.display = "none";
+            document.body.appendChild(a);
+            a.click();
+            a.remove();
+          }
+
+          model.trigger("downloadComplete");
+
+          // Track this event
+          MetacatUI.analytics?.trackEvent(
+            "download",
+            "Download Package",
+            model.get("id"),
+          );
+        };
+
+        xhr.onprogress = function (e) {
+          if (e.lengthComputable) {
+            var percent = (e.loaded / e.total) * 100;
+            model.set("downloadPercent", percent);
+          }
+        };
+
+        xhr.onerror = function (e) {
+          model.trigger("downloadError");
+
+          // Track this event
+          MetacatUI.analytics?.trackEvent(
+            "download",
+            "Download Package",
+            model.get("id"),
+          );
+        };
+        //Open and send the request with the user's auth token
+        xhr.open("GET", url);
+        xhr.responseType = "blob";
+        xhr.setRequestHeader(
+          "Authorization",
+          "Bearer " + MetacatUI.appUserModel.get("token"),
+        );
+        xhr.send();
+      },
+
+      /* Returns the SolrResult that represents the metadata doc */
+      getMetadata: function () {
+        var members = this.get("members");
+        for (var i = 0; i < members.length; i++) {
+          if (members[i].get("formatType") == "METADATA") return members[i];
         }
 
-			   //For IE, we need to use the navigator API
-			   if (navigator && navigator.msSaveOrOpenBlob) {
-				   navigator.msSaveOrOpenBlob(xhr.response, filename);
-			   }
-			   else{
-					var a = document.createElement('a');
-					a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
-					a.download = filename; // Set the file name.
-					a.style.display = 'none';
-					document.body.appendChild(a);
-					a.click();
-					a.remove();
-			   }
-
-                model.trigger("downloadComplete");
-
-                // Track this event
-                MetacatUI.analytics?.trackEvent("download", "Download Package", model.get("id"))
-
-			};
-
-			xhr.onprogress = function(e){
-			    if (e.lengthComputable){
-			        var percent = (e.loaded / e.total) * 100;
-			        model.set("downloadPercent", percent);
-			    }
-			};
-
-			xhr.onerror = function (e) {
-				model.trigger("downloadError");
-
-				// Track this event
-				MetacatUI.analytics?.trackEvent(
-					"download",
-					"Download Package",
-					model.get("id")
-				);
-				
-			};
-			//Open and send the request with the user's auth token
-			xhr.open('GET', url);
-			xhr.responseType = "blob";
-			xhr.setRequestHeader("Authorization", "Bearer " + MetacatUI.appUserModel.get("token"));
-			xhr.send();
-		},
-
-		/* Returns the SolrResult that represents the metadata doc */
-		getMetadata: function(){
-			var members = this.get("members");
-			for(var i=0; i<members.length; i++){
-				if(members[i].get("formatType") == "METADATA") return members[i];
-			}
-
-			//If there are no metadata objects in this package, make sure we have searched for them already
-			if(!this.complete && !this.pending) this.getMembers();
-
-			return false;
-		},
-
-		//Check authority of the Metadata SolrResult model instead
-		checkAuthority: function(){
-
-			//Call the auth service
-			var authServiceUrl = MetacatUI.appModel.get('authServiceUrl');
-			if(!authServiceUrl) return false;
-
-			var model = this;
-
-			var requestSettings = {
-				url: authServiceUrl + encodeURIComponent(this.get("id")) + "?action=write",
-				type: "GET",
-				success: function(data, textStatus, xhr) {
-					model.set("isAuthorized", true);
-					model.trigger("change:isAuthorized");
-				},
-				error: function(xhr, textStatus, errorThrown) {
-					model.set("isAuthorized", false);
-				}
-			}
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		flagComplete: function(){
-			this.complete = true;
-			this.pending = false;
-			this.trigger("complete", this);
-		},
-
-
-		    /*
-		    * function xmlToJson - A utility function for converting XML to JSON
-		    *
-		    * @param xml {DOM Element} - An XML or HTML DOM element to convert to json
-		    * @returns {object} - A literal JS object that represents the given XML
-		    */
-        toJson: function(xml) {
-
-        	// Create the return object
-        	var obj = {};
-
-        	// do children
-        	if (xml.hasChildNodes()) {
-        		for(var i = 0; i < xml.childNodes.length; i++) {
-        			var item = xml.childNodes.item(i);
-
-        			//If it's an empty text node, skip it
-        			if((item.nodeType == 3) && (!item.nodeValue.trim()))
-        				continue;
-
-        			//Get the node name
-        			var nodeName = item.localName;
-
-        			//If it's a new container node, convert it to JSON and add as a new object attribute
-        			if((typeof(obj[nodeName]) == "undefined") && (item.nodeType == 1)) {
-        				obj[nodeName] = this.toJson(item);
-        			}
-        			//If it's a new text node, just store the text value and add as a new object attribute
-        			else if((typeof(obj[nodeName]) == "undefined") && (item.nodeType == 3)){
-        				obj = item.nodeValue;
-        			}
-        			//If this node name is already stored as an object attribute...
-        			else if(typeof(obj[nodeName]) != "undefined"){
-        				//Cache what we have now
-        				var old = obj[nodeName];
-        				if(!Array.isArray(old))
-        					old = [old];
-
-        				//Create a new object to store this node info
-        				var newNode = {};
-
-        				//Add the new node info to the existing array we have now
-    					if(item.nodeType == 1){
-    						newNode = this.toJson(item);
-    						var newArray = old.concat(newNode);
-    					}
-    					else if(item.nodeType == 3){
-    						newNode = item.nodeValue;
-    						var newArray = old.concat(newNode);
-    					}
-
-            			//Store the attributes for this node
-            			_.each(item.attributes, function(attr){
-            				newNode[attr.localName] = attr.nodeValue;
-            			});
-
-            			//Replace the old array with the updated one
-    					obj[nodeName] = newArray;
-
-    					//Exit
-    					continue;
-        			}
-
-        			//Store the attributes for this node
-        			/*_.each(item.attributes, function(attr){
+        //If there are no metadata objects in this package, make sure we have searched for them already
+        if (!this.complete && !this.pending) this.getMembers();
+
+        return false;
+      },
+
+      //Check authority of the Metadata SolrResult model instead
+      checkAuthority: function () {
+        //Call the auth service
+        var authServiceUrl = MetacatUI.appModel.get("authServiceUrl");
+        if (!authServiceUrl) return false;
+
+        var model = this;
+
+        var requestSettings = {
+          url:
+            authServiceUrl +
+            encodeURIComponent(this.get("id")) +
+            "?action=write",
+          type: "GET",
+          success: function (data, textStatus, xhr) {
+            model.set("isAuthorized", true);
+            model.trigger("change:isAuthorized");
+          },
+          error: function (xhr, textStatus, errorThrown) {
+            model.set("isAuthorized", false);
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      flagComplete: function () {
+        this.complete = true;
+        this.pending = false;
+        this.trigger("complete", this);
+      },
+
+      /*
+       * function xmlToJson - A utility function for converting XML to JSON
+       *
+       * @param xml {DOM Element} - An XML or HTML DOM element to convert to json
+       * @returns {object} - A literal JS object that represents the given XML
+       */
+      toJson: function (xml) {
+        // Create the return object
+        var obj = {};
+
+        // do children
+        if (xml.hasChildNodes()) {
+          for (var i = 0; i < xml.childNodes.length; i++) {
+            var item = xml.childNodes.item(i);
+
+            //If it's an empty text node, skip it
+            if (item.nodeType == 3 && !item.nodeValue.trim()) continue;
+
+            //Get the node name
+            var nodeName = item.localName;
+
+            //If it's a new container node, convert it to JSON and add as a new object attribute
+            if (typeof obj[nodeName] == "undefined" && item.nodeType == 1) {
+              obj[nodeName] = this.toJson(item);
+            }
+            //If it's a new text node, just store the text value and add as a new object attribute
+            else if (
+              typeof obj[nodeName] == "undefined" &&
+              item.nodeType == 3
+            ) {
+              obj = item.nodeValue;
+            }
+            //If this node name is already stored as an object attribute...
+            else if (typeof obj[nodeName] != "undefined") {
+              //Cache what we have now
+              var old = obj[nodeName];
+              if (!Array.isArray(old)) old = [old];
+
+              //Create a new object to store this node info
+              var newNode = {};
+
+              //Add the new node info to the existing array we have now
+              if (item.nodeType == 1) {
+                newNode = this.toJson(item);
+                var newArray = old.concat(newNode);
+              } else if (item.nodeType == 3) {
+                newNode = item.nodeValue;
+                var newArray = old.concat(newNode);
+              }
+
+              //Store the attributes for this node
+              _.each(item.attributes, function (attr) {
+                newNode[attr.localName] = attr.nodeValue;
+              });
+
+              //Replace the old array with the updated one
+              obj[nodeName] = newArray;
+
+              //Exit
+              continue;
+            }
+
+            //Store the attributes for this node
+            /*_.each(item.attributes, function(attr){
         				obj[nodeName][attr.localName] = attr.nodeValue;
         			});*/
+          }
+        }
+        return obj;
+      },
 
-    			}
-
-        	}
-        	return obj;
-        },
-
-		//Sums up the byte size of each member
-		getTotalSize: function(){
-			if(this.get("totalSize")) return this.get("totalSize");
-
-			if(this.get("members").length == 1){
-				var totalSize = this.get("members")[0].get("size");
-			}
-			else{
-				var totalSize = _.reduce(this.get("members"), function(sum, member){
-					if(typeof sum == "object")
-						sum = sum.get("size");
-
-					return sum + member.get("size");
-				});
-			}
-
-			this.set("totalSize", totalSize);
-			return totalSize;
-		},
-
-		/****************************/
-		/**
-		 * Convert number of bytes into human readable format
-		 *
-		 * @param integer bytes     Number of bytes to convert
-		 * @param integer precision Number of digits after the decimal separator
-		 * @return string
-		 */
-		bytesToSize: function(bytes, precision){
-		    var kibibyte = 1024;
-		    var mebibyte = kibibyte * 1024;
-		    var gibibyte = mebibyte * 1024;
-		    var tebibyte = gibibyte * 1024;
-
-		    if(typeof bytes === "undefined") var bytes = this.get("size");
-
-		    if ((bytes >= 0) && (bytes < kibibyte)) {
-		        return bytes + ' B';
-
-		    } else if ((bytes >= kibibyte) && (bytes < mebibyte)) {
-		        return (bytes / kibibyte).toFixed(precision) + ' KiB';
-
-		    } else if ((bytes >= mebibyte) && (bytes < gibibyte)) {
-		        return (bytes / mebibyte).toFixed(precision) + ' MiB';
-
-		    } else if ((bytes >= gibibyte) && (bytes < tebibyte)) {
-		        return (bytes / gibibyte).toFixed(precision) + ' GiB';
-
-		    } else if (bytes >= tebibyte) {
-		        return (bytes / tebibyte).toFixed(precision) + ' TiB';
-
-		    } else {
-		        return bytes + ' B';
-		    }
-		}
-
-	});
-	return PackageModel;
+      //Sums up the byte size of each member
+      getTotalSize: function () {
+        if (this.get("totalSize")) return this.get("totalSize");
+
+        if (this.get("members").length == 1) {
+          var totalSize = this.get("members")[0].get("size");
+        } else {
+          var totalSize = _.reduce(this.get("members"), function (sum, member) {
+            if (typeof sum == "object") sum = sum.get("size");
+
+            return sum + member.get("size");
+          });
+        }
+
+        this.set("totalSize", totalSize);
+        return totalSize;
+      },
+
+      /****************************/
+      /**
+       * Convert number of bytes into human readable format
+       *
+       * @param integer bytes     Number of bytes to convert
+       * @param integer precision Number of digits after the decimal separator
+       * @return string
+       */
+      bytesToSize: function (bytes, precision) {
+        var kibibyte = 1024;
+        var mebibyte = kibibyte * 1024;
+        var gibibyte = mebibyte * 1024;
+        var tebibyte = gibibyte * 1024;
+
+        if (typeof bytes === "undefined") var bytes = this.get("size");
+
+        if (bytes >= 0 && bytes < kibibyte) {
+          return bytes + " B";
+        } else if (bytes >= kibibyte && bytes < mebibyte) {
+          return (bytes / kibibyte).toFixed(precision) + " KiB";
+        } else if (bytes >= mebibyte && bytes < gibibyte) {
+          return (bytes / mebibyte).toFixed(precision) + " MiB";
+        } else if (bytes >= gibibyte && bytes < tebibyte) {
+          return (bytes / gibibyte).toFixed(precision) + " GiB";
+        } else if (bytes >= tebibyte) {
+          return (bytes / tebibyte).toFixed(precision) + " TiB";
+        } else {
+          return bytes + " B";
+        }
+      },
+    },
+  );
+  return PackageModel;
 });
 
diff --git a/docs/docs/src_js_models_QualityCheckModel.js.html b/docs/docs/src_js_models_QualityCheckModel.js.html index 68181f535..357f5fb74 100644 --- a/docs/docs/src_js_models_QualityCheckModel.js.html +++ b/docs/docs/src_js_models_QualityCheckModel.js.html @@ -44,45 +44,42 @@

Source: src/js/models/QualityCheckModel.js

-
/* global define */
-"use strict";
-
-define(['jquery', 'underscore', 'backbone'], function ($, _, Backbone) {
-
-    /**
-     * @class QualityCheck
-     * @classdesc This model represents a single metadata quality check. Currently, This
-     * model is only used when an entire quality suite resuslt is fetched from
-     * the quality server (and all quality checks are populated), but in the
-     * future it may be used to request/fetch quality result for a single
-     * quality check (and not an entire suite).
-     * @classcategory Models
-     * @extends Backbone.Model
-     */
-    var QualityCheck = Backbone.Model.extend(
-        /** @lends QualityCheck.prototype */{
-
-            /* The default object format fields */
-            defaults: function () {
-                return {
-                    check: null,
-                    output: null,
-                    status: null,
-                    timestamp: null
-                };
-            },
-
-            /* Constructs a new instance */
-            initialize: function (attrs, options) {
-            },
-
-            /* No op - Formats are read only */
-            save: function () {
-                return false;
-            }
-        });
-
-    return QualityCheck;
+            
"use strict";
+
+define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @class QualityCheck
+   * @classdesc This model represents a single metadata quality check. Currently, This
+   * model is only used when an entire quality suite resuslt is fetched from
+   * the quality server (and all quality checks are populated), but in the
+   * future it may be used to request/fetch quality result for a single
+   * quality check (and not an entire suite).
+   * @classcategory Models
+   * @extends Backbone.Model
+   */
+  var QualityCheck = Backbone.Model.extend(
+    /** @lends QualityCheck.prototype */ {
+      /* The default object format fields */
+      defaults: function () {
+        return {
+          check: null,
+          output: null,
+          status: null,
+          timestamp: null,
+        };
+      },
+
+      /* Constructs a new instance */
+      initialize: function (attrs, options) {},
+
+      /* No op - Formats are read only */
+      save: function () {
+        return false;
+      },
+    },
+  );
+
+  return QualityCheck;
 });
 
diff --git a/docs/docs/src_js_models_Search.js.html b/docs/docs/src_js_models_Search.js.html index 2e4a7b565..42343ae7c 100644 --- a/docs/docs/src_js_models_Search.js.html +++ b/docs/docs/src_js_models_Search.js.html @@ -44,1222 +44,1430 @@

Source: src/js/models/Search.js

-
/*global define */
-define(["jquery", "underscore", "backbone", "models/SolrResult", "collections/Filters"],
-    function($, _, Backbone, SolrResult, Filters) {
-        'use strict';
-
-        /**
-         * @class Search
-         * @classdesc Search filters can be either plain text or a filter object with the following options:
-         * filterLabel - text that will be displayed in the filter element in the UI
-         * label - text that will be displayed in the autocomplete  list
-         * value - the value that will be included in the query
-         * description - a longer text description of the filter value
-         * Example: {filterLabel: "Creator", label: "Jared Kibele (16)", value: "Kibele", description: "Search for data creators"}
-         * @classcategory Models
-         * @extends Backbone.Model
-         * @constructor
-         */
-        var Search = Backbone.Model.extend(
-          /** @lends Search.prototype */{
-
-            /**
-            * @type {object}
-            * @property {Filters} filters - The collection of filters used to build a query, an instance of Filters
-            */
-            defaults: function() {
-                return {
-                    all: [],
-                    projectText: [],
-                    creator: [],
-                    taxon: [],
-                    isPrivate: null,
-                    documents: false,
-                    resourceMap: false,
-                    yearMin: 1900, //The user-selected minimum year
-                    yearMax: new Date().getUTCFullYear(), //The user-selected maximum year
-                    pubYear: false,
-                    dataYear: false,
-                    sortOrder: 'dateUploaded+desc',
-                    sortByReads: false, // True if we can sort by reads/popularity
-                    east: null,
-                    west: null,
-                    north: null,
-                    south: null,
-                    useGeohash: true,
-                    geohashes: [],
-                    geohashLevel: 9,
-                    geohashGroups: {},
-                    dataSource: [],
-                    username: [],
-                    rightsHolder: [],
-                    submitter: [],
-                    spatial: [],
-                    attribute: [],
-                    sem_annotation: [],
-                    annotation: [],
-                    additionalCriteria: [],
-                    id: [],
-                    seriesId: [],
-                    idOnly: [],
-                    provFields: [],
-                    formatType: [{
-                        value: "METADATA",
-                        label: "science metadata",
-                        description: null
-                    }],
-                    exclude: [{
-                        field: "obsoletedBy",
-                        value: "*"
-                    },
-                    {
-                      field: "formatId",
-                      value: "*dataone.org/collections*"
-                    },
-                    {
-                      field: "formatId",
-                      value: "*dataone.org/portals*"
-                    }],
-                    filters: null
-                }
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/SolrResult",
+  "collections/Filters",
+], function ($, _, Backbone, SolrResult, Filters) {
+  "use strict";
+
+  /**
+   * @class Search
+   * @classdesc Search filters can be either plain text or a filter object with the following options:
+   * filterLabel - text that will be displayed in the filter element in the UI
+   * label - text that will be displayed in the autocomplete  list
+   * value - the value that will be included in the query
+   * description - a longer text description of the filter value
+   * Example: {filterLabel: "Creator", label: "Jared Kibele (16)", value: "Kibele", description: "Search for data creators"}
+   * @classcategory Models
+   * @extends Backbone.Model
+   * @constructor
+   */
+  var Search = Backbone.Model.extend(
+    /** @lends Search.prototype */ {
+      /**
+       * @type {object}
+       * @property {Filters} filters - The collection of filters used to build a query, an instance of Filters
+       */
+      defaults: function () {
+        return {
+          all: [],
+          projectText: [],
+          creator: [],
+          taxon: [],
+          isPrivate: null,
+          documents: false,
+          resourceMap: false,
+          yearMin: 1900, //The user-selected minimum year
+          yearMax: new Date().getUTCFullYear(), //The user-selected maximum year
+          pubYear: false,
+          dataYear: false,
+          sortOrder: "dateUploaded+desc",
+          sortByReads: false, // True if we can sort by reads/popularity
+          east: null,
+          west: null,
+          north: null,
+          south: null,
+          useGeohash: true,
+          geohashes: [],
+          geohashLevel: 9,
+          geohashGroups: {},
+          dataSource: [],
+          username: [],
+          rightsHolder: [],
+          submitter: [],
+          spatial: [],
+          attribute: [],
+          sem_annotation: [],
+          annotation: [],
+          additionalCriteria: [],
+          id: [],
+          seriesId: [],
+          idOnly: [],
+          provFields: [],
+          formatType: [
+            {
+              value: "METADATA",
+              label: "science metadata",
+              description: null,
             },
-
-            //A list of all the filter names that are related to the spatial/map filter
-            spatialFilters: ["useGeohash", "geohashes", "geohashLevel",
-                "geohashGroups", "east", "west", "north", "south"],
-
-            initialize: function() {
-                this.listenTo(this, "change:geohashes", this.groupGeohashes);
+          ],
+          exclude: [
+            {
+              field: "obsoletedBy",
+              value: "*",
             },
-
-            fieldLabels: {
-                attribute: "Data attribute",
-                documents: "Only results with data",
-                annotation: "Annotation",
-                dataSource: "Data source",
-                creator: "Creator",
-                dataYear: "Data coverage",
-                pubYear: "Publish year",
-                id: "Identifier",
-                seriesId: "seriesId",
-                taxon: "Taxon",
-                spatial: "Location",
-                isPrivate: "Private datasets",
-                all: "",
-                projectText: "Project",
+            {
+              field: "formatId",
+              value: "*dataone.org/collections*",
             },
-
-            //Map the filter names to their index field names
-            fieldNameMap: {
-                attribute: "attribute",
-                annotation: "sem_annotation",
-                dataSource: "datasource",
-                documents: "documents",
-                formatType: "formatType",
-                all: "",
-                creator: "originText",
-                spatial: "siteText",
-                resourceMap: "resourceMap",
-                pubYear: ["datePublished", "dateUploaded"],
-                id: ["id", "identifier", "documents", "resourceMap", "seriesId"],
-                idOnly: ["id", "seriesId"],
-                rightsHolder: "rightsHolder",
-                submitter: "submitter",
-                username: ["rightsHolder", "writePermission", "changePermission"],
-                taxon: ["kingdom", "phylum", "class", "order", "family", "genus", "species"],
-                isPrivate: "isPublic",
-                projectText: "projectText"
-            },
-
-            facetNameMap: {
-                "creator": "origin",
-                "attribute": "attribute",
-                "annotation": "sem_annotation",
-                "spatial": "site",
-                "taxon": ["kingdom", "phylum", "class", "order", "family", "genus", "species"],
-                "isPublic": "isPublic",
-                "all": "keywords",
-                "projectText": "project"
+            {
+              field: "formatId",
+              value: "*dataone.org/portals*",
             },
+          ],
+          filters: null,
+        };
+      },
+
+      //A list of all the filter names that are related to the spatial/map filter
+      spatialFilters: [
+        "useGeohash",
+        "geohashes",
+        "geohashLevel",
+        "geohashGroups",
+        "east",
+        "west",
+        "north",
+        "south",
+      ],
+
+      initialize: function () {
+        this.listenTo(this, "change:geohashes", this.groupGeohashes);
+      },
+
+      fieldLabels: {
+        attribute: "Data attribute",
+        documents: "Only results with data",
+        annotation: "Annotation",
+        dataSource: "Data source",
+        creator: "Creator",
+        dataYear: "Data coverage",
+        pubYear: "Publish year",
+        id: "Identifier",
+        seriesId: "seriesId",
+        taxon: "Taxon",
+        spatial: "Location",
+        isPrivate: "Private datasets",
+        all: "",
+        projectText: "Project",
+      },
+
+      //Map the filter names to their index field names
+      fieldNameMap: {
+        attribute: "attribute",
+        annotation: "sem_annotation",
+        dataSource: "datasource",
+        documents: "documents",
+        formatType: "formatType",
+        all: "",
+        creator: "originText",
+        spatial: "siteText",
+        resourceMap: "resourceMap",
+        pubYear: ["datePublished", "dateUploaded"],
+        id: ["id", "identifier", "documents", "resourceMap", "seriesId"],
+        idOnly: ["id", "seriesId"],
+        rightsHolder: "rightsHolder",
+        submitter: "submitter",
+        username: ["rightsHolder", "writePermission", "changePermission"],
+        taxon: [
+          "kingdom",
+          "phylum",
+          "class",
+          "order",
+          "family",
+          "genus",
+          "species",
+        ],
+        isPrivate: "isPublic",
+        projectText: "projectText",
+      },
+
+      facetNameMap: {
+        creator: "origin",
+        attribute: "attribute",
+        annotation: "sem_annotation",
+        spatial: "site",
+        taxon: [
+          "kingdom",
+          "phylum",
+          "class",
+          "order",
+          "family",
+          "genus",
+          "species",
+        ],
+        isPublic: "isPublic",
+        all: "keywords",
+        projectText: "project",
+      },
+
+      getCurrentFilters: function () {
+        var changedAttr = this.changedAttributes(_.clone(this.defaults())),
+          currentFilters = Object.keys(changedAttr),
+          ignoreAttr = ["sortOrder", "provFields"];
+
+        if (!changedAttr) return new Array();
+
+        //Check for changed attributes that should be ignored
+        _.each(
+          Object.keys(changedAttr),
+          function (attr) {
+            //If the value is an empty array, but the default value is an empty array too,
+            // then it's not a changed filter attribute
+            if (
+              Array.isArray(this.get(attr)) &&
+              this.get(attr).length == 0 &&
+              Array.isArray(this.defaults()[attr]) &&
+              this.defaults()[attr].length == 0
+            ) {
+              currentFilters = _.without(currentFilters, attr);
+            } else if (ignoreAttr.includes(attr)) {
+              currentFilters = _.without(currentFilters, attr);
+            }
+          },
+          this,
+        );
+
+        //Don't count the geohashes or directions as a filter if the geohash filter is turned off
+        if (!this.get("useGeohash")) {
+          currentFilters = _.difference(currentFilters, this.spatialFilters);
+        }
+
+        return currentFilters;
+      },
+
+      filterCount: function () {
+        var currentFilters = this.getCurrentFilters();
+
+        return currentFilters.length;
+      },
+
+      //Function filterIsAvailable will check if a filter is available in this search index -
+      //if the filter name if included in the defaults of this model, it is marked as available.
+      //Comment out or remove defaults that are not in the index or should not be included in queries
+      filterIsAvailable: function (name) {
+        //Get the keys for this model as a way to list the filters that are available
+        var defaults = _.keys(this.defaults());
+        if (_.indexOf(defaults, name) >= 0) {
+          return true;
+        } else {
+          return false;
+        }
+      },
+
+      /*
+       * Removes a specified filter from the search model
+       */
+      removeFromModel: function (category, filterValueToRemove) {
+        //Remove this filter term from the model
+        if (category) {
+          //Get the current filter terms array
+          var currentFilterValues = this.get(category);
+
+          //The year filters have special rules
+          //If both year types will be reset/default, then also reset the year min and max values
+          if (category == "pubYear" || category == "dataYear") {
+            var otherType = category == "pubYear" ? "dataYear" : "pubYear";
+
+            if (_.contains(this.getCurrentFilters(), otherType))
+              var newFilterValues = this.defaults()[category];
+            else {
+              this.set(category, this.defaults()[category]);
+              this.set("yearMin", this.defaults()["yearMin"]);
+              this.set("yearMax", this.defaults()["yearMax"]);
+              return;
+            }
+          } else if (Array.isArray(currentFilterValues)) {
+            //Remove this filter term from the array
+            var newFilterValues = _.without(
+              currentFilterValues,
+              filterValueToRemove,
+            );
+            _.each(currentFilterValues, function (currentFilterValue, key) {
+              var valueString =
+                typeof currentFilterValue == "object"
+                  ? currentFilterValue.value
+                  : currentFilterValue;
+              if (valueString == filterValueToRemove) {
+                newFilterValues = _.without(
+                  newFilterValues,
+                  currentFilterValue,
+                );
+              }
+            });
+          } else {
+            //Get the default value
+            var newFilterValues = this.defaults()[category];
+          }
+
+          //Set the new value
+          this.set(category, newFilterValues);
+        }
+      },
+
+      /**
+       *
+       * @param {Filters|Filter[]} filters The collection of filters to add to this model OR an array of Filter models
+       */
+      addFilters: function (filters) {
+        try {
+          let currentFilters = this.get("filters");
+
+          //If the passed collection is the same as the one set already, return
+          if (currentFilters == filters) return;
+          //If the given Filters collec is different than the one set on the model now, combine them
+          else if (
+            Filters.isPrototypeOf(currentFilters) &&
+            Filters.isPrototypeOf(filters)
+          ) {
+            filters.models.forEach((f) => {
+              currentFilters.add(f);
+            });
+            this.set("filters", currentFilters);
+          } else if (
+            Filters.isPrototypeOf(currentFilters) &&
+            Array.isArray(filters)
+          ) {
+            filters.forEach((f) => {
+              currentFilters.add(f);
+            });
+            this.set("filters", currentFilters);
+          } else if (!currentFilters) this.set("filters", new Filters(filters));
+        } catch (e) {
+          console.error("Couldn't add Filters to the Search model: ", e);
+        }
+      },
+
+      /*
+       * Resets the geoashes and geohashLevel filters to default
+       */
+      resetGeohash: function () {
+        this.set("geohashes", this.defaults().geohashes);
+        this.set("geohashLevel", this.defaults().geohashLevel);
+        this.set("geohashGroups", this.defaults().geohashGroups);
+      },
+
+      groupGeohashes: function () {
+        //Find out if there are any geohashes that can be combined together, by looking for all 32 geohashes within the same precision level
+        var sortedGeohashes = this.get("geohashes");
+        sortedGeohashes.sort();
+
+        var groupedGeohashes = _.groupBy(sortedGeohashes, function (n) {
+          return n.substring(0, n.length - 1);
+        });
 
-            getCurrentFilters: function() {
-                var changedAttr = this.changedAttributes(_.clone(this.defaults())),
-                    currentFilters = Object.keys(changedAttr),
-                    ignoreAttr = ["sortOrder", "provFields"];
-
-                if (!changedAttr) return new Array();
-
-                //Check for changed attributes that should be ignored
-                _.each( Object.keys(changedAttr), function(attr){
-
-                  //If the value is an empty array, but the default value is an empty array too,
-                  // then it's not a changed filter attribute
-                  if( Array.isArray(this.get(attr)) && this.get(attr).length == 0 &&
-                      Array.isArray(this.defaults()[attr]) && this.defaults()[attr].length == 0 ){
-                    currentFilters = _.without(currentFilters, attr);
-                  }
-                  else if( ignoreAttr.includes(attr) ){
-                    currentFilters = _.without(currentFilters, attr);
-                  }
-                }, this);
-
-                //Don't count the geohashes or directions as a filter if the geohash filter is turned off
-                if (!this.get("useGeohash")) {
-                    currentFilters = _.difference(currentFilters, this.spatialFilters);
-                }
-
-                return currentFilters;
-            },
-
-            filterCount: function() {
-                var currentFilters = this.getCurrentFilters();
-
-                return currentFilters.length;
-            },
-
-            //Function filterIsAvailable will check if a filter is available in this search index -
-            //if the filter name if included in the defaults of this model, it is marked as available.
-            //Comment out or remove defaults that are not in the index or should not be included in queries
-            filterIsAvailable: function(name) {
-                //Get the keys for this model as a way to list the filters that are available
-                var defaults = _.keys(this.defaults());
-                if (_.indexOf(defaults, name) >= 0) {
-                    return true;
-                } else {
-                    return false;
-                }
-            },
-
-            /*
-             * Removes a specified filter from the search model
-             */
-            removeFromModel: function(category, filterValueToRemove) {
-                //Remove this filter term from the model
-                if (category) {
-                    //Get the current filter terms array
-                    var currentFilterValues = this.get(category);
-
-                    //The year filters have special rules
-                    //If both year types will be reset/default, then also reset the year min and max values
-                    if ((category == "pubYear") || (category == "dataYear")) {
-                        var otherType = (category == "pubYear") ? "dataYear" : "pubYear";
-
-                        if (_.contains(this.getCurrentFilters(), otherType))
-                            var newFilterValues = this.defaults()[category];
-                        else {
-                            this.set(category, this.defaults()[category]);
-                            this.set("yearMin", this.defaults()["yearMin"]);
-                            this.set("yearMax", this.defaults()["yearMax"]);
-                            return;
-                        }
-
-                    } else if (Array.isArray(currentFilterValues)) {
-                        //Remove this filter term from the array
-                        var newFilterValues = _.without(currentFilterValues, filterValueToRemove);
-                        _.each(currentFilterValues, function(currentFilterValue, key) {
-                            var valueString = (typeof currentFilterValue == "object") ? currentFilterValue.value : currentFilterValue;
-                            if (valueString == filterValueToRemove) {
-                                newFilterValues = _.without(newFilterValues, currentFilterValue);
-                            }
-                        });
-                    } else {
-                        //Get the default value
-                        var newFilterValues = this.defaults()[category];
-                    }
-
-                    //Set the new value
-                    this.set(category, newFilterValues);
-
-                }
-            },
+        //Find groups of geohashes that makeup a complete geohash tile (32) so we can shorten the query
+        var completeGroups = _.filter(
+          Object.keys(groupedGeohashes),
+          function (n) {
+            return groupedGeohashes[n].length == 32;
+          },
+        );
+        //Find the remaining incomplete geohash groupss
+        var incompleteGroups = [];
+        _.each(
+          _.filter(Object.keys(groupedGeohashes), function (n) {
+            return groupedGeohashes[n].length < 32;
+          }),
+          function (n) {
+            incompleteGroups.push(groupedGeohashes[n]);
+          },
+        );
+        incompleteGroups = _.flatten(incompleteGroups);
+
+        //Start a geohash group object
+        var geohashGroups = {};
+        if (
+          typeof incompleteGroups !== "undefined" &&
+          incompleteGroups.length > 0
+        ) {
+          geohashGroups[incompleteGroups[0].length.toString()] =
+            incompleteGroups;
+        }
+        if (
+          typeof completeGroups !== "undefined" &&
+          completeGroups.length > 0
+        ) {
+          geohashGroups[completeGroups[0].length.toString()] = completeGroups;
+        }
+        //Save it
+        this.set("geohashGroups", geohashGroups);
+        this.trigger("change", "geohashGroups");
+      },
+
+      hasGeohashFilter: function () {
+        var currentGeohashFilter = this.get("geohashGroups");
+        return (
+          typeof currentGeohashFilter == "object" &&
+          Object.keys(currentGeohashFilter).length > 0
+        );
+      },
+
+      /**
+       * Builds the query string to send to the query engine. Goes over each filter specified in this model and adds to the query string.
+       * Some filters have special rules on how to format the query, which are built first, then the remaining filters are tacked on to the
+       * query string as a basic name:value pair. These "other filters" are specified in the otherFilters variable.
+       * @param {string} filter - A single filter to get a query fragment for
+       * @param {object} options - Additional options for this function
+       * @property {boolean} options.forPOST - If true, the query will not be url-encoded, for POST requests
+       */
+      getQuery: function (filter, options) {
+        //----All other filters with a basic name:value pair pattern----
+        var otherFilters = [
+          "attribute",
+          "formatType",
+          "rightsHolder",
+          "submitter",
+        ];
+
+        //Start the query string
+        var query = "",
+          forPOST = false;
+
+        //See if we are looking for a sub-query or a query for all filters
+        if (typeof filter == "undefined") {
+          var filter = null;
+          var getAll = true;
+        } else {
+          var getAll = false;
+        }
+
+        //Get the options sent to this function via the options object
+        if (typeof options == "object" && options) {
+          forPOST = options.forPOST;
+        }
+
+        var model = this;
+
+        //-----Annotation-----
+        if (
+          this.filterIsAvailable("annotation") &&
+          (filter == "annotation" || getAll)
+        ) {
+          var annotations = this.get("annotation");
+          _.each(annotations, function (annotationFilter, key, list) {
+            var filterValue = "";
+
+            //Get the filter value
+            if (typeof annotationFilter == "object") {
+              filterValue = annotationFilter.value || "";
+            } else {
+              filterValue = annotationFilter;
+            }
 
-            /**
-             * 
-             * @param {Filters|Filter[]} filters The collection of filters to add to this model OR an array of Filter models
-             */
-            addFilters: function(filters){
-              try{
-
-                let currentFilters = this.get("filters");
-
-                //If the passed collection is the same as the one set already, return
-                if( currentFilters == filters )
-                  return;
-                //If the given Filters collec is different than the one set on the model now, combine them
-                else if( Filters.isPrototypeOf(currentFilters) && Filters.isPrototypeOf(filters) ){
-                  filters.models.forEach(f => { currentFilters.add(f) });
-                  this.set("filters", currentFilters);
-                }
-                else if( Filters.isPrototypeOf(currentFilters) && Array.isArray(filters) ){
-                  filters.forEach(f => { currentFilters.add(f) });
-                  this.set("filters", currentFilters);
-                }
-                else if( !currentFilters )
-                  this.set("filters", new Filters(filters));
-                
-              }
-              catch(e){
-                console.error("Couldn't add Filters to the Search model: ", e);
-              }
-            },
+            // Trim leading and trailing whitespace just in case
+            filterValue = filterValue.trim();
 
-            /*
-             * Resets the geoashes and geohashLevel filters to default
-             */
-            resetGeohash: function() {
-                this.set("geohashes", this.defaults().geohashes);
-                this.set("geohashLevel", this.defaults().geohashLevel);
-                this.set("geohashGroups", this.defaults().geohashGroups);
-            },
+            if (forPOST) {
+              // Encode and wrap URI in urlencoded double quote chars
+              filterValue = '"' + filterValue.trim() + '"';
+            } else {
+              // Encode and wrap URI in urlencoded double quote chars
+              filterValue =
+                "%22" + encodeURIComponent(filterValue.trim()) + "%22";
+            }
 
-            groupGeohashes: function() {
-                //Find out if there are any geohashes that can be combined together, by looking for all 32 geohashes within the same precision level
-                var sortedGeohashes = this.get("geohashes");
-                sortedGeohashes.sort();
+            query += model.fieldNameMap["annotation"] + ":" + filterValue;
+          });
+        }
+
+        //---Identifier---
+        if (
+          this.filterIsAvailable("id") &&
+          (filter == "id" || getAll) &&
+          this.get("id").length
+        ) {
+          var identifiers = this.get("id");
+
+          if (Array.isArray(identifiers)) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                var groupedGeohashes = _.groupBy(sortedGeohashes, function(n) {
-                    return n.substring(0, n.length - 1);
-                });
+            query += this.getGroupedQuery(
+              this.fieldNameMap["id"],
+              identifiers,
+              {
+                operator: "OR",
+                subtext: true,
+              },
+            );
+          } else if (identifiers) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                //Find groups of geohashes that makeup a complete geohash tile (32) so we can shorten the query
-                var completeGroups = _.filter(Object.keys(groupedGeohashes), function(n) {
-                    return (groupedGeohashes[n].length == 32)
-                });
-                //Find the remaining incomplete geohash groupss
-                var incompleteGroups = [];
-                _.each(_.filter(Object.keys(groupedGeohashes), function(n) {
-                    return (groupedGeohashes[n].length < 32)
-                }), function(n) {
-                    incompleteGroups.push(groupedGeohashes[n]);
-                });
-                incompleteGroups = _.flatten(incompleteGroups);
+            if (forPOST) {
+              query +=
+                this.fieldNameMap["id"] +
+                ":*" +
+                this.escapeSpecialChar(identifiers) +
+                "*";
+            } else {
+              query +=
+                this.fieldNameMap["id"] +
+                ":*" +
+                this.escapeSpecialChar(encodeURIComponent(identifiers)) +
+                "*";
+            }
+          }
+        }
+
+        //---resourceMap---
+        if (
+          this.filterIsAvailable("resourceMap") &&
+          (filter == "resourceMap" || getAll)
+        ) {
+          var resourceMap = this.get("resourceMap");
+
+          //If the resource map search setting is a list of resource map IDs
+          if (Array.isArray(resourceMap)) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                //Start a geohash group object
-                var geohashGroups = {};
-                if ((typeof incompleteGroups !== "undefined") && (incompleteGroups.length > 0)) {
-                    geohashGroups[incompleteGroups[0].length.toString()] = incompleteGroups;
-                }
-                if ((typeof completeGroups !== "undefined") && (completeGroups.length > 0)) {
-                    geohashGroups[completeGroups[0].length.toString()] = completeGroups;
-                }
-                //Save it
-                this.set("geohashGroups", geohashGroups);
-                this.trigger("change", "geohashGroups");
-            },
+            query += this.getGroupedQuery(
+              this.fieldNameMap["resourceMap"],
+              resourceMap,
+              {
+                operator: "OR",
+                forPOST: forPOST,
+              },
+            );
+          } else if (resourceMap) {
+            if (query.length) {
+              query += " AND ";
+            }
+            //Otherwise, treat it as a binary setting
+            query += this.fieldNameMap["resourceMap"] + ":*";
+          }
+        }
+
+        //---documents---
+        if (
+          this.filterIsAvailable("documents") &&
+          (filter == "documents" || getAll)
+        ) {
+          var documents = this.get("documents");
+
+          //If the documents search setting is a list ofdocuments IDs
+          if (Array.isArray(documents)) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-            hasGeohashFilter: function(){
-              var currentGeohashFilter = this.get("geohashGroups");
-              return (typeof currentGeohashFilter == "object" && Object.keys(currentGeohashFilter).length > 0);
-            },
+            query += this.getGroupedQuery(
+              this.fieldNameMap["documents"],
+              documents,
+              {
+                operator: "OR",
+                forPOST: forPOST,
+              },
+            );
+          } else if (documents) {
+            if (query.length) {
+              query += " AND ";
+            }
+            //Otherwise, treat it as a binary setting
+            query += this.fieldNameMap["documents"] + ":*";
+          }
+        }
+
+        //---Username: search for this username in rightsHolder and submitter ---
+        if (
+          this.filterIsAvailable("username") &&
+          (filter == "username" || getAll) &&
+          this.get("username").length
+        ) {
+          var username = this.get("username");
+          if (username) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-            /**
-             * Builds the query string to send to the query engine. Goes over each filter specified in this model and adds to the query string.
-             * Some filters have special rules on how to format the query, which are built first, then the remaining filters are tacked on to the
-             * query string as a basic name:value pair. These "other filters" are specified in the otherFilters variable.
-             * @param {string} filter - A single filter to get a query fragment for
-             * @param {object} options - Additional options for this function
-             * @property {boolean} options.forPOST - If true, the query will not be url-encoded, for POST requests
-             */
-            getQuery: function(filter, options) {
-
-                //----All other filters with a basic name:value pair pattern----
-                var otherFilters = ["attribute", "formatType", "rightsHolder", "submitter"];
-
-                //Start the query string
-                var query = "",
-                    forPOST = false;
-
-                //See if we are looking for a sub-query or a query for all filters
-                if (typeof filter == "undefined") {
-                    var filter = null;
-                    var getAll = true;
-                } else {
-                    var getAll = false;
-                }
+            query += this.getGroupedQuery(
+              this.fieldNameMap["username"],
+              username,
+              {
+                operator: "OR",
+                forPOST: forPOST,
+              },
+            );
+          }
+        }
+
+        //--- ID Only - searches only the id and seriesId fields ---
+        if (
+          this.filterIsAvailable("idOnly") &&
+          (filter == "idOnly" || getAll) &&
+          this.get("idOnly").length
+        ) {
+          var idOnly = this.get("idOnly");
+          if (idOnly) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                //Get the options sent to this function via the options object
-                if( typeof options == "object" && options ){
-                  forPOST = options.forPOST;
-                }
+            query += this.getGroupedQuery(this.fieldNameMap["idOnly"], idOnly, {
+              operator: "OR",
+              forPOST: forPOST,
+            });
+          }
+        }
+
+        //---Taxon---
+        if (
+          this.filterIsAvailable("taxon") &&
+          (filter == "taxon" || getAll) &&
+          this.get("taxon").length
+        ) {
+          var taxon = this.get("taxon");
+
+          for (var i = 0; i < taxon.length; i++) {
+            var value =
+              typeof taxon == "object" ? taxon[i].value : taxon[i].trim();
+
+            query += this.getMultiFieldQuery(
+              this.fieldNameMap["taxon"],
+              value,
+              {
+                subtext: true,
+                forPOST: forPOST,
+              },
+            );
+          }
+        }
+
+        //------Pub Year-----
+        if (
+          this.filterIsAvailable("pubYear") &&
+          (filter == "pubYear" || getAll)
+        ) {
+          //Get the types of year to be searched first
+          var pubYear = this.get("pubYear");
+          if (pubYear) {
+            //Get the minimum and maximum years chosen
+            var yearMin = this.get("yearMin");
+            var yearMax = this.get("yearMax");
+
+            if (query.length) {
+              query += " AND ";
+            }
 
-                var model = this;
-
-                //-----Annotation-----
-                if (this.filterIsAvailable("annotation") && ((filter == "annotation") || getAll)) {
-                    var annotations = this.get("annotation");
-                    _.each(annotations, function(annotationFilter, key, list) {
-                        var filterValue = "";
-
-                        //Get the filter value
-                        if (typeof annotationFilter == "object") {
-                            filterValue = annotationFilter.value || "";
-                        } else {
-                            filterValue = annotationFilter;
-                        }
-
-                        // Trim leading and trailing whitespace just in case
-                        filterValue = filterValue.trim();
-
-                        if( forPOST ){
-                          // Encode and wrap URI in urlencoded double quote chars
-                          filterValue = '"' + filterValue.trim() + '"';
-                        }
-                        else{
-                          // Encode and wrap URI in urlencoded double quote chars
-                          filterValue = "%22" + encodeURIComponent(filterValue.trim()) + "%22";
-                        }
-
-                        query += model.fieldNameMap["annotation"] + ":" + filterValue;
-                    });
-                }
+            var value =
+              "[" +
+              yearMin +
+              "-01-01T00:00:00Z TO " +
+              yearMax +
+              "-12-31T00:00:00Z]";
+            var opts = {
+              forPOST: forPOST,
+              escapeSquareBrackets: false,
+            };
+
+            //Add to the query if we are searching publication year
+            query += this.getMultiFieldQuery(
+              this.fieldNameMap["pubYear"],
+              value,
+              opts,
+            );
+          }
+        }
+
+        //-----Data year------
+        if (
+          this.filterIsAvailable("dataYear") &&
+          (filter == "dataYear" || getAll)
+        ) {
+          var dataYear = this.get("dataYear");
+
+          if (dataYear) {
+            //Get the minimum and maximum years chosen
+            var yearMin = this.get("yearMin");
+            var yearMax = this.get("yearMax");
+
+            if (query.length) {
+              query += " AND ";
+            }
 
-                //---Identifier---
-                if (this.filterIsAvailable("id") && ((filter == "id") || getAll) && this.get('id').length) {
-                    var identifiers = this.get('id');
-
-                    if (Array.isArray(identifiers)) {
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      query += this.getGroupedQuery(this.fieldNameMap["id"], identifiers, {
-                        operator: "OR",
-                        subtext: true
-                      });
-
-                    } else if (identifiers) {
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      if( forPOST ){
-                        query += this.fieldNameMap["id"] + ':*' + this.escapeSpecialChar(identifiers) + "*";
-                      }
-                      else{
-                        query += this.fieldNameMap["id"] + ':*' + this.escapeSpecialChar(encodeURIComponent(identifiers)) + "*";
-                      }
-                    }
-                }
+            query +=
+              "beginDate:[" +
+              yearMin +
+              "-01-01T00:00:00Z TO *]" +
+              " AND endDate:[* TO " +
+              yearMax +
+              "-12-31T00:00:00Z]";
+          }
+        }
+
+        //----- public/private ------
+        if (
+          this.filterIsAvailable("isPrivate") &&
+          (filter == "isPrivate" || getAll)
+        ) {
+          var isPrivate = this.get("isPrivate");
+          if (isPrivate !== null && isPrivate !== "undefined") {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                //---resourceMap---
-                if (this.filterIsAvailable("resourceMap") && ((filter == "resourceMap") || getAll)) {
-                    var resourceMap = this.get('resourceMap');
-
-                    //If the resource map search setting is a list of resource map IDs
-                    if (Array.isArray(resourceMap)) {
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      query += this.getGroupedQuery(this.fieldNameMap["resourceMap"], resourceMap, {
-                          operator: "OR",
-                          forPOST: forPOST
-                      });
-
-                    } else if (resourceMap) {
-                      if( query.length ){
-                        query += " AND ";
-                      }
-                      //Otherwise, treat it as a binary setting
-                      query += this.fieldNameMap["resourceMap"] + ':*';
-                    }
-                }
+            // Currently, the Solr field 'isPublic' can be set to true or false, or not set.
+            // isPrivate is equivalent to "isPublic:false" or isPublic not set
+            if (isPrivate) {
+              query += "-isPublic:true";
+            }
+          }
+        }
+
+        //-----Data Source--------
+        if (
+          this.filterIsAvailable("dataSource") &&
+          (filter == "dataSource" || getAll)
+        ) {
+          var filterValue = null;
+          var filterValues = [];
+
+          if (this.get("dataSource").length > 0) {
+            var objectValues = _.filter(this.get("dataSource"), function (v) {
+              return typeof v == "object";
+            });
+            if (objectValues && objectValues.length) {
+              filterValues.push(_.pluck(objectValues, "value"));
+            }
+          }
 
-                //---documents---
-                if (this.filterIsAvailable("documents") && ((filter == "documents") || getAll)) {
-                    var documents = this.get('documents');
-
-                    //If the documents search setting is a list ofdocuments IDs
-                    if (Array.isArray(documents)) {
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      query += this.getGroupedQuery(this.fieldNameMap["documents"], documents, {
-                          operator: "OR",
-                          forPOST: forPOST
-                      });
-                    } else if (documents) {
-
-                      if( query.length ){
-                        query += " AND ";
-                      }
-                      //Otherwise, treat it as a binary setting
-                      query += this.fieldNameMap["documents"] + ':*';
-                    }
-                }
+          var stringValues = _.filter(this.get("dataSource"), function (v) {
+            return typeof v == "string";
+          });
+          if (stringValues && stringValues.length) {
+            filterValues.push(stringValues);
+          }
 
-                //---Username: search for this username in rightsHolder and submitter ---
-                if (this.filterIsAvailable("username") && ((filter == "username") || getAll) && this.get('username').length) {
-                    var username = this.get('username');
-                    if (username) {
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      query += this.getGroupedQuery(this.fieldNameMap["username"], username, {
-                          operator: "OR",
-                          forPOST: forPOST
-                      });
-                    }
-                }
+          filterValues = _.flatten(filterValues);
 
-                //--- ID Only - searches only the id and seriesId fields ---
-                if (this.filterIsAvailable("idOnly") && ((filter == "idOnly") || getAll) && this.get('idOnly').length) {
-                    var idOnly = this.get('idOnly');
-                    if (idOnly) {
+          if (filterValues.length) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                        if( query.length ){
-                          query += " AND ";
-                        }
+            query += this.getGroupedQuery(
+              this.fieldNameMap["dataSource"],
+              filterValues,
+              {
+                operator: "OR",
+                forPOST: forPOST,
+              },
+            );
+          }
+        }
+
+        //-----Excluded fields-----
+        if (
+          this.filterIsAvailable("exclude") &&
+          (filter == "exclude" || getAll)
+        ) {
+          var exclude = this.get("exclude");
+          _.each(exclude, function (excludeField, key, list) {
+            if (model.needsQuotes(excludeField.value)) {
+              if (forPOST) {
+                var filterValue = '"' + excludeField.value + '"';
+              } else {
+                var filterValue =
+                  "%22" + encodeURIComponent(excludeField.value) + "%22";
+              }
+            } else {
+              if (forPOST) {
+                var filterValue = excludeField.value;
+              } else {
+                var filterValue = encodeURIComponent(excludeField.value);
+              }
+            }
 
-                        query += this.getGroupedQuery(this.fieldNameMap["idOnly"], idOnly, {
-                            operator: "OR",
-                            forPOST: forPOST
-                        });
-                    }
-                }
+            filterValue = model.escapeSpecialChar(filterValue);
 
-                //---Taxon---
-                if (this.filterIsAvailable("taxon") && ((filter == "taxon") || getAll) && this.get('taxon').length) {
-                    var taxon = this.get('taxon');
+            if (query.length) {
+              query += " AND ";
+            }
 
-                    for (var i = 0; i < taxon.length; i++) {
-                        var value = (typeof taxon == "object") ? taxon[i].value : taxon[i].trim();
+            query += " -" + excludeField.field + ":" + filterValue;
+          });
+        }
+
+        //-----Additional criteria - both field and value are provided-----
+        if (
+          this.filterIsAvailable("additionalCriteria") &&
+          (filter == "additionalCriteria" || getAll)
+        ) {
+          var additionalCriteria = this.get("additionalCriteria");
+          for (var i = 0; i < additionalCriteria.length; i++) {
+            var value;
+
+            if (forPOST) {
+              value = additionalCriteria[i];
+            } else {
+              //if(this.needsQuotes(additionalCriteria[i])) value = "%22" + encodeURIComponent(additionalCriteria[i]) + "%22";
+              value = encodeURIComponent(additionalCriteria[i]);
+            }
 
-                        query += this.getMultiFieldQuery(this.fieldNameMap["taxon"], value, {
-                            subtext: true,
-                            forPOST: forPOST
-                        });
-                    }
-                }
+            if (query.length) {
+              query += " AND ";
+            }
 
-                //------Pub Year-----
-                if (this.filterIsAvailable("pubYear") && ((filter == "pubYear") || getAll)) {
-                    //Get the types of year to be searched first
-                    var pubYear = this.get('pubYear');
-                    if (pubYear) {
-                        //Get the minimum and maximum years chosen
-                        var yearMin = this.get('yearMin');
-                        var yearMax = this.get('yearMax');
-
-                        if( query.length ){
-                          query += " AND ";
-                        }
-
-                      var value = "[" + yearMin + "-01-01T00:00:00Z TO " + yearMax + "-12-31T00:00:00Z]";
-                      var opts = {
-                        forPOST: forPOST,
-                        escapeSquareBrackets: false
-                      }
-
-                        //Add to the query if we are searching publication year
-                      query += this.getMultiFieldQuery(
-                        this.fieldNameMap["pubYear"], value, opts
-                      );
-                    }
-                }
+            query += model.escapeSpecialChar(value);
+          }
+        }
+
+        //-----All (full text search) -----
+        if (this.filterIsAvailable("all") && (filter == "all" || getAll)) {
+          var all = this.get("all");
+          for (var i = 0; i < all.length; i++) {
+            var filterValue = all[i];
+
+            if (typeof filterValue == "object") {
+              filterValue = filterValue.value;
+            } else if (
+              (typeof filterValue == "string" && !filterValue.length) ||
+              typeof filterValue == "undefined" ||
+              filterValue === null
+            ) {
+              continue;
+            }
 
-                //-----Data year------
-                if (this.filterIsAvailable("dataYear") && ((filter == "dataYear") || getAll)) {
-                    var dataYear = this.get('dataYear');
+            if (this.needsQuotes(filterValue)) {
+              if (forPOST) {
+                filterValue = '"' + filterValue + '"';
+              } else {
+                filterValue = "%22" + encodeURIComponent(filterValue) + "%22";
+              }
+            } else {
+              if (forPOST) {
+                filterValue = filterValue;
+              } else {
+                filterValue = encodeURIComponent(filterValue);
+              }
+            }
 
-                    if (dataYear) {
-                        //Get the minimum and maximum years chosen
-                        var yearMin = this.get('yearMin');
-                        var yearMax = this.get('yearMax');
+            if (query.length) {
+              query += " AND ";
+            }
 
-                        if( query.length ){
-                          query += " AND ";
-                        }
+            query += model.escapeSpecialChar(filterValue);
+          }
+        }
+
+        //-----Other Filters/Basic Filters-----
+        _.each(otherFilters, function (filterName, key, list) {
+          if (
+            model.filterIsAvailable(filterName) &&
+            (filter == filterName || getAll)
+          ) {
+            var filterValue = null;
+            var filterValues = model.get(filterName);
+
+            for (var i = 0; i < filterValues.length; i++) {
+              //Trim the spaces off
+              var filterValue = filterValues[i];
+              if (typeof filterValue == "object") {
+                filterValue = filterValue.value;
+              }
+              filterValue = filterValue.trim();
 
-                        query += "beginDate:[" + yearMin + "-01-01T00:00:00Z TO *]" +
-                            " AND endDate:[* TO " + yearMax + "-12-31T00:00:00Z]";
-                    }
+              // Does this need to be wrapped in quotes?
+              if (model.needsQuotes(filterValue)) {
+                if (forPOST) {
+                  filterValue = '"' + filterValue + '"';
+                } else {
+                  filterValue = "%22" + encodeURIComponent(filterValue) + "%22";
                 }
-                
-                //----- public/private ------
-                if (this.filterIsAvailable("isPrivate") && ((filter == "isPrivate") || getAll)) {
-                    
-                    var isPrivate = this.get('isPrivate');
-                    if (isPrivate !== null && isPrivate !== 'undefined') {
-
-                        if( query.length ){
-                          query += " AND ";
-                        }
-
-                        // Currently, the Solr field 'isPublic' can be set to true or false, or not set.
-                        // isPrivate is equivalent to "isPublic:false" or isPublic not set
-                        if(isPrivate) {
-                            query += "-isPublic:true"
-                        }
-                    }
+              } else {
+                if (forPOST) {
+                  filterValue = filterValue;
+                } else {
+                  filterValue = encodeURIComponent(filterValue);
                 }
+              }
 
+              if (query.length) {
+                query += " AND ";
+              }
 
-                //-----Data Source--------
-                if (this.filterIsAvailable("dataSource") && ((filter == "dataSource") || getAll)) {
-                    var filterValue = null;
-                    var filterValues = [];
-
-                    if (this.get("dataSource").length > 0) {
-                        var objectValues = _.filter(this.get("dataSource"), function(v) {
-                            return (typeof v == "object")
-                        });
-                        if (objectValues && objectValues.length) {
-                            filterValues.push(_.pluck(objectValues, "value"));
-                        }
-                    }
+              query +=
+                model.fieldNameMap[filterName] +
+                ":" +
+                model.escapeSpecialChar(filterValue);
+            }
+          }
+        });
 
-                    var stringValues = _.filter(this.get("dataSource"), function(v) {
-                        return (typeof v == "string")
-                    });
-                    if (stringValues && stringValues.length) {
-                        filterValues.push(stringValues);
-                    }
+        //-----Geohashes-----
+        if (
+          this.filterIsAvailable("geohashLevel") &&
+          (filter == "geohash" || getAll) &&
+          this.get("useGeohash")
+        ) {
+          var geohashes = this.get("geohashes");
+
+          if (typeof geohashes != undefined && geohashes.length > 0) {
+            var groups = this.get("geohashGroups"),
+              numGroups =
+                typeof groups == "object" ? Object.keys(groups).length : 0;
+
+            if (numGroups > 0) {
+              //Add the AND operator in front of the geohash filter
+              if (query.length) {
+                query += " AND ";
+              }
 
-                    filterValues = _.flatten(filterValues);
+              //If there is more than one geohash group/level, wrap them in paranthesis
+              if (numGroups > 1) {
+                query += "(";
+              }
 
-                    if( filterValues.length ){
-                      if( query.length ){
-                        query += " AND ";
-                      }
+              _.each(Object.keys(groups), function (level, i, allLevels) {
+                var geohashList = groups[level];
 
-                      query += this.getGroupedQuery(this.fieldNameMap["dataSource"], filterValues, {
-                          operator: "OR",
-                          forPOST: forPOST
-                      });
-                    }
-                }
+                query += "geohash_" + level + ":";
 
-                //-----Excluded fields-----
-                if (this.filterIsAvailable("exclude") && ((filter == "exclude") || getAll)) {
-                    var exclude = this.get("exclude");
-                    _.each(exclude, function(excludeField, key, list) {
-
-                        if (model.needsQuotes(excludeField.value)) {
-                          if( forPOST ){
-                            var filterValue = '"' + excludeField.value + '"';
-                          }
-                          else{
-                            var filterValue = "%22" + encodeURIComponent(excludeField.value) + "%22";
-                          }
-                        } else {
-                          if( forPOST ){
-                            var filterValue = excludeField.value;
-                          }
-                          else{
-                            var filterValue = encodeURIComponent(excludeField.value);
-                          }
-                        }
-
-                        filterValue = model.escapeSpecialChar(filterValue);
-
-                        if( query.length ){
-                          query += " AND ";
-                        }
-
-                        query += " -" + excludeField.field + ":" + filterValue;
-                    });
+                if (geohashList.length > 1) {
+                  query += "(";
                 }
 
-                //-----Additional criteria - both field and value are provided-----
-                if (this.filterIsAvailable("additionalCriteria") && ((filter == "additionalCriteria") || getAll)) {
-                    var additionalCriteria = this.get('additionalCriteria');
-                    for (var i = 0; i < additionalCriteria.length; i++) {
-                        var value;
-
-                        if( forPOST ){
-                          value = additionalCriteria[i];
-                        }
-                        else{
-                          //if(this.needsQuotes(additionalCriteria[i])) value = "%22" + encodeURIComponent(additionalCriteria[i]) + "%22";
-                          value = encodeURIComponent(additionalCriteria[i]);
-                        }
-
-                        if( query.length ){
-                          query += " AND ";
-                        }
-
-                        query += model.escapeSpecialChar(value);
+                _.each(geohashList, function (g, ii, allGeohashes) {
+                  //Keep URI's from getting too long if we are using GET
+                  if (
+                    MetacatUI.appModel.get("disableQueryPOSTs") &&
+                    query.length > 1900
+                  ) {
+                    //Remove the last " OR "
+                    if (query.endsWith(" OR ")) {
+                      query = query.substring(0, query.length - 4);
                     }
-                }
 
-                //-----All (full text search) -----
-                if (this.filterIsAvailable("all") && ((filter == "all") || getAll)) {
-                    var all = this.get('all');
-                    for (var i = 0; i < all.length; i++) {
-                        var filterValue = all[i];
-
-                        if (typeof filterValue == "object") {
-                            filterValue = filterValue.value;
-                        }
-                        else if( (typeof filterValue == "string" && !filterValue.length) ||
-                                  typeof filterValue == "undefined" || filterValue === null){
-                          continue;
-                        }
-
-                        if (this.needsQuotes(filterValue)) {
-                          if( forPOST ){
-                            filterValue = '"' + filterValue + '"';
-                          }
-                          else{
-                            filterValue = "%22" + encodeURIComponent(filterValue) + "%22";
-                          }
-                        } else {
-                          if( forPOST ){
-                            filterValue = filterValue;
-                          }
-                          else{
-                            filterValue = encodeURIComponent(filterValue);
-                          }
-                        }
-
-                        if( query.length ){
-                          query += " AND ";
-                        }
-
-                        query += model.escapeSpecialChar(filterValue);
-                    }
-                }
+                    return;
+                  } else {
+                    //Add the geohash value to the query
+                    query += g;
 
-                //-----Other Filters/Basic Filters-----
-                _.each(otherFilters, function(filterName, key, list) {
-                    if (model.filterIsAvailable(filterName) && ((filter == filterName) || getAll)) {
-                        var filterValue = null;
-                        var filterValues = model.get(filterName);
-
-                        for (var i = 0; i < filterValues.length; i++) {
-
-                            //Trim the spaces off
-                            var filterValue = filterValues[i];
-                            if (typeof filterValue == "object") {
-                                filterValue = filterValue.value;
-                            }
-                            filterValue = filterValue.trim();
-
-                            // Does this need to be wrapped in quotes?
-                            if (model.needsQuotes(filterValue)) {
-                              if( forPOST ){
-                                filterValue = '"' + filterValue + '"';
-                              }
-                              else{
-                                filterValue = "%22" + encodeURIComponent(filterValue) + "%22";
-                              }
-                            } else  {
-                              if( forPOST ){
-                                filterValue = filterValue;
-                              }
-                              else{
-                                filterValue = encodeURIComponent(filterValue);
-                              }
-                            }
-
-                            if( query.length ){
-                              query += " AND ";
-                            }
-
-                            query += model.fieldNameMap[filterName] + ":" + model.escapeSpecialChar(filterValue);
-                        }
+                    //Add an " OR " operator inbetween geohashes
+                    if (ii < allGeohashes.length - 1) {
+                      query += " OR ";
                     }
+                  }
                 });
 
-                //-----Geohashes-----
-                if (this.filterIsAvailable("geohashLevel") && (((filter == "geohash") || getAll)) && this.get("useGeohash")) {
-                    var geohashes = this.get("geohashes");
-
-                    if ((typeof geohashes != undefined) && (geohashes.length > 0)) {
-                        var groups = this.get("geohashGroups"),
-                            numGroups = (typeof groups == "object")? Object.keys(groups).length : 0;
-
-                        if(numGroups > 0){
-                          //Add the AND operator in front of the geohash filter
-                          if( query.length ){
-                            query += " AND ";
-                          }
-
-                          //If there is more than one geohash group/level, wrap them in paranthesis
-                          if( numGroups > 1){
-                            query += "(";
-                          }
-
-                          _.each(Object.keys(groups), function(level, i, allLevels) {
-                              var geohashList = groups[level];
-
-                              query += "geohash_" + level + ":";
-
-                              if( geohashList.length > 1 ){
-                                query += "(";
-                              }
-
-                              _.each(geohashList, function(g, ii, allGeohashes) {
-                                  //Keep URI's from getting too long if we are using GET
-                                  if( MetacatUI.appModel.get("disableQueryPOSTs") && query.length > 1900){
-
-                                    //Remove the last " OR "
-                                    if( query.endsWith(" OR ") ){
-                                      query = query.substring(0, query.length-4)
-                                    }
-
-                                    return;
-                                  }
-                                  else{
-                                    //Add the geohash value to the query
-                                    query += g;
-
-                                    //Add an " OR " operator inbetween geohashes
-                                    if( ii < allGeohashes.length-1 ){
-                                      query += " OR ";
-                                    }
-                                  }
-                              });
-
-                              //Close the paranthesis
-                              if( geohashList.length > 1 ){
-                                query += ")";
-                              }
-
-                              //Add an " OR " operator inbetween geohash levels
-                              if( i < allLevels.length-1 ){
-                                query += " OR "
-                              }
-
-                          });
-
-                          //Close the paranthesis
-                          if(numGroups > 1){
-                            query += ")";
-                          }
-                        }
-                    }
+                //Close the paranthesis
+                if (geohashList.length > 1) {
+                  query += ")";
                 }
 
-                //---Spatial---
-                if (this.filterIsAvailable("spatial") && ((filter == "spatial") || getAll)) {
-                    var spatial = this.get('spatial');
-
-                    if (Array.isArray(spatial) && spatial.length) {
-
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      query += this.getGroupedQuery(this.fieldNameMap["spatial"], spatial, {
-                          operator: "AND",
-                          subtext: false,
-                          forPOST: forPOST
-                      });
-
-                    } else if( typeof spatial == "string" && spatial.length) {
-
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      if( forPOST ){
-                        query += this.fieldNameMap["spatial"] + ':' + model.escapeSpecialChar(spatial);
-                      }
-                      else{
-                        query += this.fieldNameMap["spatial"] + ':' + model.escapeSpecialChar(encodeURIComponent(spatial));
-                      }
-
-                    }
-                }
-
-                //---Creator---
-                if (this.filterIsAvailable("creator") && ((filter == "creator") || getAll)) {
-                    var creator = this.get('creator');
-
-                    if (Array.isArray(creator) && creator.length) {
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      query += this.getGroupedQuery(this.fieldNameMap["creator"], creator, {
-                          operator: "AND",
-                          subtext: false,
-                          forPOST: forPOST
-                      });
-                    } else if (typeof creator == "string" && creator.length) {
-                      if( query.length ){
-                        query += " AND ";
-                      }
-
-                      if( forPOST ){
-                        query += this.fieldNameMap["creator"] + ':' + model.escapeSpecialChar(creator);
-                      }
-                      else{
-                        query += this.fieldNameMap["creator"] + ':' + model.escapeSpecialChar(encodeURIComponent(creator));
-                      }
-                    }
+                //Add an " OR " operator inbetween geohash levels
+                if (i < allLevels.length - 1) {
+                  query += " OR ";
                 }
-                
-                // Add project filter
-                if (this.filterIsAvailable("projectText") && ((filter == "projectText") || getAll)) {
-
-                    var project = this.get('projectText');
-                    if (project && project.length > 0) {
+              });
 
-                        if( query.length ){
-                            query += " AND ";
-                        }
-                        query += 'projectText:"' + project[0].value + '"';
-                    }
-                }
-
-
-                return query;
-            },
-
-            getFacetQuery: function(fields) {
-
-                var facetQuery = "&facet=true" +
-                    "&facet.sort=count" +
-                    "&facet.mincount=1" +
-                    "&facet.limit=-1";
-
-                //Get the list of fields
-                if (!fields) {
-                    var fields = "keywords,origin,family,species,genus,kingdom,phylum,order,class,site";
-                    if (this.filterIsAvailable("annotation")) {
-                        fields += "," + this.facetNameMap["annotation"];
-                    }
-                    if (this.filterIsAvailable("attribute")) {
-                        fields += ",attributeName,attributeLabel";
-                    }
-                }
-
-                var model = this;
-                //Add the fields to the query string
-                _.each(fields.split(","), function(f) {
-                    var fieldNames = model.facetNameMap[f] || f;
+              //Close the paranthesis
+              if (numGroups > 1) {
+                query += ")";
+              }
+            }
+          }
+        }
+
+        //---Spatial---
+        if (
+          this.filterIsAvailable("spatial") &&
+          (filter == "spatial" || getAll)
+        ) {
+          var spatial = this.get("spatial");
+
+          if (Array.isArray(spatial) && spatial.length) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                    if (typeof fieldNames == "string") {
-                        fieldNames = [fieldNames];
-                    }
+            query += this.getGroupedQuery(
+              this.fieldNameMap["spatial"],
+              spatial,
+              {
+                operator: "AND",
+                subtext: false,
+                forPOST: forPOST,
+              },
+            );
+          } else if (typeof spatial == "string" && spatial.length) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                    _.each(fieldNames, function(fName) {
-                        facetQuery += "&facet.field=" + fName;
-                    });
-                });
+            if (forPOST) {
+              query +=
+                this.fieldNameMap["spatial"] +
+                ":" +
+                model.escapeSpecialChar(spatial);
+            } else {
+              query +=
+                this.fieldNameMap["spatial"] +
+                ":" +
+                model.escapeSpecialChar(encodeURIComponent(spatial));
+            }
+          }
+        }
+
+        //---Creator---
+        if (
+          this.filterIsAvailable("creator") &&
+          (filter == "creator" || getAll)
+        ) {
+          var creator = this.get("creator");
+
+          if (Array.isArray(creator) && creator.length) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-                return facetQuery;
-            },
+            query += this.getGroupedQuery(
+              this.fieldNameMap["creator"],
+              creator,
+              {
+                operator: "AND",
+                subtext: false,
+                forPOST: forPOST,
+              },
+            );
+          } else if (typeof creator == "string" && creator.length) {
+            if (query.length) {
+              query += " AND ";
+            }
 
-            //Check for spaces in a string - we'll use this to url encode the query
-            needsQuotes: function(entry) {
+            if (forPOST) {
+              query +=
+                this.fieldNameMap["creator"] +
+                ":" +
+                model.escapeSpecialChar(creator);
+            } else {
+              query +=
+                this.fieldNameMap["creator"] +
+                ":" +
+                model.escapeSpecialChar(encodeURIComponent(creator));
+            }
+          }
+        }
+
+        // Add project filter
+        if (
+          this.filterIsAvailable("projectText") &&
+          (filter == "projectText" || getAll)
+        ) {
+          var project = this.get("projectText");
+          if (project && project.length > 0) {
+            if (query.length) {
+              query += " AND ";
+            }
+            query += 'projectText:"' + project[0].value + '"';
+          }
+        }
+
+        return query;
+      },
+
+      getFacetQuery: function (fields) {
+        var facetQuery =
+          "&facet=true" +
+          "&facet.sort=count" +
+          "&facet.mincount=1" +
+          "&facet.limit=-1";
+
+        //Get the list of fields
+        if (!fields) {
+          var fields =
+            "keywords,origin,family,species,genus,kingdom,phylum,order,class,site";
+          if (this.filterIsAvailable("annotation")) {
+            fields += "," + this.facetNameMap["annotation"];
+          }
+          if (this.filterIsAvailable("attribute")) {
+            fields += ",attributeName,attributeLabel";
+          }
+        }
+
+        var model = this;
+        //Add the fields to the query string
+        _.each(fields.split(","), function (f) {
+          var fieldNames = model.facetNameMap[f] || f;
+
+          if (typeof fieldNames == "string") {
+            fieldNames = [fieldNames];
+          }
+
+          _.each(fieldNames, function (fName) {
+            facetQuery += "&facet.field=" + fName;
+          });
+        });
 
-                //Check for spaces
-                var value = "";
+        return facetQuery;
+      },
+
+      //Check for spaces in a string - we'll use this to url encode the query
+      needsQuotes: function (entry) {
+        //Check for spaces
+        var value = "";
+
+        if (typeof entry == "object") {
+          value = entry.value;
+        } else if (typeof entry == "string") {
+          value = entry;
+        } else {
+          return false;
+        }
+
+        //Is this a date range search? If so, we don't use quote marks
+        var ISODateRegEx =
+          /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)/;
+        if (ISODateRegEx.exec(value)) {
+          return false;
+        }
+
+        //Check for a space character
+        if (value.indexOf(" ") > -1) {
+          return true;
+        }
+
+        //Check if this is an account subject string
+        var LDAPSubjectRegEx =
+            /(uid=|UID=|cn=|CN=).+([a-zA-Z]=).+([a-zA-Z]=).*/,
+          ORCIDRegEx =
+            /^http\:\/\/orcid\.org\/[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9X]{4}/;
+
+        if (LDAPSubjectRegEx.exec(value) || ORCIDRegEx.exec(value)) {
+          return true;
+        }
+
+        return false;
+      },
+
+      escapeSpecialChar: function (term, escapeSquareBrackets = true) {
+        term = term.replace(/%7B/g, "%5C%7B");
+        term = term.replace(/%7D/g, "%5C%7D");
+        term = term.replace(/%3A/g, "%5C%3A");
+        term = term.replace(/:/g, "%5C:");
+        term = term.replace(/\(/g, "%5C(");
+        term = term.replace(/\)/g, "%5C)");
+        term = term.replace(/\?/g, "%5C?");
+        term = term.replace(/%3F/g, "%5C%3F");
+        term = term.replace(/%2B/g, "%5C%2B");
+        //Remove ampersands (&) for now since they are reserved Solr characters and the Metacat Solr can't seem to handle them even when they are escaped properly for some reason
+        term = term.replace(/%26/g, "");
+        term = term.replace(/%7C%7C/g, "%5C%7C%5C%7C");
+        term = term.replace(/%21/g, "%5C%21");
+        term = term.replace(/%28/g, "%5C%28");
+        term = term.replace(/%29/g, "%5C%29");
+        term = term.replace(/%5E/g, "%5C%5E");
+        term = term.replace(/%22/g, "%5C%22");
+        term = term.replace(/~/g, "%5C~");
+        term = term.replace(/-/g, "%5C-");
+        term = term.replace(/%2F/g, "%5C%2F");
+
+        if (escapeSquareBrackets) {
+          term = term.replace(/%5B/g, "%5C%5B");
+          term = term.replace(/%5D/g, "%5C%5D");
+        }
+
+        return term;
+      },
+      /*
+       * Makes a Solr syntax grouped query using the field name, the field values to search for, and the operator.
+       * Example:  title:(resistance OR salmon OR "pink salmon")
+       */
+      getGroupedQuery: function (fieldName, values, options) {
+        if (!values) return "";
+        values = _.compact(values);
+        if (!values.length) return "";
+
+        var query = "",
+          numValues = values.length,
+          model = this;
+
+        if (Array.isArray(fieldName) && fieldName.length > 1) {
+          return this.getMultiFieldQuery(fieldName, values, options);
+        }
+
+        if (options && typeof options == "object") {
+          var operator = options.operator,
+            subtext = options.subtext,
+            forPOST = options.forPOST;
+        }
+
+        if (
+          typeof operator === "undefined" ||
+          !operator ||
+          (operator != "OR" && operator != "AND")
+        ) {
+          var operator = "OR";
+        }
+
+        if (numValues == 1) {
+          var value = values[0],
+            queryAddition;
+
+          if (
+            !Array.isArray(value) &&
+            typeof value === "object" &&
+            value.value
+          ) {
+            value = value.value.trim();
+          }
+
+          if (this.needsQuotes(values[0])) {
+            if (forPOST) {
+              queryAddition = '"' + this.escapeSpecialChar(value) + '"';
+            } else {
+              queryAddition =
+                "%22" +
+                this.escapeSpecialChar(encodeURIComponent(value)) +
+                "%22";
+            }
+          } else if (subtext) {
+            if (forPOST) {
+              queryAddition = "*" + this.escapeSpecialChar(value) + "*";
+            } else {
+              queryAddition =
+                "*" + this.escapeSpecialChar(encodeURIComponent(value)) + "*";
+            }
+          } else {
+            if (forPOST) {
+              queryAddition = this.escapeSpecialChar(value);
+            } else {
+              queryAddition = this.escapeSpecialChar(encodeURIComponent(value));
+            }
+          }
+          query = fieldName + ":" + queryAddition;
+        } else {
+          _.each(
+            values,
+            function (value, i) {
+              //Check for filter objects
+              if (
+                !Array.isArray(value) &&
+                typeof value === "object" &&
+                value.value
+              ) {
+                value = value.value.trim();
+              }
 
-                if (typeof entry == "object") {
-                    value = entry.value;
-                } else if (typeof entry == "string") {
-                    value = entry;
+              if (model.needsQuotes(value)) {
+                if (forPOST) {
+                  value = '"' + value + '"';
                 } else {
-                    return false;
-                }
-
-                //Is this a date range search? If so, we don't use quote marks
-                var ISODateRegEx = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)/;
-                if (ISODateRegEx.exec(value)) {
-                    return false;
+                  value = "%22" + encodeURIComponent(value) + "%22";
                 }
-
-                //Check for a space character
-                if (value.indexOf(" ") > -1) {
-                    return true;
+              } else if (subtext) {
+                if (forPOST) {
+                  value = "*" + this.escapeSpecialChar(value) + "*";
+                } else {
+                  value =
+                    "*" +
+                    this.escapeSpecialChar(encodeURIComponent(value)) +
+                    "*";
                 }
-
-                //Check if this is an account subject string
-                var LDAPSubjectRegEx = /(uid=|UID=|cn=|CN=).+([a-zA-Z]=).+([a-zA-Z]=).*/,
-                    ORCIDRegEx = /^http\:\/\/orcid\.org\/[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9X]{4}/;
-
-                if (LDAPSubjectRegEx.exec(value) || ORCIDRegEx.exec(value)) {
-                    return true;
+              } else {
+                if (forPOST) {
+                  value = this.escapeSpecialChar(value);
+                } else {
+                  value = this.escapeSpecialChar(encodeURIComponent(value));
                 }
+              }
 
-              return false;
+              if (i == 0 && numValues > 1) {
+                query += fieldName + ":(" + value;
+              } else if (i > 0 && i < numValues - 1 && query.length) {
+                query += " " + operator + " " + value;
+              } else if (i > 0 && i < numValues - 1) {
+                query += value;
+              } else if (i == numValues - 1) {
+                query += " " + operator + " " + value + ")";
+              }
             },
-
-            escapeSpecialChar: function(term, escapeSquareBrackets=true) {
-                term = term.replace(/%7B/g, "%5C%7B");
-                term = term.replace(/%7D/g, "%5C%7D");
-                term = term.replace(/%3A/g, "%5C%3A");
-                term = term.replace(/:/g, "%5C:");
-                term = term.replace(/\(/g, "%5C(");
-                term = term.replace(/\)/g, "%5C)");
-                term = term.replace(/\?/g, "%5C?");
-                term = term.replace(/%3F/g, "%5C%3F");
-                term = term.replace(/%2B/g, "%5C%2B");
-                //Remove ampersands (&) for now since they are reserved Solr characters and the Metacat Solr can't seem to handle them even when they are escaped properly for some reason
-                term = term.replace(/%26/g, "");
-                term = term.replace(/%7C%7C/g, "%5C%7C%5C%7C");
-                term = term.replace(/%21/g, "%5C%21");
-                term = term.replace(/%28/g, "%5C%28");
-                term = term.replace(/%29/g, "%5C%29");
-                term = term.replace(/%5E/g, "%5C%5E");
-                term = term.replace(/%22/g, "%5C%22");
-                term = term.replace(/~/g, "%5C~");
-                term = term.replace(/-/g, "%5C-");
-                term = term.replace(/%2F/g, "%5C%2F");
-              
-              if (escapeSquareBrackets) {
-                term = term.replace(/%5B/g, "%5C%5B");
-                term = term.replace(/%5D/g, "%5C%5D");
+            this,
+          );
+        }
+
+        return query;
+      },
+
+      /*
+       * Makes a Solr syntax query using multiple field names, one field value to search for, and some options.
+       * Example: (family:*Pin* OR class:*Pin* OR order:*Pin* OR phlyum:*Pin*)
+       * Options:
+       * 		- operator (OR or AND)
+       * 		- subtext (binary) - will surround search value with wildcards to search for partial matches
+       * 		- Example:
+       * 			var options = { operator: "OR", subtext: true }
+       */
+      getMultiFieldQuery: function (fieldNames, value, options) {
+        var query = "",
+          numFields = fieldNames.length,
+          model = this;
+
+        //Catch errors
+        if (numFields < 1 || !value) return "";
+
+        //If only one field was given, then use the grouped query function
+        if (numFields == 1) {
+          return this.getGroupedQuery(fieldNames, value, options);
+        }
+
+        //Get the options
+        if (options && typeof options == "object") {
+          var operator = options.operator,
+            subtext = options.subtext,
+            forPOST = options.forPOST;
+        }
+        var esb =
+          typeof options.escapeSquareBrackets == "boolean"
+            ? options.escapeSquareBrackets
+            : true;
+
+        //Default to the OR operator
+        if (
+          typeof operator === "undefined" ||
+          !operator ||
+          (operator != "OR" && operator != "AND")
+        ) {
+          var operator = "OR";
+        }
+        if (typeof subtext === "undefined") {
+          var subtext = false;
+        }
+
+        //Create the value string
+        //Trim the spaces off
+        if (!Array.isArray(value) && typeof value === "object" && value.value) {
+          value = [value.value.trim()];
+        } else if (typeof value == "string") {
+          value = [value.trim()];
+        }
+
+        var valueString = "";
+        if (Array.isArray(value)) {
+          var model = this;
+
+          _.each(
+            value,
+            function (v, i) {
+              if (typeof v == "object" && v.value) {
+                v = v.value;
               }
 
-                return term;
-              },
-            /*
-             * Makes a Solr syntax grouped query using the field name, the field values to search for, and the operator.
-             * Example:  title:(resistance OR salmon OR "pink salmon")
-             */
-            getGroupedQuery: function(fieldName, values, options) {
-                if (!values) return "";
-                values = _.compact(values);
-                if (!values.length) return "";
-
-                var query = "",
-                    numValues = values.length,
-                    model = this;
-
-                if (Array.isArray(fieldName) && (fieldName.length > 1)) {
-                    return this.getMultiFieldQuery(fieldName, values, options);
-                }
+              if (value.length > 1 && i == 0) {
+                valueString += "(";
+              }
 
-                if (options && (typeof options == "object")) {
-                    var operator = options.operator,
-                        subtext  = options.subtext,
-                        forPOST  = options.forPOST;
+              if (model.needsQuotes(v) || _.contains(fieldNames, "id")) {
+                if (forPOST) {
+                  valueString +=
+                    '"' + this.escapeSpecialChar(v.trim(), esb) + '"';
+                } else {
+                  valueString +=
+                    '"' +
+                    this.escapeSpecialChar(encodeURIComponent(v.trim()), esb) +
+                    '"';
                 }
-
-                if ((typeof operator === "undefined") || !operator || ((operator != "OR") && (operator != "AND"))) {
-                    var operator = "OR";
+              } else if (subtext) {
+                if (forPOST) {
+                  valueString +=
+                    "*" + this.escapeSpecialChar(v.trim(), esb) + "*";
+                } else {
+                  valueString +=
+                    "*" +
+                    this.escapeSpecialChar(encodeURIComponent(v.trim()), esb) +
+                    "*";
                 }
-
-                if (numValues == 1) {
-                    var value = values[0],
-                        queryAddition;
-
-                    if (!Array.isArray(value) && (typeof value === "object") && value.value) {
-                        value = value.value.trim();
-                    }
-
-                    if (this.needsQuotes(values[0])) {
-                      if( forPOST ){
-                        queryAddition = '"' + this.escapeSpecialChar(value) + '"';
-                      }
-                      else{
-                        queryAddition = '%22' + this.escapeSpecialChar(encodeURIComponent(value)) + '%22';
-                      }
-                    } else if (subtext) {
-                      if( forPOST ){
-                        queryAddition = "*" + this.escapeSpecialChar(value) + "*";
-                      }
-                      else{
-                        queryAddition = "*" + this.escapeSpecialChar(encodeURIComponent(value)) + "*";
-                      }
-                    } else {
-                      if( forPOST ){
-                        queryAddition = this.escapeSpecialChar(value);
-                      }
-                      else{
-                        queryAddition = this.escapeSpecialChar(encodeURIComponent(value));
-                      }
-                    }
-                    query = fieldName + ":" + queryAddition;
+              } else {
+                if (forPOST) {
+                  valueString += this.escapeSpecialChar(v.trim(), esb);
                 } else {
-                    _.each(values, function(value, i) {
-                        //Check for filter objects
-                        if (!Array.isArray(value) && (typeof value === "object") && value.value) {
-                            value = value.value.trim();
-                        }
-
-                        if (model.needsQuotes(value)) {
-                          if( forPOST ){
-                            value = '"' + value + '"';
-                          }
-                          else{
-                            value = '%22' + encodeURIComponent(value) + '%22';
-                          }
-                        } else if (subtext) {
-                          if( forPOST ){
-                            value = "*" + this.escapeSpecialChar(value) + "*";
-                          }
-                          else{
-                            value = "*" + this.escapeSpecialChar(encodeURIComponent(value)) + "*";
-                          }
-                        } else {
-                          if( forPOST ){
-                            value = this.escapeSpecialChar(value);
-                          }
-                          else{
-                            value = this.escapeSpecialChar(encodeURIComponent(value));
-                          }
-                        }
-
-                        if ((i == 0) && (numValues > 1)) {
-                            query += fieldName + ":(" + value;
-                        } else if ((i > 0) && (i < numValues - 1) && query.length) {
-                            query += " " + operator + " " + value;
-                        } else if( (i > 0) && (i < numValues - 1) ){
-                            query += value;
-                        } else if (i == numValues - 1) {
-                            query += " " + operator + " " + value + ")";
-                        }
-                    }, this);
+                  valueString += this.escapeSpecialChar(
+                    encodeURIComponent(v.trim()),
+                    esb,
+                  );
                 }
+              }
 
-                return query;
+              if (i < value.length - 1) {
+                valueString += " OR ";
+              } else if (i == value.length - 1 && value.length > 1) {
+                valueString += ")";
+              }
             },
+            this,
+          );
+        } else valueString = value;
+
+        query = "(";
+
+        //Create the query string
+        var last = numFields - 1;
+        _.each(fieldNames, function (field, i) {
+          query += field + ":" + valueString;
+          if (i < last) {
+            query += " " + operator + " ";
+          }
+        });
 
-            /*
-             * Makes a Solr syntax query using multiple field names, one field value to search for, and some options.
-             * Example: (family:*Pin* OR class:*Pin* OR order:*Pin* OR phlyum:*Pin*)
-             * Options:
-             * 		- operator (OR or AND)
-             * 		- subtext (binary) - will surround search value with wildcards to search for partial matches
-             * 		- Example:
-             * 			var options = { operator: "OR", subtext: true }
-             */
-            getMultiFieldQuery: function(fieldNames, value, options) {
-                var query = "",
-                    numFields = fieldNames.length,
-                    model = this;
-
-                //Catch errors
-                if ((numFields < 1) || !value) return "";
-
-                //If only one field was given, then use the grouped query function
-                if (numFields == 1) {
-                    return this.getGroupedQuery(fieldNames, value, options);
-                }
-
-                //Get the options
-                if (options && (typeof options == "object")) {
-                    var operator = options.operator,
-                        subtext  = options.subtext,
-                        forPOST  = options.forPOST;
-                }
-                var esb = (typeof options.escapeSquareBrackets == "boolean") ?
-                  options.escapeSquareBrackets : true;
-
-                //Default to the OR operator
-                if ((typeof operator === "undefined") || !operator ||
-                    ((operator != "OR") && (operator != "AND"))) {
-                    var operator = "OR";
-                }
-                if ((typeof subtext === "undefined")) {
-                    var subtext = false;
-                }
-
-                //Create the value string
-                //Trim the spaces off
-                if (!Array.isArray(value) && (typeof value === "object") && value.value) {
-                    value = [value.value.trim()];
-                } else if (typeof value == "string") {
-                    value = [value.trim()];
-                }
-
-                var valueString = "";
-                if (Array.isArray(value)) {
-                    var model = this;
-
-                    _.each(value, function(v, i) {
-                        if ((typeof v == "object") && v.value) {
-                            v = v.value;
-                        }
-
-                        if ((value.length > 1) && (i == 0)) {
-                            valueString += "("
-                        }
-
-                      if (model.needsQuotes(v) || _.contains(fieldNames, "id")) {
-                          if( forPOST ){
-                            valueString += '"' + this.escapeSpecialChar(v.trim(), esb) + '"';
-                          }
-                          else{
-                            valueString += '"' + this.escapeSpecialChar(encodeURIComponent(v.trim()), esb) + '"';
-                          }
-                      } else if (subtext) {
-                          if( forPOST ){
-                            valueString += "*" + this.escapeSpecialChar(v.trim(), esb) + "*";
-                          }
-                          else{
-                            valueString += "*" + this.escapeSpecialChar(encodeURIComponent(v.trim()), esb) + "*";
-                          }
-                        } else {
-                        if (forPOST) {
-                            valueString += this.escapeSpecialChar(v.trim(), esb);
-                          }
-                        else {
-                          valueString += this.escapeSpecialChar(encodeURIComponent(v.trim()), esb);
-                          }
-                        }
-
-                        if (i < value.length - 1) {
-                            valueString += " OR ";
-                        } else if ((i == value.length - 1) && (value.length > 1)) {
-                            valueString += ")";
-                        }
-
-                    }, this);
-                } else valueString = value;
-
-                query = "(";
-
-                //Create the query string
-                var last = numFields - 1;
-                _.each(fieldNames, function(field, i) {
-                    query += field + ":" + valueString;
-                    if (i < last) {
-                        query += " " + operator + " ";
-                    }
-                });
-
-                query += ")";
+        query += ")";
 
-                return query;
-            },
+        return query;
+      },
 
-            /**** Provenance-related functions ****/
-            // Returns which fields are provenance-related in this model
-            // Useful for querying the index and such
-            getProvFields: function() {
-              var provFields = this.get("provFields");
+      /**** Provenance-related functions ****/
+      // Returns which fields are provenance-related in this model
+      // Useful for querying the index and such
+      getProvFields: function () {
+        var provFields = this.get("provFields");
 
-              if( !provFields.length ){
-                var defaultFields = Object.keys(SolrResult.prototype.defaults);
-                provFields = _.filter(defaultFields, function(fieldName) {
-                    if (fieldName.indexOf("prov_") == 0) return true;
-                });
+        if (!provFields.length) {
+          var defaultFields = Object.keys(SolrResult.prototype.defaults);
+          provFields = _.filter(defaultFields, function (fieldName) {
+            if (fieldName.indexOf("prov_") == 0) return true;
+          });
 
-                this.set("provFields", provFields);
-              }
+          this.set("provFields", provFields);
+        }
 
-              return provFields;
-            },
+        return provFields;
+      },
 
-            getProvFlList: function() {
-                var provFields = this.getProvFields(),
-                    provFl = "";
-                _.each(provFields, function(provField, i) {
-                    provFl += provField;
-                    if (i < provFields.length - 1) provFl += ",";
-                });
-
-                return provFl;
-            },
+      getProvFlList: function () {
+        var provFields = this.getProvFields(),
+          provFl = "";
+        _.each(provFields, function (provField, i) {
+          provFl += provField;
+          if (i < provFields.length - 1) provFl += ",";
+        });
 
-            clear: function() {
-                return this.set(_.clone(this.defaults()));
-            }
+        return provFl;
+      },
 
-        });
-        return Search;
-    });
+      clear: function () {
+        return this.set(_.clone(this.defaults()));
+      },
+    },
+  );
+  return Search;
+});
 
diff --git a/docs/docs/src_js_models_SolrResult.js.html b/docs/docs/src_js_models_SolrResult.js.html index faea50378..b9c96faaf 100644 --- a/docs/docs/src_js_models_SolrResult.js.html +++ b/docs/docs/src_js_models_SolrResult.js.html @@ -44,800 +44,902 @@

Source: src/js/models/SolrResult.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone'],
-	function($, _, Backbone) {
-
-	/**
-  * @class SolrResult
-  * @classdesc A single result from the Solr search service
-  * @classcategory Models
-  * @extends Backbone.Model
-  */
-	var SolrResult = Backbone.Model.extend(
-    /** @lends SolrResult.prototype */{
-		// This model contains all of the attributes found in the SOLR 'docs' field inside of the SOLR response element
-		defaults: {
-			abstract: null,
-			entityName: null,
-			indexed: true,
-			archived: false,
-			origin: '',
-            keywords: '',
-			title: '',
-			pubDate: '',
-			eastBoundCoord: '',
-			westBoundCoord: '',
-			northBoundCoord: '',
-			southBoundCoord: '',
-			attributeName: '',
-			beginDate: '',
-			endDate: '',
-			pubDate: '',
-			id: '',
-			seriesId: null,
-			resourceMap: null,
-			downloads: null,
-			citations: 0,
-			selected: false,
-			formatId: null,
-			formatType: null,
-			fileName: null,
-			datasource: null,
-			rightsHolder: null,
-			size: 0,
-			type: "",
-			url: null,
-			obsoletedBy: null,
-			geohash_9: null,
-			read_count_i: 0,
-			reads: 0,
-			isDocumentedBy: null,
-			isPublic: null,
-			isService: false,
-			serviceDescription: null,
-			serviceTitle: null,
-			serviceEndpoint: null,
-			serviceOutput: null,
-			notFound: false,
-			newestVersion: null,
-      //@type {string} - The system metadata XML as a string
-      systemMetadata: null,
-			provSources: [],
-			provDerivations: [],
-			//Provenance index fields
-			prov_generated: null,
-			prov_generatedByDataONEDN: null,
-			prov_generatedByExecution: null,
-			prov_generatedByFoafName: null,
-			prov_generatedByOrcid: null,
-			prov_generatedByProgram: null,
-			prov_generatedByUser: null,
- 			prov_hasDerivations: null,
-			prov_hasSources: null,
-			prov_instanceOfClass: null,
-			prov_used: null,
-			prov_usedByDataONEDN: null,
-			prov_usedByExecution: null,
-			prov_usedByFoafName: null,
-			prov_usedByOrcid: null,
-			prov_usedByProgram: null,
-			prov_usedByUser: null,
-			prov_wasDerivedFrom: null,
-			prov_wasExecutedByExecution: null,
-			prov_wasExecutedByUser: null,
-			prov_wasGeneratedBy: null,
-			prov_wasInformedBy: null
-		},
-
-		initialize: function(){
-			this.setURL();
-			this.on("change:id", this.setURL);
-
-			this.set("type", this.getType());
-			this.on("change:read_count_i", function(){ this.set("reads", this.get("read_count_i"))});
-		},
-
-		type: "SolrResult",
-
-		// Toggle the `selected` state of the result
-		toggle: function () {
-			this.selected = !this.get('selected');
-		},
-
-		/**
-    * Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data
-    * @return {string}
-    */
-		getType: function(){
-			//The list of formatIds that are images
-			var imageIds = ["image/gif",
-			                "image/jp2",
-			                "image/jpeg",
-			                "image/png",
-			                "image/svg xml",
-			                "image/svg+xml",
-			                "image/bmp"];
-			//The list of formatIds that are images
-			var pdfIds = ["application/pdf"];
-			var annotationIds = ["http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html"];
-			var collectionIds = ["https://purl.dataone.org/collections-1.0.0",
-				"https://purl.dataone.org/collections-1.1.0"];
-			var portalIds = ["https://purl.dataone.org/portals-1.0.0",
-				"https://purl.dataone.org/portals-1.1.0"];
-
-			//Determine the type via provONE
-			var instanceOfClass = this.get("prov_instanceOfClass");
-			if(typeof instanceOfClass !== "undefined"){
-				var programClass = _.filter(instanceOfClass, function(className){
-					return (className.indexOf("#Program") > -1);
-				});
-				if((typeof programClass !== "undefined") && programClass.length)
-					return "program";
-			}
-			else{
-				if(this.get("prov_generated") || this.get("prov_used"))
-					return "program";
-			}
-
-			//Determine the type via file format
-      if(_.contains(collectionIds, this.get("formatId"))) return "collection";
-      if(_.contains(portalIds, this.get("formatId"))) return "portal";
-			if(this.get("formatType") == "METADATA") return "metadata";
-			if(_.contains(imageIds, this.get("formatId"))) return "image";
-			if(_.contains(pdfIds, this.get("formatId")))   return "PDF";
-			if(_.contains(annotationIds, this.get("formatId")))   return "annotation";
-
-			else return "data";
-		},
-
-		//Returns a plain-english version of the specific format ID (for selected ids)
-		getFormat: function(){
-			var formatMap = {
-				"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : "Microsoft Excel OpenXML",
-				"application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "Microsoft Word OpenXML",
-				"application/vnd.ms-excel.sheet.binary.macroEnabled.12" : "Microsoft Office Excel 2007 binary workbooks",
-				"application/vnd.openxmlformats-officedocument.presentationml.presentation" : "Microsoft Office OpenXML Presentation",
-				"application/vnd.ms-excel" : "Microsoft Excel",
-				"application/msword" : "Microsoft Word",
-				"application/vnd.ms-powerpoint" : "Microsoft Powerpoint",
-				"text/html" : "HTML",
-				"text/plain": "plain text (.txt)",
-				"video/avi" : "Microsoft AVI file",
-				"video/x-ms-wmv" : "Windows Media Video (.wmv)",
-				"audio/x-ms-wma" : "Windows Media Audio (.wma)",
-				"application/vnd.google-earth.kml xml" : "Google Earth Keyhole Markup Language (KML)",
-				"http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html" : "annotation",
-				"application/mathematica" : "Mathematica Notebook",
-				"application/postscript" : "Postscript",
-				"application/rtf" : "Rich Text Format (RTF)",
-				"application/xml" : "XML Application",
-				"text/xml" : "XML",
-				"application/x-fasta" : "FASTA sequence file",
-				"nexus/1997" : "NEXUS File Format for Systematic Information",
-				"anvl/erc-v02" :  "Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13",
-				"http://purl.org/dryad/terms/" : "Dryad Metadata Application Profile Version 3.0",
-				"http://datadryad.org/profile/v3.1" : "Dryad Metadata Application Profile Version 3.1",
-				"application/pdf" : "PDF",
-				"application/zip" : "ZIP file",
-				"http://www.w3.org/TR/rdf-syntax-grammar" : "RDF/XML",
-				"http://www.w3.org/TR/rdfa-syntax" : "RDFa",
-				"application/rdf xml" : "RDF",
-				"text/turtle" : "TURTLE",
-				"text/n3" : "N3",
-				"application/x-gzip" : "GZIP Format",
-				"application/x-python" : "Python script",
-				"http://www.w3.org/2005/Atom" : "ATOM-1.0",
-				"application/octet-stream" : "octet stream (application file)",
-				"http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd" : "Darwin Core, v2.0",
-				"http://rs.tdwg.org/dwc/xsd/simpledarwincore/" : "Simple Darwin Core",
-				"eml://ecoinformatics.org/eml-2.1.0" : "EML v2.1.0",
-				"eml://ecoinformatics.org/eml-2.1.1" : "EML v2.1.1",
-				"eml://ecoinformatics.org/eml-2.0.1" : "EML v2.0.1",
-				"eml://ecoinformatics.org/eml-2.0.0" : "EML v2.0.0",
-				"https://eml.ecoinformatics.org/eml-2.2.0" : "EML v2.2.0",
-
-			}
-
-			return formatMap[this.get("formatId")] || this.get("formatId");
-		},
-
-		setURL: function(){
-			if(MetacatUI.appModel.get("objectServiceUrl"))
-				this.set("url", MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(this.get("id")));
-			else if(MetacatUI.appModel.get("resolveServiceUrl"))
-				this.set("url", MetacatUI.appModel.get("resolveServiceUrl") + encodeURIComponent(this.get("id")));
-		},
-
-		/**
-		 * Checks if the pid or sid or given string is a DOI
-		 *
-		 * @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model
-		 * @returns {boolean} True if it is a DOI
-		 */
-		isDOI: function (customString) {
-			return MetacatUI.appModel.isDOI(customString) ||
-			MetacatUI.appModel.isDOI(this.get("id")) ||
-			MetacatUI.appModel.isDOI(this.get("seriesId"));
-		},
-
-		/*
-		 * Checks if the currently-logged-in user is authorized to change
-		 * permissions (or other action if set as parameter) on this doc
-		 * @param {string} [action=changePermission] - The action (read, write, or changePermission) to check
-		 * if the current user has authorization to perform. By default checks for the highest level of permission.
-		 */
-		checkAuthority: function(action = "changePermission"){
-			var authServiceUrl = MetacatUI.appModel.get('authServiceUrl');
-			if(!authServiceUrl) return false;
-
-			var model = this;
-
-			var requestSettings = {
-				url: authServiceUrl + encodeURIComponent(this.get("id")) + "?action=" + action,
-				type: "GET",
-				success: function(data, textStatus, xhr) {
-					model.set("isAuthorized_" + action, true);
-					model.set("isAuthorized", true);
-					model.trigger("change:isAuthorized");
-				},
-				error: function(xhr, textStatus, errorThrown) {
-					model.set("isAuthorized_" + action, false);
-					model.set("isAuthorized", false);
-				}
-			}
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		/*
-		 * This method will download this object while sending the user's auth token in the request.
-		 */
-		downloadWithCredentials: function(){
-			//if(this.get("isPublic")) return;
-
-			//Get info about this object
-			var url = this.get("url"),
-				model = this;
-
-			//Create an XHR
-			var xhr = new XMLHttpRequest();
-
-      //Open and send the request with the user's auth token
-      xhr.open('GET', url);
-
-			if(MetacatUI.appUserModel.get("loggedIn"))
-				xhr.withCredentials = true;
-
-			//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
-			xhr.onload = function(){
-
-        if( this.status == 404 ){
-          this.onerror.call(this);
-          return;
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @class SolrResult
+   * @classdesc A single result from the Solr search service
+   * @classcategory Models
+   * @extends Backbone.Model
+   */
+  var SolrResult = Backbone.Model.extend(
+    /** @lends SolrResult.prototype */ {
+      // This model contains all of the attributes found in the SOLR 'docs' field inside of the SOLR response element
+      defaults: {
+        abstract: null,
+        entityName: null,
+        indexed: true,
+        archived: false,
+        origin: "",
+        keywords: "",
+        title: "",
+        pubDate: "",
+        eastBoundCoord: "",
+        westBoundCoord: "",
+        northBoundCoord: "",
+        southBoundCoord: "",
+        attributeName: "",
+        beginDate: "",
+        endDate: "",
+        pubDate: "",
+        id: "",
+        seriesId: null,
+        resourceMap: null,
+        downloads: null,
+        citations: 0,
+        selected: false,
+        formatId: null,
+        formatType: null,
+        fileName: null,
+        datasource: null,
+        rightsHolder: null,
+        size: 0,
+        type: "",
+        url: null,
+        obsoletedBy: null,
+        geohash_9: null,
+        read_count_i: 0,
+        reads: 0,
+        isDocumentedBy: null,
+        isPublic: null,
+        isService: false,
+        serviceDescription: null,
+        serviceTitle: null,
+        serviceEndpoint: null,
+        serviceOutput: null,
+        notFound: false,
+        newestVersion: null,
+        //@type {string} - The system metadata XML as a string
+        systemMetadata: null,
+        provSources: [],
+        provDerivations: [],
+        //Provenance index fields
+        prov_generated: null,
+        prov_generatedByDataONEDN: null,
+        prov_generatedByExecution: null,
+        prov_generatedByFoafName: null,
+        prov_generatedByOrcid: null,
+        prov_generatedByProgram: null,
+        prov_generatedByUser: null,
+        prov_hasDerivations: null,
+        prov_hasSources: null,
+        prov_instanceOfClass: null,
+        prov_used: null,
+        prov_usedByDataONEDN: null,
+        prov_usedByExecution: null,
+        prov_usedByFoafName: null,
+        prov_usedByOrcid: null,
+        prov_usedByProgram: null,
+        prov_usedByUser: null,
+        prov_wasDerivedFrom: null,
+        prov_wasExecutedByExecution: null,
+        prov_wasExecutedByUser: null,
+        prov_wasGeneratedBy: null,
+        prov_wasInformedBy: null,
+      },
+
+      initialize: function () {
+        this.setURL();
+        this.on("change:id", this.setURL);
+
+        this.set("type", this.getType());
+        this.on("change:read_count_i", function () {
+          this.set("reads", this.get("read_count_i"));
+        });
+      },
+
+      type: "SolrResult",
+
+      // Toggle the `selected` state of the result
+      toggle: function () {
+        this.selected = !this.get("selected");
+      },
+
+      /**
+       * Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data
+       * @return {string}
+       */
+      getType: function () {
+        //The list of formatIds that are images
+        var imageIds = [
+          "image/gif",
+          "image/jp2",
+          "image/jpeg",
+          "image/png",
+          "image/svg xml",
+          "image/svg+xml",
+          "image/bmp",
+        ];
+        //The list of formatIds that are images
+        var pdfIds = ["application/pdf"];
+        var annotationIds = [
+          "http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html",
+        ];
+        var collectionIds = [
+          "https://purl.dataone.org/collections-1.0.0",
+          "https://purl.dataone.org/collections-1.1.0",
+        ];
+        var portalIds = [
+          "https://purl.dataone.org/portals-1.0.0",
+          "https://purl.dataone.org/portals-1.1.0",
+        ];
+
+        //Determine the type via provONE
+        var instanceOfClass = this.get("prov_instanceOfClass");
+        if (typeof instanceOfClass !== "undefined") {
+          var programClass = _.filter(instanceOfClass, function (className) {
+            return className.indexOf("#Program") > -1;
+          });
+          if (typeof programClass !== "undefined" && programClass.length)
+            return "program";
+        } else {
+          if (this.get("prov_generated") || this.get("prov_used"))
+            return "program";
         }
 
-			   //Get the file name to save this file as
-			   var filename = xhr.getResponseHeader('Content-Disposition');
-
-			   if(!filename){
-				   filename = model.get("fileName") || model.get("title") || model.get("id") || "download";
-			   }
-			   else
-				   filename = filename.substring(filename.indexOf("filename=")+9).replace(/"/g, "");
-
-         //Replace any whitespaces
-			   filename = filename.trim().replace(/ /g, "_");
-
-			   //For IE, we need to use the navigator API
-			   if (navigator && navigator.msSaveOrOpenBlob) {
-				   navigator.msSaveOrOpenBlob(xhr.response, filename);
-			   }
-			   //Other browsers can download it via a link
-			   else{
-				    var a = document.createElement('a');
-				    a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
-
-				    // Set the file name.
-				    a.download = filename
-
-				    a.style.display = 'none';
-				    document.body.appendChild(a);
-				    a.click();
-				    a.remove();
-			   }
-
-					model.trigger("downloadComplete");
-
-					// Track this event
-					MetacatUI.analytics?.trackEvent(
-						"download",
-						"Download DataONEObject", 
-						model.get("id")
-					);
-			};
-
-			xhr.onerror = function(e){
-        model.trigger("downloadError");
-
-				// Track the error
-				MetacatUI.analytics?.trackException(
-					`Download DataONEObject error: ${e || ""}`, model.get("id"), true
-				);
-			};
-
-			xhr.onprogress = function(e){
-			    if (e.lengthComputable){
-			        var percent = (e.loaded / e.total) * 100;
-			        model.set("downloadPercent", percent);
-			    }
-			};
-
-			xhr.responseType = "blob";
-
-			if(MetacatUI.appUserModel.get("loggedIn"))
-				xhr.setRequestHeader("Authorization", "Bearer " + MetacatUI.appUserModel.get("token"));
-
-			xhr.send();
-		},
-
-		getInfo: function(fields){
-			var model = this;
-
-			if(!fields)
-				var fields = "abstract,id,seriesId,fileName,resourceMap,formatType,formatId,obsoletedBy,isDocumentedBy,documents,title,origin,keywords,attributeName,pubDate,eastBoundCoord,westBoundCoord,northBoundCoord,southBoundCoord,beginDate,endDate,dateUploaded,archived,datasource,replicaMN,isAuthorized,isPublic,size,read_count_i,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription,serviceType,project,dateModified";
-
-			var escapeSpecialChar = MetacatUI.appSearchModel.escapeSpecialChar;
-
-			var query = "q=";
-
-			//If there is no seriesId set, then search for pid or sid
-			if(!this.get("seriesId"))
-				query += '(id:"' + escapeSpecialChar(encodeURIComponent(this.get("id"))) + '" OR seriesId:"' + escapeSpecialChar(encodeURIComponent(this.get("id"))) + '")';
-			//If a seriesId is specified, then search for that
-			else if(this.get("seriesId") && (this.get("id").length > 0))
-				query += '(seriesId:"' + escapeSpecialChar(encodeURIComponent(this.get("seriesId"))) + '" AND id:"' + escapeSpecialChar(encodeURIComponent(this.get("id"))) + '")';
-			//If only a seriesId is specified, then just search for the most recent version
-			else if(this.get("seriesId") && !this.get("id"))
-				query += 'seriesId:"' + escapeSpecialChar(encodeURIComponent(this.get("id"))) + '" -obsoletedBy:*';
-
-			query += "&fl=" + fields + //Specify the fields to return
-			         "&wt=json&rows=1000" + //Get the results in JSON format and get 1000 rows
-			         "&archived=archived:*"; //Get archived or unarchived content
-
-			var requestSettings = {
-				url: MetacatUI.appModel.get("queryServiceUrl") + query,
-				type: "GET",
-				success: function(data, response, xhr){
-          //If the Solr response was not as expected, trigger and error and exit
-          if( !data || typeof data.response == "undefined" ){
-            model.set("indexed", false);
-            model.trigger("getInfoError");
+        //Determine the type via file format
+        if (_.contains(collectionIds, this.get("formatId")))
+          return "collection";
+        if (_.contains(portalIds, this.get("formatId"))) return "portal";
+        if (this.get("formatType") == "METADATA") return "metadata";
+        if (_.contains(imageIds, this.get("formatId"))) return "image";
+        if (_.contains(pdfIds, this.get("formatId"))) return "PDF";
+        if (_.contains(annotationIds, this.get("formatId")))
+          return "annotation";
+        else return "data";
+      },
+
+      //Returns a plain-english version of the specific format ID (for selected ids)
+      getFormat: function () {
+        var formatMap = {
+          "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
+            "Microsoft Excel OpenXML",
+          "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
+            "Microsoft Word OpenXML",
+          "application/vnd.ms-excel.sheet.binary.macroEnabled.12":
+            "Microsoft Office Excel 2007 binary workbooks",
+          "application/vnd.openxmlformats-officedocument.presentationml.presentation":
+            "Microsoft Office OpenXML Presentation",
+          "application/vnd.ms-excel": "Microsoft Excel",
+          "application/msword": "Microsoft Word",
+          "application/vnd.ms-powerpoint": "Microsoft Powerpoint",
+          "text/html": "HTML",
+          "text/plain": "plain text (.txt)",
+          "video/avi": "Microsoft AVI file",
+          "video/x-ms-wmv": "Windows Media Video (.wmv)",
+          "audio/x-ms-wma": "Windows Media Audio (.wma)",
+          "application/vnd.google-earth.kml xml":
+            "Google Earth Keyhole Markup Language (KML)",
+          "http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html":
+            "annotation",
+          "application/mathematica": "Mathematica Notebook",
+          "application/postscript": "Postscript",
+          "application/rtf": "Rich Text Format (RTF)",
+          "application/xml": "XML Application",
+          "text/xml": "XML",
+          "application/x-fasta": "FASTA sequence file",
+          "nexus/1997": "NEXUS File Format for Systematic Information",
+          "anvl/erc-v02":
+            "Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13",
+          "http://purl.org/dryad/terms/":
+            "Dryad Metadata Application Profile Version 3.0",
+          "http://datadryad.org/profile/v3.1":
+            "Dryad Metadata Application Profile Version 3.1",
+          "application/pdf": "PDF",
+          "application/zip": "ZIP file",
+          "http://www.w3.org/TR/rdf-syntax-grammar": "RDF/XML",
+          "http://www.w3.org/TR/rdfa-syntax": "RDFa",
+          "application/rdf xml": "RDF",
+          "text/turtle": "TURTLE",
+          "text/n3": "N3",
+          "application/x-gzip": "GZIP Format",
+          "application/x-python": "Python script",
+          "http://www.w3.org/2005/Atom": "ATOM-1.0",
+          "application/octet-stream": "octet stream (application file)",
+          "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd":
+            "Darwin Core, v2.0",
+          "http://rs.tdwg.org/dwc/xsd/simpledarwincore/": "Simple Darwin Core",
+          "eml://ecoinformatics.org/eml-2.1.0": "EML v2.1.0",
+          "eml://ecoinformatics.org/eml-2.1.1": "EML v2.1.1",
+          "eml://ecoinformatics.org/eml-2.0.1": "EML v2.0.1",
+          "eml://ecoinformatics.org/eml-2.0.0": "EML v2.0.0",
+          "https://eml.ecoinformatics.org/eml-2.2.0": "EML v2.2.0",
+        };
+
+        return formatMap[this.get("formatId")] || this.get("formatId");
+      },
+
+      setURL: function () {
+        if (MetacatUI.appModel.get("objectServiceUrl"))
+          this.set(
+            "url",
+            MetacatUI.appModel.get("objectServiceUrl") +
+              encodeURIComponent(this.get("id")),
+          );
+        else if (MetacatUI.appModel.get("resolveServiceUrl"))
+          this.set(
+            "url",
+            MetacatUI.appModel.get("resolveServiceUrl") +
+              encodeURIComponent(this.get("id")),
+          );
+      },
+
+      /**
+       * Checks if the pid or sid or given string is a DOI
+       *
+       * @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model
+       * @returns {boolean} True if it is a DOI
+       */
+      isDOI: function (customString) {
+        return (
+          MetacatUI.appModel.isDOI(customString) ||
+          MetacatUI.appModel.isDOI(this.get("id")) ||
+          MetacatUI.appModel.isDOI(this.get("seriesId"))
+        );
+      },
+
+      /*
+       * Checks if the currently-logged-in user is authorized to change
+       * permissions (or other action if set as parameter) on this doc
+       * @param {string} [action=changePermission] - The action (read, write, or changePermission) to check
+       * if the current user has authorization to perform. By default checks for the highest level of permission.
+       */
+      checkAuthority: function (action = "changePermission") {
+        var authServiceUrl = MetacatUI.appModel.get("authServiceUrl");
+        if (!authServiceUrl) return false;
+
+        var model = this;
+
+        var requestSettings = {
+          url:
+            authServiceUrl +
+            encodeURIComponent(this.get("id")) +
+            "?action=" +
+            action,
+          type: "GET",
+          success: function (data, textStatus, xhr) {
+            model.set("isAuthorized_" + action, true);
+            model.set("isAuthorized", true);
+            model.trigger("change:isAuthorized");
+          },
+          error: function (xhr, textStatus, errorThrown) {
+            model.set("isAuthorized_" + action, false);
+            model.set("isAuthorized", false);
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      /*
+       * This method will download this object while sending the user's auth token in the request.
+       */
+      downloadWithCredentials: function () {
+        //if(this.get("isPublic")) return;
+
+        //Get info about this object
+        var url = this.get("url"),
+          model = this;
+
+        //Create an XHR
+        var xhr = new XMLHttpRequest();
+
+        //Open and send the request with the user's auth token
+        xhr.open("GET", url);
+
+        if (MetacatUI.appUserModel.get("loggedIn")) xhr.withCredentials = true;
+
+        //When the XHR is ready, create a link with the raw data (Blob) and click the link to download
+        xhr.onload = function () {
+          if (this.status == 404) {
+            this.onerror.call(this);
             return;
           }
 
-					var docs = data.response.docs;
-
-					if(docs.length == 1){
-            docs[0].resourceMap = model.parseResourceMapField(docs[0]);
-						model.set(docs[0]);
-						model.trigger("sync");
-					}
-					//If we searched by seriesId, then let's find the most recent version in the series
-					else if(docs.length > 1){
-						//Filter out docs that are obsoleted
-						var mostRecent = _.reject(docs, function(doc){
-							return (typeof doc.obsoletedBy !== "undefined");
-						});
-
-						//If there is only one doc that is not obsoleted (the most recent), then
-						// set this doc's values on this model
-						if(mostRecent.length == 1){
-              mostRecent[0].resourceMap = model.parseResourceMapField(mostRecent[0]);
-							model.set(mostRecent[0]);
-							model.trigger("sync");
-						}
-						else{
-							//If there are multiple docs without an obsoletedBy statement, then
-							// retreive the head of the series via the system metadata
-							var sysMetaRequestSettings = {
-								url: MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(docs[0].seriesId),
-								type: "GET",
-								success: function(sysMetaData){
-									//Get the identifier node from the system metadata
-									var seriesHeadID = $(sysMetaData).find("identifier").text();
-									//Get the doc from the Solr results with that identifier
-									var seriesHead = _.findWhere(docs, { id: seriesHeadID });
-
-									//If there is a doc in the Solr results list that matches the series head id
-									if(seriesHead){
-                    seriesHead.resourceMap = model.parseResourceMapField(seriesHead);
-										//Set those values on this model
-										model.set(seriesHead);
-									}
-									//Otherwise, just fall back on the first doc in the list
-									else if( mostRecent.length ){
-                    mostRecent[0].resourceMap = model.parseResourceMapField(mostRecent[0]);
-										model.set(mostRecent[0]);
-									}
-									else {
-                    docs[0].resourceMap = model.parseResourceMapField(docs[0]);
-										model.set(docs[0]);
-									}
-
-									model.trigger("sync");
-
-								},
-								error: function(xhr, textStatus, errorThrown){
-
-									// Fall back on the first doc in the list
-									if( mostRecent.length ){
-										model.set(mostRecent[0]);
-									}
-									else {
-										model.set(docs[0]);
-									}
-
-									model.trigger("sync");
-
-								}
-							};
-
-							$.ajax(_.extend(sysMetaRequestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-
-						}
-
-					}
-					else{
-						model.set("indexed", false);
-						//Try getting the system metadata as a backup
-						model.getSysMeta();
-					}
-				},
-				error: function(xhr, textStatus, errorThrown){
-					model.set("indexed", false);
-					model.trigger("getInfoError");
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		getCitationInfo: function(){
-			this.getInfo("id,seriesId,origin,pubDate,dateUploaded,title,datasource,project");
-		},
-
-		/*
-		 * Get the system metadata for this object
-		 */
-		getSysMeta: function(){
-			var url = MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(this.get("id")),
-				model = this;
-
-			var requestSettings = {
-				url: url,
-				type: "GET",
-				dataType: "text",
-				success: function(data, response, xhr){
-
-          if( data && data.length ){
-            model.set("systemMetadata", data);
-          }
-
-					//Check if this is archvied
-					var archived = ($(data).find("archived").text() == "true");
-					model.set("archived", archived);
-
-					//Get the file size
-					model.set("size", ($(data).find("size").text() || ""));
-
-					//Get the entity name
-					model.set("filename", ($(data).find("filename").text() || ""));
-
-					//Check if this is a metadata doc
-					var formatId = $(data).find("formatid").text() || "",
-						formatType;
-					model.set("formatId", formatId);
-					if((formatId.indexOf("ecoinformatics.org") > -1) ||
-							(formatId.indexOf("FGDC") > -1) ||
-							(formatId.indexOf("INCITS") > -1) ||
-							(formatId.indexOf("namespaces/netcdf") > -1) ||
-							(formatId.indexOf("waterML") > -1) ||
-							(formatId.indexOf("darwin") > -1) ||
-							(formatId.indexOf("dryad") > -1) ||
-							(formatId.indexOf("http://www.loc.gov/METS") > -1) ||
-							(formatId.indexOf("ddi:codebook:2_5") > -1) ||
-							(formatId.indexOf("http://www.icpsr.umich.edu/DDI") > -1) ||
-							(formatId.indexOf("http://purl.org/ornl/schema/mercury/terms/v1.0") > -1) ||
-							(formatId.indexOf("datacite") > -1) ||
-							(formatId.indexOf("isotc211") > -1) ||
-							(formatId.indexOf("metadata") > -1))
-						model.set("formatType", "METADATA");
-
-					//Trigger the sync event so the app knows we found the model info
-					model.trigger("sync");
-				},
-				error: function(response){
-
-          //When the user is unauthorized to access this object, trigger a 401 error
-          if( response.status == 401 ){
-            model.set("notFound", true);
-            model.trigger("401");
-          }
-          //When the object doesn't exist, trigger a 404 error
-          else if( response.status == 404 ){
-            model.set("notFound", true);
-      			model.trigger("404");
-          }
-          //Other error codes trigger a generic error
-          else{
-            model.trigger("error");
-          }
-
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		//Transgresses the obsolence chain until it finds the newest version that this user is authorized to read
-		findLatestVersion: function(newestVersion, possiblyNewer) {
-			// Make sure we have the /meta service configured
-			if(!MetacatUI.appModel.get('metaServiceUrl')) return;
-
-			//If no pid was supplied, use this model's id
-			if(!newestVersion){
-				var newestVersion = this.get("id");
-				var possiblyNewer = this.get("obsoletedBy");
-			}
-
-			//If this isn't obsoleted by anything, then there is no newer version
-			if(!possiblyNewer){
-				this.set("newestVersion", newestVersion);
-				return;
-			}
-
-			var model = this;
-
-			//Get the system metadata for the possibly newer version
-			var requestSettings = {
-				url: MetacatUI.appModel.get('metaServiceUrl') + encodeURIComponent(possiblyNewer),
-				type: "GET",
-				success: function(data) {
-
-					// the response may have an obsoletedBy element
-					var obsoletedBy = $(data).find("obsoletedBy").text();
-
-					//If there is an even newer version, then get it and rerun this function
-					if(obsoletedBy)
-						model.findLatestVersion(possiblyNewer, obsoletedBy);
-					//If there isn't a newer version, then this is it
-					else
-						model.set("newestVersion", possiblyNewer);
-
-				},
-				error: function(xhr){
-					//If this newer version isn't found or accessible, then save the last
-          // accessible id as the newest version
-          if(xhr.status == 401 || xhr.status == 404 || xhr.status == "401" ||
-             xhr.status == "404"){
-            model.set("newestVersion", newestVersion);
+          //Get the file name to save this file as
+          var filename = xhr.getResponseHeader("Content-Disposition");
+
+          if (!filename) {
+            filename =
+              model.get("fileName") ||
+              model.get("title") ||
+              model.get("id") ||
+              "download";
+          } else
+            filename = filename
+              .substring(filename.indexOf("filename=") + 9)
+              .replace(/"/g, "");
+
+          //Replace any whitespaces
+          filename = filename.trim().replace(/ /g, "_");
+
+          //For IE, we need to use the navigator API
+          if (navigator && navigator.msSaveOrOpenBlob) {
+            navigator.msSaveOrOpenBlob(xhr.response, filename);
           }
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-
-		},
-
-		/**** Provenance-related functions ****/
-		/*
-		 * Returns true if this provenance field points to a source of this data or metadata object
-		 */
-		isSourceField: function(field){
-			if((typeof field == "undefined") || !field) return false;
-			if(!_.contains(MetacatUI.appSearchModel.getProvFields(), field)) return false;
-
-			if(field == "prov_generatedByExecution" ||
-			   field == "prov_generatedByProgram"   ||
-			   field == "prov_used" 		  		||
-			   field == "prov_wasDerivedFrom" 		||
-			   field == "prov_wasInformedBy")
-				return true;
-			else
-				return false;
-		},
-
-		/*
-		 * Returns true if this provenance field points to a derivation of this data or metadata object
-		 */
-		isDerivationField: function(field){
-			if((typeof field == "undefined") || !field) return false;
-			if(!_.contains(MetacatUI.appSearchModel.getProvFields(), field)) return false;
-
-			if(field == "prov_usedByExecution" ||
-			   field == "prov_usedByProgram"   ||
-			   field == "prov_hasDerivations" ||
-			   field == "prov_generated")
-				return true;
-			else
-				return false;
-		},
-
-		/*
-		 * Returns true if this SolrResult has a provenance trace (i.e. has either sources or derivations)
-		 */
-		hasProvTrace: function(){
-
-			if(this.get("formatType") == "METADATA"){
-				if(this.get("prov_hasSources") || this.get("prov_hasDerivations"))
-					return true;
-			}
-
-			var fieldNames = MetacatUI.appSearchModel.getProvFields(),
-				currentField = "";
-
-			for(var i=0; i < fieldNames.length; i++){
-				currentField = fieldNames[i];
-				if(this.has(currentField))
-					return true;
-			}
-
-			return false;
-		},
-
-		/*
-		 * Returns an array of all the IDs of objects that are sources of this object
-		 */
-		getSources: function(){
-			var sources = new Array(),
-				model = this,
-				//Get the prov fields but leave out references to executions which are not used in the UI yet
-				fields = _.reject(MetacatUI.appSearchModel.getProvFields(), function(f){ return f.indexOf("xecution") > -1 }); //Leave out the first e in execution so we don't have to worry about case sensitivity
-
-			_.each(fields, function(provField, i){
-				if(model.isSourceField(provField) && model.has(provField))
-					sources.push(model.get(provField));
-			});
-
-			return _.uniq(_.flatten(sources));
-		},
-
-		/*
-		 * Returns an array of all the IDs of objects that are derivations of this object
-		 */
-		getDerivations: function(){
-			var derivations = new Array(),
-				model = this,
-				//Get the prov fields but leave out references to executions which are not used in the UI yet
-				fields = _.reject(MetacatUI.appSearchModel.getProvFields(), function(f){ return f.indexOf("xecution") > -1 }); //Leave out the first e in execution so we don't have to worry about case sensitivity
-
-			_.each(fields, function(provField, i){
-				if(model.isDerivationField(provField) && model.has(provField))
-					derivations.push(model.get(provField));
-			});
-
-			return _.uniq(_.flatten(derivations));
-		},
-
-		getInputs: function(){
-			return this.get("prov_used");
-		},
-
-		getOutputs: function(){
-			return this.get("prov_generated");
-		},
-
-    /*
-    * Uses the app configuration to check if this model's metrics should be hidden in the display
-    *
-    * @return {boolean}
-    */
-    hideMetrics: function(){
-
-      //If the AppModel is configured with cases of where to hide metrics,
-      if( typeof MetacatUI.appModel.get("hideMetricsWhen") == "object" && MetacatUI.appModel.get("hideMetricsWhen") ){
-
-        //Check for at least one match
-        return _.some( MetacatUI.appModel.get("hideMetricsWhen"), function(value, modelProperty){
-
-          //Get the value of this property from this model
-          var modelValue = this.get(modelProperty);
-
-          //Check for the presence of this model's value in the AppModel value
-          if( Array.isArray(value) && typeof modelValue == "string" ){
-            return _.contains(value, modelValue)
+          //Other browsers can download it via a link
+          else {
+            var a = document.createElement("a");
+            a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
+
+            // Set the file name.
+            a.download = filename;
+
+            a.style.display = "none";
+            document.body.appendChild(a);
+            a.click();
+            a.remove();
           }
-          //Check for the presence of the AppModel's value in this model's value
-          else if( typeof value == "string" && Array.isArray(modelValue) ){
-            return _.contains(modelValue, value);
-          }
-          //Check for overlap of two arrays
-          else if( Array.isArray(value) && Array.isArray(modelValue) ){
-            return ( _.intersection(value, modelValue).length > 0 );
-          }
-          //If the AppModel value is a function, execute it
-          else if( typeof value == "function" ){
-            return value(modelValue);
-          }
-          //Otherwise, just check for equality
-          else{
-            return value === modelValue;
-          }
-
-        }, this);
-      }
-      else {
-        return false;
-      }
-    },
-
-    /**
-    * Creates a URL for viewing more information about this metadata
-    * @return {string}
-    */
-    createViewURL: function(){
-      return (this.getType() == "portal" || this.getType() == "collection") ?
-              MetacatUI.root + "/" + MetacatUI.appModel.get("portalTermPlural") + "/" + encodeURIComponent((this.get("label") || this.get("seriesId") || this.get("id"))) :
-              MetacatUI.root + "/view/" + encodeURIComponent((this.get("seriesId") || this.get("id")));
-    },
 
-    parseResourceMapField: function(json){
-      if( typeof json.resourceMap == "string" ){
-        return json.resourceMap.trim();
-      }
-      else if( Array.isArray(json.resourceMap) ){
-        let newResourceMapIds = [];
-        _.each(json.resourceMap, function(rMapId){
-          if( typeof rMapId == "string" ){
-            newResourceMapIds.push(rMapId.trim());
+          model.trigger("downloadComplete");
+
+          // Track this event
+          MetacatUI.analytics?.trackEvent(
+            "download",
+            "Download DataONEObject",
+            model.get("id"),
+          );
+        };
+
+        xhr.onerror = function (e) {
+          model.trigger("downloadError");
+
+          // Track the error
+          MetacatUI.analytics?.trackException(
+            `Download DataONEObject error: ${e || ""}`,
+            model.get("id"),
+            true,
+          );
+        };
+
+        xhr.onprogress = function (e) {
+          if (e.lengthComputable) {
+            var percent = (e.loaded / e.total) * 100;
+            model.set("downloadPercent", percent);
           }
-        });
-        return newResourceMapIds;
-      }
-
-      //If nothing works so far, return an empty array
-      return [];
-    },
-
-		/****************************/
-
-		/**
-		 * Convert number of bytes into human readable format
-		 *
-		 * @param integer bytes     Number of bytes to convert
-		 * @param integer precision Number of digits after the decimal separator
-		 * @return string
-		 */
-		bytesToSize: function(bytes, precision){
-		    var kibibyte = 1024;
-		    var mebibyte = kibibyte * 1024;
-		    var gibibyte = mebibyte * 1024;
-		    var tebibyte = gibibyte * 1024;
+        };
+
+        xhr.responseType = "blob";
+
+        if (MetacatUI.appUserModel.get("loggedIn"))
+          xhr.setRequestHeader(
+            "Authorization",
+            "Bearer " + MetacatUI.appUserModel.get("token"),
+          );
+
+        xhr.send();
+      },
+
+      getInfo: function (fields) {
+        var model = this;
+
+        if (!fields)
+          var fields =
+            "abstract,id,seriesId,fileName,resourceMap,formatType,formatId,obsoletedBy,isDocumentedBy,documents,title,origin,keywords,attributeName,pubDate,eastBoundCoord,westBoundCoord,northBoundCoord,southBoundCoord,beginDate,endDate,dateUploaded,archived,datasource,replicaMN,isAuthorized,isPublic,size,read_count_i,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription,serviceType,project,dateModified";
+
+        var escapeSpecialChar = MetacatUI.appSearchModel.escapeSpecialChar;
+
+        var query = "q=";
+
+        //If there is no seriesId set, then search for pid or sid
+        if (!this.get("seriesId"))
+          query +=
+            '(id:"' +
+            escapeSpecialChar(encodeURIComponent(this.get("id"))) +
+            '" OR seriesId:"' +
+            escapeSpecialChar(encodeURIComponent(this.get("id"))) +
+            '")';
+        //If a seriesId is specified, then search for that
+        else if (this.get("seriesId") && this.get("id").length > 0)
+          query +=
+            '(seriesId:"' +
+            escapeSpecialChar(encodeURIComponent(this.get("seriesId"))) +
+            '" AND id:"' +
+            escapeSpecialChar(encodeURIComponent(this.get("id"))) +
+            '")';
+        //If only a seriesId is specified, then just search for the most recent version
+        else if (this.get("seriesId") && !this.get("id"))
+          query +=
+            'seriesId:"' +
+            escapeSpecialChar(encodeURIComponent(this.get("id"))) +
+            '" -obsoletedBy:*';
+
+        query +=
+          "&fl=" +
+          fields + //Specify the fields to return
+          "&wt=json&rows=1000" + //Get the results in JSON format and get 1000 rows
+          "&archived=archived:*"; //Get archived or unarchived content
+
+        var requestSettings = {
+          url: MetacatUI.appModel.get("queryServiceUrl") + query,
+          type: "GET",
+          success: function (data, response, xhr) {
+            //If the Solr response was not as expected, trigger and error and exit
+            if (!data || typeof data.response == "undefined") {
+              model.set("indexed", false);
+              model.trigger("getInfoError");
+              return;
+            }
+
+            var docs = data.response.docs;
+
+            if (docs.length == 1) {
+              docs[0].resourceMap = model.parseResourceMapField(docs[0]);
+              model.set(docs[0]);
+              model.trigger("sync");
+            }
+            //If we searched by seriesId, then let's find the most recent version in the series
+            else if (docs.length > 1) {
+              //Filter out docs that are obsoleted
+              var mostRecent = _.reject(docs, function (doc) {
+                return typeof doc.obsoletedBy !== "undefined";
+              });
+
+              //If there is only one doc that is not obsoleted (the most recent), then
+              // set this doc's values on this model
+              if (mostRecent.length == 1) {
+                mostRecent[0].resourceMap = model.parseResourceMapField(
+                  mostRecent[0],
+                );
+                model.set(mostRecent[0]);
+                model.trigger("sync");
+              } else {
+                //If there are multiple docs without an obsoletedBy statement, then
+                // retreive the head of the series via the system metadata
+                var sysMetaRequestSettings = {
+                  url:
+                    MetacatUI.appModel.get("metaServiceUrl") +
+                    encodeURIComponent(docs[0].seriesId),
+                  type: "GET",
+                  success: function (sysMetaData) {
+                    //Get the identifier node from the system metadata
+                    var seriesHeadID = $(sysMetaData).find("identifier").text();
+                    //Get the doc from the Solr results with that identifier
+                    var seriesHead = _.findWhere(docs, { id: seriesHeadID });
+
+                    //If there is a doc in the Solr results list that matches the series head id
+                    if (seriesHead) {
+                      seriesHead.resourceMap =
+                        model.parseResourceMapField(seriesHead);
+                      //Set those values on this model
+                      model.set(seriesHead);
+                    }
+                    //Otherwise, just fall back on the first doc in the list
+                    else if (mostRecent.length) {
+                      mostRecent[0].resourceMap = model.parseResourceMapField(
+                        mostRecent[0],
+                      );
+                      model.set(mostRecent[0]);
+                    } else {
+                      docs[0].resourceMap = model.parseResourceMapField(
+                        docs[0],
+                      );
+                      model.set(docs[0]);
+                    }
+
+                    model.trigger("sync");
+                  },
+                  error: function (xhr, textStatus, errorThrown) {
+                    // Fall back on the first doc in the list
+                    if (mostRecent.length) {
+                      model.set(mostRecent[0]);
+                    } else {
+                      model.set(docs[0]);
+                    }
+
+                    model.trigger("sync");
+                  },
+                };
+
+                $.ajax(
+                  _.extend(
+                    sysMetaRequestSettings,
+                    MetacatUI.appUserModel.createAjaxSettings(),
+                  ),
+                );
+              }
+            } else {
+              model.set("indexed", false);
+              //Try getting the system metadata as a backup
+              model.getSysMeta();
+            }
+          },
+          error: function (xhr, textStatus, errorThrown) {
+            model.set("indexed", false);
+            model.trigger("getInfoError");
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      getCitationInfo: function () {
+        this.getInfo(
+          "id,seriesId,origin,pubDate,dateUploaded,title,datasource,project",
+        );
+      },
+
+      /*
+       * Get the system metadata for this object
+       */
+      getSysMeta: function () {
+        var url =
+            MetacatUI.appModel.get("metaServiceUrl") +
+            encodeURIComponent(this.get("id")),
+          model = this;
+
+        var requestSettings = {
+          url: url,
+          type: "GET",
+          dataType: "text",
+          success: function (data, response, xhr) {
+            if (data && data.length) {
+              model.set("systemMetadata", data);
+            }
+
+            //Check if this is archvied
+            var archived = $(data).find("archived").text() == "true";
+            model.set("archived", archived);
+
+            //Get the file size
+            model.set("size", $(data).find("size").text() || "");
+
+            //Get the entity name
+            model.set("filename", $(data).find("filename").text() || "");
+
+            //Check if this is a metadata doc
+            var formatId = $(data).find("formatid").text() || "",
+              formatType;
+            model.set("formatId", formatId);
+            if (
+              formatId.indexOf("ecoinformatics.org") > -1 ||
+              formatId.indexOf("FGDC") > -1 ||
+              formatId.indexOf("INCITS") > -1 ||
+              formatId.indexOf("namespaces/netcdf") > -1 ||
+              formatId.indexOf("waterML") > -1 ||
+              formatId.indexOf("darwin") > -1 ||
+              formatId.indexOf("dryad") > -1 ||
+              formatId.indexOf("http://www.loc.gov/METS") > -1 ||
+              formatId.indexOf("ddi:codebook:2_5") > -1 ||
+              formatId.indexOf("http://www.icpsr.umich.edu/DDI") > -1 ||
+              formatId.indexOf(
+                "http://purl.org/ornl/schema/mercury/terms/v1.0",
+              ) > -1 ||
+              formatId.indexOf("datacite") > -1 ||
+              formatId.indexOf("isotc211") > -1 ||
+              formatId.indexOf("metadata") > -1
+            )
+              model.set("formatType", "METADATA");
+
+            //Trigger the sync event so the app knows we found the model info
+            model.trigger("sync");
+          },
+          error: function (response) {
+            //When the user is unauthorized to access this object, trigger a 401 error
+            if (response.status == 401) {
+              model.set("notFound", true);
+              model.trigger("401");
+            }
+            //When the object doesn't exist, trigger a 404 error
+            else if (response.status == 404) {
+              model.set("notFound", true);
+              model.trigger("404");
+            }
+            //Other error codes trigger a generic error
+            else {
+              model.trigger("error");
+            }
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      //Transgresses the obsolence chain until it finds the newest version that this user is authorized to read
+      findLatestVersion: function (newestVersion, possiblyNewer) {
+        // Make sure we have the /meta service configured
+        if (!MetacatUI.appModel.get("metaServiceUrl")) return;
+
+        //If no pid was supplied, use this model's id
+        if (!newestVersion) {
+          var newestVersion = this.get("id");
+          var possiblyNewer = this.get("obsoletedBy");
+        }
 
-		    if(typeof bytes === "undefined") var bytes = this.get("size");
+        //If this isn't obsoleted by anything, then there is no newer version
+        if (!possiblyNewer) {
+          this.set("newestVersion", newestVersion);
+          return;
+        }
 
-		    if ((bytes >= 0) && (bytes < kibibyte)) {
-		        return bytes + ' B';
+        var model = this;
+
+        //Get the system metadata for the possibly newer version
+        var requestSettings = {
+          url:
+            MetacatUI.appModel.get("metaServiceUrl") +
+            encodeURIComponent(possiblyNewer),
+          type: "GET",
+          success: function (data) {
+            // the response may have an obsoletedBy element
+            var obsoletedBy = $(data).find("obsoletedBy").text();
+
+            //If there is an even newer version, then get it and rerun this function
+            if (obsoletedBy)
+              model.findLatestVersion(possiblyNewer, obsoletedBy);
+            //If there isn't a newer version, then this is it
+            else model.set("newestVersion", possiblyNewer);
+          },
+          error: function (xhr) {
+            //If this newer version isn't found or accessible, then save the last
+            // accessible id as the newest version
+            if (
+              xhr.status == 401 ||
+              xhr.status == 404 ||
+              xhr.status == "401" ||
+              xhr.status == "404"
+            ) {
+              model.set("newestVersion", newestVersion);
+            }
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      /**** Provenance-related functions ****/
+      /*
+       * Returns true if this provenance field points to a source of this data or metadata object
+       */
+      isSourceField: function (field) {
+        if (typeof field == "undefined" || !field) return false;
+        if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
+          return false;
+
+        if (
+          field == "prov_generatedByExecution" ||
+          field == "prov_generatedByProgram" ||
+          field == "prov_used" ||
+          field == "prov_wasDerivedFrom" ||
+          field == "prov_wasInformedBy"
+        )
+          return true;
+        else return false;
+      },
+
+      /*
+       * Returns true if this provenance field points to a derivation of this data or metadata object
+       */
+      isDerivationField: function (field) {
+        if (typeof field == "undefined" || !field) return false;
+        if (!_.contains(MetacatUI.appSearchModel.getProvFields(), field))
+          return false;
+
+        if (
+          field == "prov_usedByExecution" ||
+          field == "prov_usedByProgram" ||
+          field == "prov_hasDerivations" ||
+          field == "prov_generated"
+        )
+          return true;
+        else return false;
+      },
+
+      /*
+       * Returns true if this SolrResult has a provenance trace (i.e. has either sources or derivations)
+       */
+      hasProvTrace: function () {
+        if (this.get("formatType") == "METADATA") {
+          if (this.get("prov_hasSources") || this.get("prov_hasDerivations"))
+            return true;
+        }
 
-		    } else if ((bytes >= kibibyte) && (bytes < mebibyte)) {
-		        return (bytes / kibibyte).toFixed(precision) + ' KiB';
+        var fieldNames = MetacatUI.appSearchModel.getProvFields(),
+          currentField = "";
 
-		    } else if ((bytes >= mebibyte) && (bytes < gibibyte)) {
-		        return (bytes / mebibyte).toFixed(precision) + ' MiB';
+        for (var i = 0; i < fieldNames.length; i++) {
+          currentField = fieldNames[i];
+          if (this.has(currentField)) return true;
+        }
 
-		    } else if ((bytes >= gibibyte) && (bytes < tebibyte)) {
-		        return (bytes / gibibyte).toFixed(precision) + ' GiB';
+        return false;
+      },
+
+      /*
+       * Returns an array of all the IDs of objects that are sources of this object
+       */
+      getSources: function () {
+        var sources = new Array(),
+          model = this,
+          //Get the prov fields but leave out references to executions which are not used in the UI yet
+          fields = _.reject(
+            MetacatUI.appSearchModel.getProvFields(),
+            function (f) {
+              return f.indexOf("xecution") > -1;
+            },
+          ); //Leave out the first e in execution so we don't have to worry about case sensitivity
+
+        _.each(fields, function (provField, i) {
+          if (model.isSourceField(provField) && model.has(provField))
+            sources.push(model.get(provField));
+        });
 
-		    } else if (bytes >= tebibyte) {
-		        return (bytes / tebibyte).toFixed(precision) + ' TiB';
+        return _.uniq(_.flatten(sources));
+      },
+
+      /*
+       * Returns an array of all the IDs of objects that are derivations of this object
+       */
+      getDerivations: function () {
+        var derivations = new Array(),
+          model = this,
+          //Get the prov fields but leave out references to executions which are not used in the UI yet
+          fields = _.reject(
+            MetacatUI.appSearchModel.getProvFields(),
+            function (f) {
+              return f.indexOf("xecution") > -1;
+            },
+          ); //Leave out the first e in execution so we don't have to worry about case sensitivity
+
+        _.each(fields, function (provField, i) {
+          if (model.isDerivationField(provField) && model.has(provField))
+            derivations.push(model.get(provField));
+        });
 
-		    } else {
-		        return bytes + ' B';
-		    }
-		}
+        return _.uniq(_.flatten(derivations));
+      },
+
+      getInputs: function () {
+        return this.get("prov_used");
+      },
+
+      getOutputs: function () {
+        return this.get("prov_generated");
+      },
+
+      /*
+       * Uses the app configuration to check if this model's metrics should be hidden in the display
+       *
+       * @return {boolean}
+       */
+      hideMetrics: function () {
+        //If the AppModel is configured with cases of where to hide metrics,
+        if (
+          typeof MetacatUI.appModel.get("hideMetricsWhen") == "object" &&
+          MetacatUI.appModel.get("hideMetricsWhen")
+        ) {
+          //Check for at least one match
+          return _.some(
+            MetacatUI.appModel.get("hideMetricsWhen"),
+            function (value, modelProperty) {
+              //Get the value of this property from this model
+              var modelValue = this.get(modelProperty);
+
+              //Check for the presence of this model's value in the AppModel value
+              if (Array.isArray(value) && typeof modelValue == "string") {
+                return _.contains(value, modelValue);
+              }
+              //Check for the presence of the AppModel's value in this model's value
+              else if (typeof value == "string" && Array.isArray(modelValue)) {
+                return _.contains(modelValue, value);
+              }
+              //Check for overlap of two arrays
+              else if (Array.isArray(value) && Array.isArray(modelValue)) {
+                return _.intersection(value, modelValue).length > 0;
+              }
+              //If the AppModel value is a function, execute it
+              else if (typeof value == "function") {
+                return value(modelValue);
+              }
+              //Otherwise, just check for equality
+              else {
+                return value === modelValue;
+              }
+            },
+            this,
+          );
+        } else {
+          return false;
+        }
+      },
+
+      /**
+       * Creates a URL for viewing more information about this metadata
+       * @return {string}
+       */
+      createViewURL: function () {
+        return this.getType() == "portal" || this.getType() == "collection"
+          ? MetacatUI.root +
+              "/" +
+              MetacatUI.appModel.get("portalTermPlural") +
+              "/" +
+              encodeURIComponent(
+                this.get("label") || this.get("seriesId") || this.get("id"),
+              )
+          : MetacatUI.root +
+              "/view/" +
+              encodeURIComponent(this.get("seriesId") || this.get("id"));
+      },
+
+      parseResourceMapField: function (json) {
+        if (typeof json.resourceMap == "string") {
+          return json.resourceMap.trim();
+        } else if (Array.isArray(json.resourceMap)) {
+          let newResourceMapIds = [];
+          _.each(json.resourceMap, function (rMapId) {
+            if (typeof rMapId == "string") {
+              newResourceMapIds.push(rMapId.trim());
+            }
+          });
+          return newResourceMapIds;
+        }
 
-	});
-	return SolrResult;
+        //If nothing works so far, return an empty array
+        return [];
+      },
+
+      /****************************/
+
+      /**
+       * Convert number of bytes into human readable format
+       *
+       * @param integer bytes     Number of bytes to convert
+       * @param integer precision Number of digits after the decimal separator
+       * @return string
+       */
+      bytesToSize: function (bytes, precision) {
+        var kibibyte = 1024;
+        var mebibyte = kibibyte * 1024;
+        var gibibyte = mebibyte * 1024;
+        var tebibyte = gibibyte * 1024;
+
+        if (typeof bytes === "undefined") var bytes = this.get("size");
+
+        if (bytes >= 0 && bytes < kibibyte) {
+          return bytes + " B";
+        } else if (bytes >= kibibyte && bytes < mebibyte) {
+          return (bytes / kibibyte).toFixed(precision) + " KiB";
+        } else if (bytes >= mebibyte && bytes < gibibyte) {
+          return (bytes / mebibyte).toFixed(precision) + " MiB";
+        } else if (bytes >= gibibyte && bytes < tebibyte) {
+          return (bytes / gibibyte).toFixed(precision) + " GiB";
+        } else if (bytes >= tebibyte) {
+          return (bytes / tebibyte).toFixed(precision) + " TiB";
+        } else {
+          return bytes + " B";
+        }
+      },
+    },
+  );
+  return SolrResult;
 });
 
diff --git a/docs/docs/src_js_models_Stats.js.html b/docs/docs/src_js_models_Stats.js.html index e8f63ac47..cc00437a9 100644 --- a/docs/docs/src_js_models_Stats.js.html +++ b/docs/docs/src_js_models_Stats.js.html @@ -44,119 +44,123 @@

Source: src/js/models/Stats.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'promise'],
-  function($, _, Backbone, Promise) {
-  'use strict';
+            
define(["jquery", "underscore", "backbone", "promise"], function (
+  $,
+  _,
+  Backbone,
+  Promise,
+) {
+  "use strict";
 
   /**
-  * @class Stats
-  * @classdesc This model contains all a collection of statistics/metrics about a collection of DataONE objects
-  * @classcategory Models
-  * @name Stats
-  * @extends Backbone.Model
-  * @constructor
-  */
+   * @class Stats
+   * @classdesc This model contains all a collection of statistics/metrics about a collection of DataONE objects
+   * @classcategory Models
+   * @name Stats
+   * @extends Backbone.Model
+   * @constructor
+   */
   var Stats = Backbone.Model.extend(
-    /** @lends Stats.prototype */{
-
-    /**
-    * Default attributes for Stats models
-    * @type {Object}
-    * @property {string} query - The base query that defines the data collection to get statistis about.
-    * @property {string} postQuery - A copy of the `query`, but without any URL encoding
-    * @property {boolean} isSystemMetadataQuery - If true, the `query` set on this model is only filtering on system metadata fields which are common between both metadata and data objects.
-    * @property {number} metadataCount - The number of metadata objects in this data collection @readonly
-    * @property {number} dataCount - The number of data objects in this data collection
-    * @property {number} totalCount - The number of metadata and data objects in this data collection. Essentially this is the sum of metadataCount and dataCount
-    * @property {number|string[]} metadataFormatIDs - An array of metadata formatIds and the number of metadata objects with that formatId. Uses same structure as Solr facet counts: ["text/csv", 5]
-    * @property {number|string[]} dataFormatIDs - An array of data formatIds and the number of data objects with that formatId. Uses same structure as Solr facet counts: ["text/csv", 5]
-    * @property {string} firstUpdate - The earliest upload date for any object in this collection, excluding uploads of obsoleted objects
-    * @property {number|string[]} dataUpdateDates - An array of date strings and the number of data objects uploaded on that date. Uses same structure as Solr facet counts: ["2015-08-02", 5]
-    * @property {number|string[]} metadataUpdateDates An array of date strings and the number of data objects uploaded on that date. Uses same structure as Solr facet counts: ["2015-08-02", 5]
-    * @property {string} firstBeginDate - An ISO date string of the earliest year that this data collection describes, from the science metadata
-    * @property {string} lastEndDate - An ISO date string of the latest year that this data collection describes, from the science metadata
-    * @property {string} firstPossibleDate - The first possible date (as a string) that data could have been collected. This is to weed out badly formatted dates when sending queries.
-    * @property {object} temporalCoverage A simple object of date ranges (the object key) and the number of metadata objects uploaded in that date range (the object value). Example: { "1990-2000": 5 }
-    * @property {number} queryCoverageFrom - The year to start the temporal coverage range query
-    * @property {number} queryCoverageUntil - The year to end the temporal coverage range query
-    * @property {number} metadataTotalSize - The total number of bytes of all metadata files
-    * @property {number} dataTotalSize - The total number of bytes of all data files
-    * @property {number} totalSize - The total number of bytes or metadata and data files
-    * @property {boolean} hideMetadataAssessment - If true, metadata assessment scores will not be retrieved
-    * @property {Image} mdqScoresImage - The Image objet of an aggregated metadata assessment chart
-    * @property {number} maxQueryLength - The maximum query length that will be sent via GET to the query service. Queries that go beyond this length will be sent via POST, if POST is enabled in the AppModel
-    * @property {string} mdqImageId - The identifier to use in the request for the metadata assessment chart
-    */
-    defaults: function(){
-      return{
-      query: "*:* ",
-      postQuery: "",
-      isSystemMetadataQuery: false,
-
-      metadataCount: 0,
-      dataCount: 0,
-      totalCount: 0,
-
-      metadataFormatIDs: [],
-      dataFormatIDs: [],
-
-      firstUpload: 0,
-      totalUploads: 0,
-      metadataUploads: null,
-      dataUploads: null,
-      metadataUploadDates: null,
-      dataUploadDates: null,
-
-      firstUpdate: null,
-      dataUpdateDates: null,
-      metadataUpdateDates: null,
-
-      firstBeginDate: null,
-      lastEndDate : null,
-      firstPossibleDate: "1800-01-01T00:00:00Z",
-      temporalCoverage: 0,
-      queryCoverageFrom: null,
-      queryCoverageUntil: null,
-
-      metadataTotalSize: null,
-      dataTotalSize: null,
-      totalSize: null,
-
-      hideMetadataAssessment: false,
-      mdqScoresImage: null,
-      mdqImageId: null,
-
-      //HTTP GET requests are typically limited to 2,083 characters. So query lengths
-      // should have this maximum before switching over to HTTP POST
-      maxQueryLength: 2000
-    }},
-
-    /**
-    * This function serves as a shorthand way to get all of the statistics stored in the model
-    */
-    getAll: function(){
-
-      // Only get the MetaDIG scores if MetacatUI is configured to display metadata assesments
-      // AND this model has them enabled, too.
-      if ( !MetacatUI.appModel.get("hideSummaryMetadataAssessment") && !this.get("hideMetadataAssessment") ){
-        this.getMdqScores();
-      }
-
-      //Send the call the get both the metadata and data stats
-      this.getMetadataStats();
-      this.getDataStats();
-
-    },
+    /** @lends Stats.prototype */ {
+      /**
+       * Default attributes for Stats models
+       * @type {Object}
+       * @property {string} query - The base query that defines the data collection to get statistis about.
+       * @property {string} postQuery - A copy of the `query`, but without any URL encoding
+       * @property {boolean} isSystemMetadataQuery - If true, the `query` set on this model is only filtering on system metadata fields which are common between both metadata and data objects.
+       * @property {number} metadataCount - The number of metadata objects in this data collection @readonly
+       * @property {number} dataCount - The number of data objects in this data collection
+       * @property {number} totalCount - The number of metadata and data objects in this data collection. Essentially this is the sum of metadataCount and dataCount
+       * @property {number|string[]} metadataFormatIDs - An array of metadata formatIds and the number of metadata objects with that formatId. Uses same structure as Solr facet counts: ["text/csv", 5]
+       * @property {number|string[]} dataFormatIDs - An array of data formatIds and the number of data objects with that formatId. Uses same structure as Solr facet counts: ["text/csv", 5]
+       * @property {string} firstUpdate - The earliest upload date for any object in this collection, excluding uploads of obsoleted objects
+       * @property {number|string[]} dataUpdateDates - An array of date strings and the number of data objects uploaded on that date. Uses same structure as Solr facet counts: ["2015-08-02", 5]
+       * @property {number|string[]} metadataUpdateDates An array of date strings and the number of data objects uploaded on that date. Uses same structure as Solr facet counts: ["2015-08-02", 5]
+       * @property {string} firstBeginDate - An ISO date string of the earliest year that this data collection describes, from the science metadata
+       * @property {string} lastEndDate - An ISO date string of the latest year that this data collection describes, from the science metadata
+       * @property {string} firstPossibleDate - The first possible date (as a string) that data could have been collected. This is to weed out badly formatted dates when sending queries.
+       * @property {object} temporalCoverage A simple object of date ranges (the object key) and the number of metadata objects uploaded in that date range (the object value). Example: { "1990-2000": 5 }
+       * @property {number} queryCoverageFrom - The year to start the temporal coverage range query
+       * @property {number} queryCoverageUntil - The year to end the temporal coverage range query
+       * @property {number} metadataTotalSize - The total number of bytes of all metadata files
+       * @property {number} dataTotalSize - The total number of bytes of all data files
+       * @property {number} totalSize - The total number of bytes or metadata and data files
+       * @property {boolean} hideMetadataAssessment - If true, metadata assessment scores will not be retrieved
+       * @property {Image} mdqScoresImage - The Image objet of an aggregated metadata assessment chart
+       * @property {number} maxQueryLength - The maximum query length that will be sent via GET to the query service. Queries that go beyond this length will be sent via POST, if POST is enabled in the AppModel
+       * @property {string} mdqImageId - The identifier to use in the request for the metadata assessment chart
+       */
+      defaults: function () {
+        return {
+          query: "*:* ",
+          postQuery: "",
+          isSystemMetadataQuery: false,
+
+          metadataCount: 0,
+          dataCount: 0,
+          totalCount: 0,
+
+          metadataFormatIDs: [],
+          dataFormatIDs: [],
+
+          firstUpload: 0,
+          totalUploads: 0,
+          metadataUploads: null,
+          dataUploads: null,
+          metadataUploadDates: null,
+          dataUploadDates: null,
+
+          firstUpdate: null,
+          dataUpdateDates: null,
+          metadataUpdateDates: null,
+
+          firstBeginDate: null,
+          lastEndDate: null,
+          firstPossibleDate: "1800-01-01T00:00:00Z",
+          temporalCoverage: 0,
+          queryCoverageFrom: null,
+          queryCoverageUntil: null,
+
+          metadataTotalSize: null,
+          dataTotalSize: null,
+          totalSize: null,
+
+          hideMetadataAssessment: false,
+          mdqScoresImage: null,
+          mdqImageId: null,
+
+          //HTTP GET requests are typically limited to 2,083 characters. So query lengths
+          // should have this maximum before switching over to HTTP POST
+          maxQueryLength: 2000,
+        };
+      },
+
+      /**
+       * This function serves as a shorthand way to get all of the statistics stored in the model
+       */
+      getAll: function () {
+        // Only get the MetaDIG scores if MetacatUI is configured to display metadata assesments
+        // AND this model has them enabled, too.
+        if (
+          !MetacatUI.appModel.get("hideSummaryMetadataAssessment") &&
+          !this.get("hideMetadataAssessment")
+        ) {
+          this.getMdqScores();
+        }
 
-    /**
-    * Queries for statistics about metadata objects
-    */
-    getMetadataStats: function(){
+        //Send the call the get both the metadata and data stats
+        this.getMetadataStats();
+        this.getDataStats();
+      },
 
-      var query = this.get("query"),
+      /**
+       * Queries for statistics about metadata objects
+       */
+      getMetadataStats: function () {
+        var query = this.get("query"),
           //Filter out the portal and collection documents
-          filterQuery = "-formatId:*dataone.org/collections* AND -formatId:*dataone.org/portals* AND formatType:METADATA AND -obsoletedBy:*",
+          filterQuery =
+            "-formatId:*dataone.org/collections* AND -formatId:*dataone.org/portals* AND formatType:METADATA AND -obsoletedBy:*",
           //Use the stats feature to get the sum of the file size
           stats = "true",
           statsField = "size",
@@ -170,7 +174,7 @@ 

Source: src/js/models/Stats.js

facetRange = "dateUploaded", facetRangeGap = "+1MONTH", facetRangeStart = "1900-01-01T00:00:00.000Z", - facetRangeEnd = (new Date()).toISOString(), + facetRangeEnd = new Date().toISOString(), facetMissing = "true", //Query for the temporal coverage ranges facetQueries = [], @@ -183,325 +187,458 @@

Source: src/js/models/Stats.js

//Use JSON for the response format wt = "json"; - //How many years back should we look for temporal coverage? - var lastYear = this.get('queryCoverageUntil') || new Date().getUTCFullYear(), - firstYear = this.get('queryCoverageFrom') || 1950, + //How many years back should we look for temporal coverage? + var lastYear = + this.get("queryCoverageUntil") || new Date().getUTCFullYear(), + firstYear = this.get("queryCoverageFrom") || 1950, totalYears = lastYear - firstYear, today = new Date().getUTCFullYear(), yearsFromToday = { - fromBeginning: today - firstYear, - fromEnd: today - lastYear + fromBeginning: today - firstYear, + fromEnd: today - lastYear, }; - //Determine our year range/bin size - var binSize = 1; - - if((totalYears > 10) && (totalYears <= 20)){ - binSize = 2; - } - else if((totalYears > 20) && (totalYears <= 50)){ - binSize = 5; - } - else if((totalYears > 50) && (totalYears <= 100)){ - binSize = 10; - } - else if(totalYears > 100){ - binSize = 25; - } - - //Count all the datasets with coverage before the first year in the year range queries - var beginDateLimit = new Date(Date.UTC(firstYear-1, 11, 31, 23, 59, 59, 999)); - facetQueries.push("{!key=<" + firstYear + "}(beginDate:[* TO " + - beginDateLimit.toISOString() +"/YEAR])"); - - //Construct our facet.queries for the beginDate and endDates, starting with all years before this current year - var key = ""; - - for(var yearsAgo = yearsFromToday.fromBeginning; (yearsAgo >= yearsFromToday.fromEnd && yearsAgo > 0); yearsAgo -= binSize){ - // The query logic here is: If the beginnning year is anytime before or - // during the last year of the bin AND the ending year is anytime after - // or during the first year of the bin, it counts. - if(binSize == 1){ - //Querying for just the current year needs to be treated a bit differently - // and won't be caught in our for loop - if(lastYear == today){ - var oneYearFromNow = new Date(Date.UTC(today+1, 0, 1)); - var now = new Date(); + //Determine our year range/bin size + var binSize = 1; + + if (totalYears > 10 && totalYears <= 20) { + binSize = 2; + } else if (totalYears > 20 && totalYears <= 50) { + binSize = 5; + } else if (totalYears > 50 && totalYears <= 100) { + binSize = 10; + } else if (totalYears > 100) { + binSize = 25; + } - facetQueries.push("{!key=" + lastYear + "}(beginDate:[* TO " + - oneYearFromNow.toISOString() + "/YEAR] AND endDate:[" + - now.toISOString() + "/YEAR TO *])"); + //Count all the datasets with coverage before the first year in the year range queries + var beginDateLimit = new Date( + Date.UTC(firstYear - 1, 11, 31, 23, 59, 59, 999), + ); + facetQueries.push( + "{!key=<" + + firstYear + + "}(beginDate:[* TO " + + beginDateLimit.toISOString() + + "/YEAR])", + ); + + //Construct our facet.queries for the beginDate and endDates, starting with all years before this current year + var key = ""; + + for ( + var yearsAgo = yearsFromToday.fromBeginning; + yearsAgo >= yearsFromToday.fromEnd && yearsAgo > 0; + yearsAgo -= binSize + ) { + // The query logic here is: If the beginnning year is anytime before or + // during the last year of the bin AND the ending year is anytime after + // or during the first year of the bin, it counts. + if (binSize == 1) { + //Querying for just the current year needs to be treated a bit differently + // and won't be caught in our for loop + if (lastYear == today) { + var oneYearFromNow = new Date(Date.UTC(today + 1, 0, 1)); + var now = new Date(); + + facetQueries.push( + "{!key=" + + lastYear + + "}(beginDate:[* TO " + + oneYearFromNow.toISOString() + + "/YEAR] AND endDate:[" + + now.toISOString() + + "/YEAR TO *])", + ); + } else { + key = today - yearsAgo; + + //The coverage should start sometime in this year range or earlier. + var beginDateLimit = new Date( + Date.UTC(today - (yearsAgo - 1), 0, 1), + ); + //The coverage should end sometime in this year range or later. + var endDateLimit = new Date(Date.UTC(today - yearsAgo, 0, 1)); + + facetQueries.push( + "{!key=" + + key + + "}(beginDate:[* TO " + + beginDateLimit.toISOString() + + "/YEAR] AND endDate:[" + + endDateLimit.toISOString() + + "/YEAR TO *])", + ); + } } - else{ - key = today - yearsAgo; + //If this is the last date range + else if (yearsAgo <= binSize) { + //Get the last year that will be included in this bin + var firstYearInBin = today - yearsAgo, + lastYearInBin = lastYear; + + //Label the facet query with a key for easier parsing + // Because this is the last year range, which could be uneven with the other year ranges, use the exact end year + key = firstYearInBin + "-" + lastYearInBin; //The coverage should start sometime in this year range or earlier. - var beginDateLimit = new Date(Date.UTC(today-(yearsAgo-1), 0, 1)); + // Because this is the last year range, which could be uneven with the other year ranges, use the exact end year + var beginDateLimit = new Date( + Date.UTC(lastYearInBin, 11, 31, 23, 59, 59, 999), + ); //The coverage should end sometime in this year range or later. - var endDateLimit = new Date(Date.UTC(today-yearsAgo, 0, 1)); + var endDateLimit = new Date(Date.UTC(firstYearInBin, 0, 1)); + + facetQueries.push( + "{!key=" + + key + + "}(beginDate:[* TO " + + beginDateLimit.toISOString() + + "/YEAR] AND endDate:[" + + endDateLimit.toISOString() + + "/YEAR TO *])", + ); + } + //For all other bins, + else { + //Get the last year that will be included in this bin + var firstYearInBin = today - yearsAgo, + lastYearInBin = today - yearsAgo + binSize - 1; + + //Label the facet query with a key for easier parsing + key = firstYearInBin + "-" + lastYearInBin; - facetQueries.push("{!key=" + key + "}(beginDate:[* TO " + - beginDateLimit.toISOString() + "/YEAR] AND endDate:[" + - endDateLimit.toISOString() + "/YEAR TO *])"); + //The coverage should start sometime in this year range or earlier. + // var beginDateLimit = new Date(Date.UTC(today - (yearsAgo - binSize), 0, 1)); + var beginDateLimit = new Date( + Date.UTC(lastYearInBin, 11, 31, 23, 59, 59, 999), + ); + //The coverage should end sometime in this year range or later. + var endDateLimit = new Date(Date.UTC(firstYearInBin, 0, 1)); + + facetQueries.push( + "{!key=" + + key + + "}(beginDate:[* TO " + + beginDateLimit.toISOString() + + "/YEAR] AND endDate:[" + + endDateLimit.toISOString() + + "/YEAR TO *])", + ); } } - //If this is the last date range - else if (yearsAgo <= binSize){ - //Get the last year that will be included in this bin - var firstYearInBin = (today - yearsAgo), - lastYearInBin = lastYear; - - //Label the facet query with a key for easier parsing - // Because this is the last year range, which could be uneven with the other year ranges, use the exact end year - key = firstYearInBin + "-" + lastYearInBin; - - //The coverage should start sometime in this year range or earlier. - // Because this is the last year range, which could be uneven with the other year ranges, use the exact end year - var beginDateLimit = new Date(Date.UTC(lastYearInBin, 11, 31, 23, 59, 59, 999)); - //The coverage should end sometime in this year range or later. - var endDateLimit = new Date(Date.UTC(firstYearInBin, 0, 1)); - - facetQueries.push("{!key=" + key + "}(beginDate:[* TO " + - beginDateLimit.toISOString() +"/YEAR] AND endDate:[" + - endDateLimit.toISOString() + "/YEAR TO *])"); - } - //For all other bins, - else{ - //Get the last year that will be included in this bin - var firstYearInBin = (today - yearsAgo), - lastYearInBin = (today - yearsAgo + binSize-1); - - //Label the facet query with a key for easier parsing - key = firstYearInBin + "-" + lastYearInBin; - - //The coverage should start sometime in this year range or earlier. - // var beginDateLimit = new Date(Date.UTC(today - (yearsAgo - binSize), 0, 1)); - var beginDateLimit = new Date(Date.UTC(lastYearInBin, 11, 31, 23, 59, 59, 999)); - //The coverage should end sometime in this year range or later. - var endDateLimit = new Date(Date.UTC(firstYearInBin, 0, 1)); - - facetQueries.push("{!key=" + key + "}(beginDate:[* TO " + - beginDateLimit.toISOString() + "/YEAR] AND endDate:[" + - endDateLimit.toISOString() + "/YEAR TO *])"); - } - } - - var model = this; - var successCallback = function(data, textStatus, xhr) { - - if( !data || !data.response || !data.response.numFound ){ - //Store falsey data - model.set("totalCount", 0); - model.trigger("change:totalCount"); - model.set('metadataCount', 0); - model.trigger("change:metadataCount"); - model.set('metadataFormatIDs', ["", 0]); - model.set('firstUpdate', null); - model.set("metadataUpdateDates", []); - model.set("temporalCoverage", 0); - model.trigger("change:temporalCoverage"); - } - else{ - //Save tthe number of metadata docs found - model.set('metadataCount', data.response.numFound); - model.set("totalCount", model.get("dataCount") + data.response.numFound); - - //Save the format ID facet counts - if( data.facet_counts && data.facet_counts.facet_fields && data.facet_counts.facet_fields.formatId ){ - model.set("metadataFormatIDs", data.facet_counts.facet_fields.formatId); - } - else{ - model.set("metadataFormatIDs", ["", 0]); - } - //Save the metadata update date counts - if( data.facet_counts && data.facet_counts.facet_ranges && data.facet_counts.facet_ranges.dateUploaded ){ + var model = this; + var successCallback = function (data, textStatus, xhr) { + if (!data || !data.response || !data.response.numFound) { + //Store falsey data + model.set("totalCount", 0); + model.trigger("change:totalCount"); + model.set("metadataCount", 0); + model.trigger("change:metadataCount"); + model.set("metadataFormatIDs", ["", 0]); + model.set("firstUpdate", null); + model.set("metadataUpdateDates", []); + model.set("temporalCoverage", 0); + model.trigger("change:temporalCoverage"); + } else { + //Save tthe number of metadata docs found + model.set("metadataCount", data.response.numFound); + model.set( + "totalCount", + model.get("dataCount") + data.response.numFound, + ); + + //Save the format ID facet counts + if ( + data.facet_counts && + data.facet_counts.facet_fields && + data.facet_counts.facet_fields.formatId + ) { + model.set( + "metadataFormatIDs", + data.facet_counts.facet_fields.formatId, + ); + } else { + model.set("metadataFormatIDs", ["", 0]); + } - //Find the index of the first update date - var updateFacets = data.facet_counts.facet_ranges.dateUploaded.counts, + //Save the metadata update date counts + if ( + data.facet_counts && + data.facet_counts.facet_ranges && + data.facet_counts.facet_ranges.dateUploaded + ) { + //Find the index of the first update date + var updateFacets = + data.facet_counts.facet_ranges.dateUploaded.counts, cropAt = 0; - for( var i=1; i<updateFacets.length; i+=2 ){ - //If there was at least one update/upload in this date range, then save this as the first update - if( typeof updateFacets[i] == "number" && updateFacets[i] > 0){ - //Save the first first update date - cropAt = i; - model.set('firstUpdate', updateFacets[i-1]); - //Save the update dates, but crop out months that are empty - model.set("metadataUpdateDates", updateFacets.slice(cropAt+1)); - i = updateFacets.length; + for (var i = 1; i < updateFacets.length; i += 2) { + //If there was at least one update/upload in this date range, then save this as the first update + if (typeof updateFacets[i] == "number" && updateFacets[i] > 0) { + //Save the first first update date + cropAt = i; + model.set("firstUpdate", updateFacets[i - 1]); + //Save the update dates, but crop out months that are empty + model.set( + "metadataUpdateDates", + updateFacets.slice(cropAt + 1), + ); + i = updateFacets.length; + } } - } - //If no update dates were found, save falsey values - if( cropAt === 0 ){ - model.set('firstUpdate', null); - model.set("metadataUpdateDates", []); + //If no update dates were found, save falsey values + if (cropAt === 0) { + model.set("firstUpdate", null); + model.set("metadataUpdateDates", []); + } } - } - - //Save the temporal coverage dates - if( data.facet_counts && data.facet_counts.facet_queries ){ - //Find the beginDate and facets so we can store the earliest beginDate - if( data.facet_counts.facet_fields && data.facet_counts.facet_fields.beginDate ){ - var earliestBeginDate = _.find(data.facet_counts.facet_fields.beginDate, function(value){ - return ( typeof value == "string" && parseInt(value.substring(0,4)) > 1000 ); - }); - if( earliestBeginDate ){ - model.set("firstBeginDate", earliestBeginDate); + //Save the temporal coverage dates + if (data.facet_counts && data.facet_counts.facet_queries) { + //Find the beginDate and facets so we can store the earliest beginDate + if ( + data.facet_counts.facet_fields && + data.facet_counts.facet_fields.beginDate + ) { + var earliestBeginDate = _.find( + data.facet_counts.facet_fields.beginDate, + function (value) { + return ( + typeof value == "string" && + parseInt(value.substring(0, 4)) > 1000 + ); + }, + ); + if (earliestBeginDate) { + model.set("firstBeginDate", earliestBeginDate); + } } - } - //Find the endDate and facets so we can store the latest endDate - if( data.facet_counts.facet_fields && data.facet_counts.facet_fields.endDate ){ - var latestEndDate, + //Find the endDate and facets so we can store the latest endDate + if ( + data.facet_counts.facet_fields && + data.facet_counts.facet_fields.endDate + ) { + var latestEndDate, endDates = data.facet_counts.facet_fields.endDate, - nextYear = (new Date()).getUTCFullYear() + 1, + nextYear = new Date().getUTCFullYear() + 1, i = 0; - //Iterate over each endDate and find the first valid one. (After year 1000 but not after today) - while( !latestEndDate && i<endDates.length ){ - var endDate = endDates[i]; - if( typeof endDate == "string" ){ - endDate = parseInt(endDate.substring(0,3)); - if( endDate > 1000 && endDate < nextYear){ - latestEndDate = endDate; + //Iterate over each endDate and find the first valid one. (After year 1000 but not after today) + while (!latestEndDate && i < endDates.length) { + var endDate = endDates[i]; + if (typeof endDate == "string") { + endDate = parseInt(endDate.substring(0, 3)); + if (endDate > 1000 && endDate < nextYear) { + latestEndDate = endDate; + } } + i++; } - i++; - } - //Save the latest endDate if one was found - if( latestEndDate ){ - model.set("lastEndDate", latestEndDate); + //Save the latest endDate if one was found + if (latestEndDate) { + model.set("lastEndDate", latestEndDate); + } } - } - //Save the temporal coverage year ranges - var tempCoverages = data.facet_counts.facet_queries; - model.set("temporalCoverage", tempCoverages); - } + //Save the temporal coverage year ranges + var tempCoverages = data.facet_counts.facet_queries; + model.set("temporalCoverage", tempCoverages); + } - //Get the total size of all the files in the index - if( data.stats && data.stats.stats_fields && data.stats.stats_fields.size && data.stats.stats_fields.size.sum ){ - //Save the size sum - model.set("metadataTotalSize", data.stats.stats_fields.size.sum); - //If there is a data size sum, - if( typeof model.get("dataTotalSize") == "number" ){ - //Add it to the metadata size sum as the total sum - model.set("totalSize", model.get("dataTotalSize") + data.stats.stats_fields.size.sum); + //Get the total size of all the files in the index + if ( + data.stats && + data.stats.stats_fields && + data.stats.stats_fields.size && + data.stats.stats_fields.size.sum + ) { + //Save the size sum + model.set("metadataTotalSize", data.stats.stats_fields.size.sum); + //If there is a data size sum, + if (typeof model.get("dataTotalSize") == "number") { + //Add it to the metadata size sum as the total sum + model.set( + "totalSize", + model.get("dataTotalSize") + data.stats.stats_fields.size.sum, + ); + } } } - } - } - - //Construct the full URL for the query - var fullQueryURL = MetacatUI.appModel.get('queryServiceUrl') + - "q=" + query + - "&fq=" + filterQuery + - "&stats=" + stats + - "&stats.field=" + statsField + - "&facet=" + facet + - "&facet.field=" + facetFormatIdField + - "&facet.field=" + facetBeginDateField + - "&facet.field=" + facetEndDateField + - "&f." + facetFormatIdField + ".facet.mincount=" + facetFormatIdMin + - "&f." + facetFormatIdField + ".facet.missing=" + facetFormatIdMissing + - "&f." + facetBeginDateField + ".facet.mincount=" + facetDateMin + - "&f." + facetEndDateField + ".facet.mincount=" + facetDateMin + - "&f." + facetBeginDateField + ".facet.missing=" + facetDateMissing + - "&f." + facetEndDateField + ".facet.missing=" + facetDateMissing + - "&facet.limit=" + facetLimit + - "&f." + facetRange + ".facet.missing=" + facetMissing + - "&facet.range=" + facetRange + - "&facet.range.start=" + facetRangeStart + - "&facet.range.end=" + facetRangeEnd + - "&facet.range.gap=" + encodeURIComponent(facetRangeGap) + - "&facet.query=" + facetQueries.join("&facet.query=") + - "&rows=" + rows + - "&wt=" + wt; - - if( this.getRequestType(fullQueryURL) == "POST" ){ - - if( this.get("postQuery") ){ - query = this.get("postQuery"); - } - else if( this.get("searchModel") ){ - query = this.get("searchModel").getQuery(undefined, { forPOST: true }); - this.set("postQuery", query); - } + }; + + //Construct the full URL for the query + var fullQueryURL = + MetacatUI.appModel.get("queryServiceUrl") + + "q=" + + query + + "&fq=" + + filterQuery + + "&stats=" + + stats + + "&stats.field=" + + statsField + + "&facet=" + + facet + + "&facet.field=" + + facetFormatIdField + + "&facet.field=" + + facetBeginDateField + + "&facet.field=" + + facetEndDateField + + "&f." + + facetFormatIdField + + ".facet.mincount=" + + facetFormatIdMin + + "&f." + + facetFormatIdField + + ".facet.missing=" + + facetFormatIdMissing + + "&f." + + facetBeginDateField + + ".facet.mincount=" + + facetDateMin + + "&f." + + facetEndDateField + + ".facet.mincount=" + + facetDateMin + + "&f." + + facetBeginDateField + + ".facet.missing=" + + facetDateMissing + + "&f." + + facetEndDateField + + ".facet.missing=" + + facetDateMissing + + "&facet.limit=" + + facetLimit + + "&f." + + facetRange + + ".facet.missing=" + + facetMissing + + "&facet.range=" + + facetRange + + "&facet.range.start=" + + facetRangeStart + + "&facet.range.end=" + + facetRangeEnd + + "&facet.range.gap=" + + encodeURIComponent(facetRangeGap) + + "&facet.query=" + + facetQueries.join("&facet.query=") + + "&rows=" + + rows + + "&wt=" + + wt; + + if (this.getRequestType(fullQueryURL) == "POST") { + if (this.get("postQuery")) { + query = this.get("postQuery"); + } else if (this.get("searchModel")) { + query = this.get("searchModel").getQuery(undefined, { + forPOST: true, + }); + this.set("postQuery", query); + } - var queryData = new FormData(); - queryData.append("q", decodeURIComponent(query)); - queryData.append("fq", filterQuery); - queryData.append("stats", stats); - queryData.append("stats.field", statsField); - queryData.append("facet", facet); - queryData.append("facet.field", facetFormatIdField); - queryData.append("facet.field", facetBeginDateField); - queryData.append("facet.field", facetEndDateField); - queryData.append("f." + facetFormatIdField + ".facet.mincount", facetFormatIdMin); - queryData.append("f." + facetFormatIdField + ".facet.missing", facetFormatIdMissing); - queryData.append("f." + facetBeginDateField + ".facet.mincount", facetDateMin); - queryData.append("f." + facetEndDateField + ".facet.mincount", facetDateMin); - queryData.append("f." + facetBeginDateField + ".facet.missing", facetDateMissing); - queryData.append("f." + facetEndDateField + ".facet.missing", facetDateMissing); - queryData.append("facet.limit", facetLimit); - queryData.append("facet.range", facetRange); - queryData.append("facet.range.start", facetRangeStart); - queryData.append("facet.range.end", facetRangeEnd); - queryData.append("facet.range.gap", facetRangeGap); - queryData.append("f." + facetRange + ".facet.missing", facetMissing); - queryData.append("rows", rows); - queryData.append("wt", wt); - - //Add the facet queries to the POST body - _.each(facetQueries, function(facetQuery){ - queryData.append("facet.query", facetQuery); - }); + var queryData = new FormData(); + queryData.append("q", decodeURIComponent(query)); + queryData.append("fq", filterQuery); + queryData.append("stats", stats); + queryData.append("stats.field", statsField); + queryData.append("facet", facet); + queryData.append("facet.field", facetFormatIdField); + queryData.append("facet.field", facetBeginDateField); + queryData.append("facet.field", facetEndDateField); + queryData.append( + "f." + facetFormatIdField + ".facet.mincount", + facetFormatIdMin, + ); + queryData.append( + "f." + facetFormatIdField + ".facet.missing", + facetFormatIdMissing, + ); + queryData.append( + "f." + facetBeginDateField + ".facet.mincount", + facetDateMin, + ); + queryData.append( + "f." + facetEndDateField + ".facet.mincount", + facetDateMin, + ); + queryData.append( + "f." + facetBeginDateField + ".facet.missing", + facetDateMissing, + ); + queryData.append( + "f." + facetEndDateField + ".facet.missing", + facetDateMissing, + ); + queryData.append("facet.limit", facetLimit); + queryData.append("facet.range", facetRange); + queryData.append("facet.range.start", facetRangeStart); + queryData.append("facet.range.end", facetRangeEnd); + queryData.append("facet.range.gap", facetRangeGap); + queryData.append("f." + facetRange + ".facet.missing", facetMissing); + queryData.append("rows", rows); + queryData.append("wt", wt); + + //Add the facet queries to the POST body + _.each(facetQueries, function (facetQuery) { + queryData.append("facet.query", facetQuery); + }); - //Create the request settings for POST requests - var requestSettings = { - url: MetacatUI.appModel.get('queryServiceUrl'), - type: "POST", - contentType: false, - processData: false, - data: queryData, - dataType: "json", - success: successCallback - } - } - else{ - //Create the request settings for GET requests - var requestSettings = { - url: fullQueryURL, - type: "GET", - dataType: "json", - success: successCallback + //Create the request settings for POST requests + var requestSettings = { + url: MetacatUI.appModel.get("queryServiceUrl"), + type: "POST", + contentType: false, + processData: false, + data: queryData, + dataType: "json", + success: successCallback, + }; + } else { + //Create the request settings for GET requests + var requestSettings = { + url: fullQueryURL, + type: "GET", + dataType: "json", + success: successCallback, + }; } - } - - //Send the request - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); - }, + //Send the request + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + }, + + /** + * Queries for statistics about data objects + */ + getDataStats: function () { + //Get the query string from this model + var query = this.get("query") || ""; + //If there is a query set on the model, do a join on the resourceMap field + if ( + query.trim() !== "*:*" && + query.trim().length > 0 && + !this.get("isSystemMetadataQuery") && + MetacatUI.appModel.get("enableSolrJoins") + ) { + query = "{!join from=resourceMap to=resourceMap}" + query; + } - /** - * Queries for statistics about data objects - */ - getDataStats: function(){ - - //Get the query string from this model - var query = this.get("query") || ""; - //If there is a query set on the model, do a join on the resourceMap field - if((query.trim() !== "*:*" && query.trim().length > 0 && !this.get("isSystemMetadataQuery")) - && MetacatUI.appModel.get("enableSolrJoins")){ - query = "{!join from=resourceMap to=resourceMap}" + query; - } - - //Filter out resource maps and metatdata objects - var filterQuery = "formatType:DATA AND -obsoletedBy:*", + //Filter out resource maps and metatdata objects + var filterQuery = "formatType:DATA AND -obsoletedBy:*", //Use the stats feature to get the sum of the file size stats = "true", statsField = "size", @@ -515,266 +652,335 @@

Source: src/js/models/Stats.js

facetRange = "dateUploaded", facetRangeGap = "+1MONTH", facetRangeStart = "1900-01-01T00:00:00.000Z", - facetRangeEnd = (new Date()).toISOString(), + facetRangeEnd = new Date().toISOString(), facetRangeMissing = "true", //Don't return any result docs rows = "0", //Use JSON for the response format wt = "json"; - var fullQueryURL = MetacatUI.appModel.get('queryServiceUrl') + - "q=" + query + - "&fq=" + filterQuery + - "&stats=" + stats + - "&stats.field=" + statsField + - "&facet=" + facet + - "&facet.field=" + facetField + - "&facet.limit=" + facetLimit + - "&f." + facetField + ".facet.mincount=" + facetFormatIdMin + - "&f." + facetField + ".facet.missing=" + facetFormatIdMissing + - "&f." + facetRange + ".facet.missing=" + facetRangeMissing + - "&facet.range=" + facetRange + - "&facet.range.start=" + facetRangeStart + - "&facet.range.end=" + facetRangeEnd + - "&facet.range.gap=" + encodeURIComponent(facetRangeGap) + - "&rows=" + rows + - "&wt=" + wt; - - var model = this; - var successCallback = function(data, textStatus, xhr) { - - if( !data || !data.response || !data.response.numFound ){ - //Store falsey data - model.set('dataCount', 0); - model.trigger("change:dataCount"); - model.set('dataFormatIDs', ["", 0]); - model.set("dataUpdateDates", []); - model.set("dataTotalSize", 0); - - if( typeof model.get("metadataTotalSize") == "number" ){ - //Use the metadata total size as the total size - model.set("totalSize", model.get("metadataTotalSize")); - } - } - else{ - //Save the number of data docs found - model.set('dataCount', data.response.numFound); - model.set("totalCount", model.get("metadataCount") + data.response.numFound); - - //Save the format ID facet counts - if( data.facet_counts && data.facet_counts.facet_fields && data.facet_counts.facet_fields.formatId ){ - model.set("dataFormatIDs", data.facet_counts.facet_fields.formatId); - } - else{ + var fullQueryURL = + MetacatUI.appModel.get("queryServiceUrl") + + "q=" + + query + + "&fq=" + + filterQuery + + "&stats=" + + stats + + "&stats.field=" + + statsField + + "&facet=" + + facet + + "&facet.field=" + + facetField + + "&facet.limit=" + + facetLimit + + "&f." + + facetField + + ".facet.mincount=" + + facetFormatIdMin + + "&f." + + facetField + + ".facet.missing=" + + facetFormatIdMissing + + "&f." + + facetRange + + ".facet.missing=" + + facetRangeMissing + + "&facet.range=" + + facetRange + + "&facet.range.start=" + + facetRangeStart + + "&facet.range.end=" + + facetRangeEnd + + "&facet.range.gap=" + + encodeURIComponent(facetRangeGap) + + "&rows=" + + rows + + "&wt=" + + wt; + + var model = this; + var successCallback = function (data, textStatus, xhr) { + if (!data || !data.response || !data.response.numFound) { + //Store falsey data + model.set("dataCount", 0); + model.trigger("change:dataCount"); model.set("dataFormatIDs", ["", 0]); - } + model.set("dataUpdateDates", []); + model.set("dataTotalSize", 0); - //Save the data update date counts - if( data.facet_counts && data.facet_counts.facet_ranges && data.facet_counts.facet_ranges.dateUploaded ){ + if (typeof model.get("metadataTotalSize") == "number") { + //Use the metadata total size as the total size + model.set("totalSize", model.get("metadataTotalSize")); + } + } else { + //Save the number of data docs found + model.set("dataCount", data.response.numFound); + model.set( + "totalCount", + model.get("metadataCount") + data.response.numFound, + ); + + //Save the format ID facet counts + if ( + data.facet_counts && + data.facet_counts.facet_fields && + data.facet_counts.facet_fields.formatId + ) { + model.set( + "dataFormatIDs", + data.facet_counts.facet_fields.formatId, + ); + } else { + model.set("dataFormatIDs", ["", 0]); + } - //Find the index of the first update date - var updateFacets = data.facet_counts.facet_ranges.dateUploaded.counts, + //Save the data update date counts + if ( + data.facet_counts && + data.facet_counts.facet_ranges && + data.facet_counts.facet_ranges.dateUploaded + ) { + //Find the index of the first update date + var updateFacets = + data.facet_counts.facet_ranges.dateUploaded.counts, cropAt = 0; - for( var i=1; i<updateFacets.length; i+=2 ){ - //If there was at least one update/upload in this date range, then save this as the first update - if( typeof updateFacets[i] == "number" && updateFacets[i] > 0){ - //Save the first first update date - cropAt = i; - model.set('firstUpdate', updateFacets[i-1]); - //Save the update dates, but crop out months that are empty - model.set("dataUpdateDates", updateFacets.slice(cropAt+1)); - i = updateFacets.length; + for (var i = 1; i < updateFacets.length; i += 2) { + //If there was at least one update/upload in this date range, then save this as the first update + if (typeof updateFacets[i] == "number" && updateFacets[i] > 0) { + //Save the first first update date + cropAt = i; + model.set("firstUpdate", updateFacets[i - 1]); + //Save the update dates, but crop out months that are empty + model.set("dataUpdateDates", updateFacets.slice(cropAt + 1)); + i = updateFacets.length; + } } - } - //If no update dates were found, save falsey values - if( cropAt === 0 ){ - model.set('firstUpdate', null); - model.set("dataUpdateDates", []); + //If no update dates were found, save falsey values + if (cropAt === 0) { + model.set("firstUpdate", null); + model.set("dataUpdateDates", []); + } } - } - //Get the total size of all the files in the index - if( data.stats && data.stats.stats_fields && data.stats.stats_fields.size && data.stats.stats_fields.size.sum ){ - //Save the size sum - model.set("dataTotalSize", data.stats.stats_fields.size.sum); - //If there is a metadata size sum, - if( model.get("metadataTotalSize") > 0 ){ - //Add it to the data size sum as the total sum - model.set("totalSize", model.get("metadataTotalSize") + data.stats.stats_fields.size.sum); + //Get the total size of all the files in the index + if ( + data.stats && + data.stats.stats_fields && + data.stats.stats_fields.size && + data.stats.stats_fields.size.sum + ) { + //Save the size sum + model.set("dataTotalSize", data.stats.stats_fields.size.sum); + //If there is a metadata size sum, + if (model.get("metadataTotalSize") > 0) { + //Add it to the data size sum as the total sum + model.set( + "totalSize", + model.get("metadataTotalSize") + + data.stats.stats_fields.size.sum, + ); + } } } - } - } - - if( this.getRequestType(fullQueryURL) == "POST" ){ - - if( this.get("postQuery") ){ - query = this.get("postQuery"); - } - else if( this.get("searchModel") ){ - query = this.get("searchModel").getQuery(undefined, { forPOST: true }); - this.set("postQuery", query); - } - - var queryData = new FormData(); - queryData.append("q", decodeURIComponent(query)); - queryData.append("fq", filterQuery); - queryData.append("stats", stats); - queryData.append("stats.field", statsField); - queryData.append("facet", facet); - queryData.append("facet.field", facetField); - queryData.append("facet.limit", facetLimit); - queryData.append("f." + facetField + ".facet.mincount", facetFormatIdMin); - queryData.append("f." + facetField + ".facet.missing", facetFormatIdMissing); - queryData.append("f." + facetRange + ".facet.missing", facetRangeMissing); - queryData.append("facet.range", facetRange); - queryData.append("facet.range.start", facetRangeStart); - queryData.append("facet.range.end", facetRangeEnd); - queryData.append("facet.range.gap", facetRangeGap); - queryData.append("rows", rows); - queryData.append("wt", wt); - - //Create the request settings for POST requests - var requestSettings = { - url: MetacatUI.appModel.get('queryServiceUrl'), - type: "POST", - contentType: false, - processData: false, - data: queryData, - dataType: "json", - success: successCallback - } - } - else{ - //Create the request settings for GET requests - var requestSettings = { - url: fullQueryURL, - type: "GET", - dataType: "json", - success: successCallback - } - } - - //Send the request - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); - - }, - - /** - * Retrieves an image of the metadata assessment scores - */ - getMdqScores: function(){ - try{ - var myImage = new Image(); - var model = this; - myImage.crossOrigin = ""; // or "anonymous" - - // Call the function with the URL we want to load, but then chain the - // promise then() method on to the end of it. This contains two callbacks - var serviceUrl = MetacatUI.appModel.get('mdqScoresServiceUrl'); + }; + + if (this.getRequestType(fullQueryURL) == "POST") { + if (this.get("postQuery")) { + query = this.get("postQuery"); + } else if (this.get("searchModel")) { + query = this.get("searchModel").getQuery(undefined, { + forPOST: true, + }); + this.set("postQuery", query); + } - if( !serviceUrl ){ - this.set("mdqScoresImage", this.defaults().mdqScoresImage); - this.trigger("change:mdqScoresImage"); - return; + var queryData = new FormData(); + queryData.append("q", decodeURIComponent(query)); + queryData.append("fq", filterQuery); + queryData.append("stats", stats); + queryData.append("stats.field", statsField); + queryData.append("facet", facet); + queryData.append("facet.field", facetField); + queryData.append("facet.limit", facetLimit); + queryData.append( + "f." + facetField + ".facet.mincount", + facetFormatIdMin, + ); + queryData.append( + "f." + facetField + ".facet.missing", + facetFormatIdMissing, + ); + queryData.append( + "f." + facetRange + ".facet.missing", + facetRangeMissing, + ); + queryData.append("facet.range", facetRange); + queryData.append("facet.range.start", facetRangeStart); + queryData.append("facet.range.end", facetRangeEnd); + queryData.append("facet.range.gap", facetRangeGap); + queryData.append("rows", rows); + queryData.append("wt", wt); + + //Create the request settings for POST requests + var requestSettings = { + url: MetacatUI.appModel.get("queryServiceUrl"), + type: "POST", + contentType: false, + processData: false, + data: queryData, + dataType: "json", + success: successCallback, + }; + } else { + //Create the request settings for GET requests + var requestSettings = { + url: fullQueryURL, + type: "GET", + dataType: "json", + success: successCallback, + }; } - if( Array.isArray(MetacatUI.appModel.get('mdqAggregatedSuiteIds')) && MetacatUI.appModel.get('mdqAggregatedSuiteIds').length ){ - var suite = MetacatUI.appModel.get('mdqAggregatedSuiteIds')[0]; - - var id; - - if( this.get("mdqImageId") && typeof this.get("mdqImageId") == "string" ){ - id = this.get("mdqImageId"); - } - else if( MetacatUI.appView.currentView ){ - id = MetacatUI.appView.currentView.model.get("seriesId"); - } - - //If no ID was found, exit without getting the image - if( !id ){ + //Send the request + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + }, + + /** + * Retrieves an image of the metadata assessment scores + */ + getMdqScores: function () { + try { + var myImage = new Image(); + var model = this; + myImage.crossOrigin = ""; // or "anonymous" + + // Call the function with the URL we want to load, but then chain the + // promise then() method on to the end of it. This contains two callbacks + var serviceUrl = MetacatUI.appModel.get("mdqScoresServiceUrl"); + + if (!serviceUrl) { + this.set("mdqScoresImage", this.defaults().mdqScoresImage); + this.trigger("change:mdqScoresImage"); return; } - var url = serviceUrl + "?id=" + id + "&suite=" + suite; + if ( + Array.isArray(MetacatUI.appModel.get("mdqAggregatedSuiteIds")) && + MetacatUI.appModel.get("mdqAggregatedSuiteIds").length + ) { + var suite = MetacatUI.appModel.get("mdqAggregatedSuiteIds")[0]; + + var id; + + if ( + this.get("mdqImageId") && + typeof this.get("mdqImageId") == "string" + ) { + id = this.get("mdqImageId"); + } else if (MetacatUI.appView.currentView) { + id = MetacatUI.appView.currentView.model.get("seriesId"); + } - this.imgLoad(url).then(function (response) { - // The first runs when the promise resolves, with the request.reponse specified within the resolve() method. - var imageURL = window.URL.createObjectURL(response); - myImage.src = imageURL; - model.set('mdqScoresImage', myImage); - // The second runs when the promise is rejected, and logs the Error specified with the reject() method. - }, function (Error) { - console.error(Error); - }); - } - else{ + //If no ID was found, exit without getting the image + if (!id) { + return; + } + + var url = serviceUrl + "?id=" + id + "&suite=" + suite; + + this.imgLoad(url).then( + function (response) { + // The first runs when the promise resolves, with the request.reponse specified within the resolve() method. + var imageURL = window.URL.createObjectURL(response); + myImage.src = imageURL; + model.set("mdqScoresImage", myImage); + // The second runs when the promise is rejected, and logs the Error specified with the reject() method. + }, + function (Error) { + console.error(Error); + }, + ); + } else { + this.set("mdqScoresImage", this.defaults().mdqScoresImage); + } + } catch (e) { this.set("mdqScoresImage", this.defaults().mdqScoresImage); + this.trigger("change:mdqScoresImage"); + console.error("Cannot get the Metadata Assessment scores: ", e); } - } - catch(e){ - this.set("mdqScoresImage", this.defaults().mdqScoresImage); - this.trigger("change:mdqScoresImage"); - console.error("Cannot get the Metadata Assessment scores: ", e); - } - }, + }, - /** - * Retrieves an image via a Promise. Primarily used by {@link Stats#getMdqScores} - * @param {string} url - The URL of the image - */ - imgLoad: function(url) { + /** + * Retrieves an image via a Promise. Primarily used by {@link Stats#getMdqScores} + * @param {string} url - The URL of the image + */ + imgLoad: function (url) { // Create new promise with the Promise() constructor; // This has as its argument a function with two parameters, resolve and reject var model = this; return new Promise(function (resolve, reject) { - // Standard XHR to load an image - var request = new XMLHttpRequest(); - request.open('GET', url); - request.responseType = 'blob'; - - // When the request loads, check whether it was successful - request.onload = function () { - if (request.status === 200) { - // If successful, resolve the promise by passing back the request response - resolve(request.response); - } else { - // If it fails, reject the promise with a error message - reject(new Error('Image didn\'t load successfully; error code:' + request.statusText)); - model.set('mdqScoresError', request.statusText); - } - }; + // Standard XHR to load an image + var request = new XMLHttpRequest(); + request.open("GET", url); + request.responseType = "blob"; + + // When the request loads, check whether it was successful + request.onload = function () { + if (request.status === 200) { + // If successful, resolve the promise by passing back the request response + resolve(request.response); + } else { + // If it fails, reject the promise with a error message + reject( + new Error( + "Image didn't load successfully; error code:" + + request.statusText, + ), + ); + model.set("mdqScoresError", request.statusText); + } + }; - request.onerror = function () { - console.log("onerror"); - // Also deal with the case when the entire request fails to begin with - // This is probably a network error, so reject the promise with an appropriate message - reject(new Error('There was a network error.')); - }; + request.onerror = function () { + console.log("onerror"); + // Also deal with the case when the entire request fails to begin with + // This is probably a network error, so reject the promise with an appropriate message + reject(new Error("There was a network error.")); + }; - // Send the request - request.send(); + // Send the request + request.send(); }); - }, + }, - /** - * Sends a Solr query to get the earliest beginDate. If there are no beginDates in the index, then it - * searches for the earliest endDate. - */ - getFirstBeginDate: function(){ - var model = this; - - //Define a success callback when the query is successful - var successCallback = function(data, textStatus, xhr) { - - //If nothing was found... - if( !data || !data.response || !data.response.numFound ){ + /** + * Sends a Solr query to get the earliest beginDate. If there are no beginDates in the index, then it + * searches for the earliest endDate. + */ + getFirstBeginDate: function () { + var model = this; - //Construct a query to find the earliest endDate - var query = model.get('query') + - " AND endDate:[" + model.get("firstPossibleDate") + " TO " + (new Date()).toISOString() + "]" + //Use date filter to weed out badly formatted data + //Define a success callback when the query is successful + var successCallback = function (data, textStatus, xhr) { + //If nothing was found... + if (!data || !data.response || !data.response.numFound) { + //Construct a query to find the earliest endDate + var query = + model.get("query") + + " AND endDate:[" + + model.get("firstPossibleDate") + + " TO " + + new Date().toISOString() + + "]" + //Use date filter to weed out badly formatted data " AND -obsoletedBy:*", //Get one row only rows = "1", @@ -783,59 +989,83 @@

Source: src/js/models/Stats.js

//Return only the endDate field fl = "endDate"; - var successCallback = function(endDateData, textStatus, xhr) { - //If not endDates or beginDates are found, there is no temporal data in the index, so save falsey values - if( !endDateData || !endDateData.response || !endDateData.response.numFound){ - model.set('firstBeginDate', null); - model.set('lastEndDate', null); - } - else{ - model.set('firstBeginDate', new Date(endDateData.response.docs[0].endDate)); - } - } + var successCallback = function (endDateData, textStatus, xhr) { + //If not endDates or beginDates are found, there is no temporal data in the index, so save falsey values + if ( + !endDateData || + !endDateData.response || + !endDateData.response.numFound + ) { + model.set("firstBeginDate", null); + model.set("lastEndDate", null); + } else { + model.set( + "firstBeginDate", + new Date(endDateData.response.docs[0].endDate), + ); + } + }; - if( model.get("usePOST") ){ - - var queryData = new FormData(); - queryData.append("q", decodeURIComponent(query)); - queryData.append("rows", rows); - queryData.append("sort", sort); - queryData.append("fl", fl); - queryData.append("wt", "json"); - - var requestSettings = { - url: MetacatUI.appModel.get('queryServiceUrl'), - type: "POST", - contentType: false, - processData: false, - data: queryData, - dataType: "json", - success: successCallback + if (model.get("usePOST")) { + var queryData = new FormData(); + queryData.append("q", decodeURIComponent(query)); + queryData.append("rows", rows); + queryData.append("sort", sort); + queryData.append("fl", fl); + queryData.append("wt", "json"); + + var requestSettings = { + url: MetacatUI.appModel.get("queryServiceUrl"), + type: "POST", + contentType: false, + processData: false, + data: queryData, + dataType: "json", + success: successCallback, + }; + } else { + //Find the earliest endDate if there are no beginDates + var requestSettings = { + url: + MetacatUI.appModel.get("queryServiceUrl") + + "q=" + + query + + "&rows=" + + rows + + "&sort=" + + sort + + "&fl=" + + fl + + "&wt=json", + type: "GET", + dataType: "json", + success: successCallback, + }; } + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + } else { + // Save the earliest beginDate + model.set( + "firstBeginDate", + new Date(data.response.docs[0].beginDate), + ); + model.trigger("change:firstBeginDate"); } - else{ - //Find the earliest endDate if there are no beginDates - var requestSettings = { - url: MetacatUI.appModel.get('queryServiceUrl') + "q=" + query + - "&rows=" + rows + "&sort=" + sort + "&fl=" + fl + "&wt=json", - type: "GET", - dataType: "json", - success: successCallback - } - } - - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); - } - else{ - // Save the earliest beginDate - model.set('firstBeginDate', new Date(data.response.docs[0].beginDate)); - model.trigger("change:firstBeginDate"); - } - } - - //Construct a query - var specialQueryParams = " AND beginDate:[" + this.get("firstPossibleDate") + " TO " + (new Date()).toISOString() + "] AND -obsoletedBy:* AND -formatId:*dataone.org/collections* AND -formatId:*dataone.org/portals*", + }; + + //Construct a query + var specialQueryParams = + " AND beginDate:[" + + this.get("firstPossibleDate") + + " TO " + + new Date().toISOString() + + "] AND -obsoletedBy:* AND -formatId:*dataone.org/collections* AND -formatId:*dataone.org/portals*", query = this.get("query") + specialQueryParams, //Get one row only rows = "1", @@ -844,227 +1074,264 @@

Source: src/js/models/Stats.js

//Return only the beginDate field fl = "beginDate"; - if( this.get("usePOST") ){ - - //Get the unencoded query string - if( this.get("postQuery") ){ - query = this.get("postQuery") + specialQueryParams; - } - else if( this.get("searchModel") ){ - query = this.get("searchModel").getQuery(undefined, { forPOST: true }); - this.set("postQuery", query); - query = query + specialQueryParams; - } - - var queryData = new FormData(); - queryData.append("q", decodeURIComponent(query)); - queryData.append("rows", rows); - queryData.append("sort", sort); - queryData.append("fl", fl); - queryData.append("wt", "json"); + if (this.get("usePOST")) { + //Get the unencoded query string + if (this.get("postQuery")) { + query = this.get("postQuery") + specialQueryParams; + } else if (this.get("searchModel")) { + query = this.get("searchModel").getQuery(undefined, { + forPOST: true, + }); + this.set("postQuery", query); + query = query + specialQueryParams; + } - var requestSettings = { - url: MetacatUI.appModel.get('queryServiceUrl'), - type: "POST", - contentType: false, - processData: false, - data: queryData, - dataType: "json", - success: successCallback + var queryData = new FormData(); + queryData.append("q", decodeURIComponent(query)); + queryData.append("rows", rows); + queryData.append("sort", sort); + queryData.append("fl", fl); + queryData.append("wt", "json"); + + var requestSettings = { + url: MetacatUI.appModel.get("queryServiceUrl"), + type: "POST", + contentType: false, + processData: false, + data: queryData, + dataType: "json", + success: successCallback, + }; + } else { + var requestSettings = { + url: + MetacatUI.appModel.get("queryServiceUrl") + + "q=" + + query + + "&rows=" + + rows + + "&fl=" + + fl + + "&sort=" + + sort + + "&wt=json", + type: "GET", + dataType: "json", + success: successCallback, + }; } - } - else{ + //Send the query + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + }, + + // Getting total number of replicas for repository profiles + getTotalReplicas: function (memberNodeID) { + var model = this; var requestSettings = { - url: MetacatUI.appModel.get('queryServiceUrl') + "q=" + query + - "&rows=" + rows + - "&fl=" + fl + - "&sort=" + sort + - "&wt=json", - type: "GET", - dataType: "json", - success: successCallback - } - - } - - //Send the query - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); - }, - - // Getting total number of replicas for repository profiles - getTotalReplicas: function(memberNodeID) { - - var model = this; - - var requestSettings = { - url: MetacatUI.appModel.get("queryServiceUrl") + - "q=replicaMN:" + memberNodeID + - " AND -datasource:" + memberNodeID + + url: + MetacatUI.appModel.get("queryServiceUrl") + + "q=replicaMN:" + + memberNodeID + + " AND -datasource:" + + memberNodeID + " AND formatType:METADATA" + " AND -obsoletedBy:*" + " &wt=json&rows=0", type: "GET", dataType: "json", - success: function(data, textStatus, xhr){ - model.set("totalReplicas", data.response.numFound ); + success: function (data, textStatus, xhr) { + model.set("totalReplicas", data.response.numFound); }, - error: function(data, textStatus, xhr){ - model.set("totalReplicas", 0 ); - } - } - - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); - }, - - /** - * Gets the latest endDate from the Solr index - */ - getLastEndDate: function(){ - var model = this; + error: function (data, textStatus, xhr) { + model.set("totalReplicas", 0); + }, + }; + + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + }, + + /** + * Gets the latest endDate from the Solr index + */ + getLastEndDate: function () { + var model = this; - var now = new Date(); + var now = new Date(); - //Get the latest temporal data coverage year - var specialQueryParams = " AND endDate:[" + this.get("firstPossibleDate") + " TO " + now.toISOString() + "]" + //Use date filter to weed out badly formatted data + //Get the latest temporal data coverage year + var specialQueryParams = + " AND endDate:[" + + this.get("firstPossibleDate") + + " TO " + + now.toISOString() + + "]" + //Use date filter to weed out badly formatted data " AND -obsoletedBy:* AND -formatId:*dataone.org/collections* AND -formatId:*dataone.org/portals*", - query = this.get('query') + specialQueryParams, + query = this.get("query") + specialQueryParams, rows = 1, - fl = "endDate", + fl = "endDate", sort = "endDate desc", - wt = "json"; + wt = "json"; - var successCallback = function(data, textStatus, xhr) { - if(typeof data == "string"){ - data = JSON.parse(data); - } + var successCallback = function (data, textStatus, xhr) { + if (typeof data == "string") { + data = JSON.parse(data); + } - if(!data || !data.response || !data.response.numFound){ - //Save some falsey values if none are found - model.set('lastEndDate', null); - } - else{ - // Save the earliest beginDate and total found in our model - but do not accept a year greater than this current year - var now = new Date(); - if(new Date(data.response.docs[0].endDate).getUTCFullYear() > now.getUTCFullYear()){ - model.set('lastEndDate', now); + if (!data || !data.response || !data.response.numFound) { + //Save some falsey values if none are found + model.set("lastEndDate", null); + } else { + // Save the earliest beginDate and total found in our model - but do not accept a year greater than this current year + var now = new Date(); + if ( + new Date(data.response.docs[0].endDate).getUTCFullYear() > + now.getUTCFullYear() + ) { + model.set("lastEndDate", now); + } else { + model.set("lastEndDate", new Date(data.response.docs[0].endDate)); + } + + model.trigger("change:lastEndDate"); } - else{ - model.set('lastEndDate', new Date(data.response.docs[0].endDate)); + }; + + if (this.get("usePOST")) { + //Get the unencoded query string + if (this.get("postQuery")) { + query = this.get("postQuery") + specialQueryParams; + } else if (this.get("searchModel")) { + query = this.get("searchModel").getQuery(undefined, { + forPOST: true, + }); + this.set("postQuery", query); + query = query + specialQueryParams; } - model.trigger("change:lastEndDate"); + var queryData = new FormData(); + queryData.append("q", decodeURIComponent(query)); + queryData.append("rows", rows); + queryData.append("sort", sort); + queryData.append("fl", fl); + queryData.append("wt", "json"); + + var requestSettings = { + url: MetacatUI.appModel.get("queryServiceUrl"), + type: "POST", + contentType: false, + processData: false, + data: queryData, + dataType: "json", + success: successCallback, + }; + } else { + //Query for the latest endDate + var requestSettings = { + url: + MetacatUI.appModel.get("queryServiceUrl") + + "q=" + + query + + "&rows=" + + rows + + "&fl=" + + fl + + "&sort=" + + sort + + "&wt=" + + wt, + type: "GET", + dataType: "json", + success: successCallback, + }; } - } - if( this.get("usePOST") ){ - - //Get the unencoded query string - if( this.get("postQuery") ){ - query = this.get("postQuery") + specialQueryParams; + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + }, + + /** + * Given the query or URL, determine whether this model should send GET or POST + * requests, because of URL length restrictions in browsers. + * @param {string} queryOrURLString - The full query or URL that will be sent to the query service + * @returns {string} The request type to use. Either `GET` or `POST` + */ + getRequestType: function (queryOrURLString) { + //If POSTs to the query service are disabled completely, use GET + if (MetacatUI.appModel.get("disableQueryPOSTs")) { + return "GET"; } - else if( this.get("searchModel") ){ - query = this.get("searchModel").getQuery(undefined, { forPOST: true }); - this.set("postQuery", query); - query = query + specialQueryParams; + //If POSTs are enabled and the URL is over the maximum, use POST + else if ( + queryOrURLString && + queryOrURLString.length > this.get("maxQueryLength") + ) { + return "POST"; } - - var queryData = new FormData(); - queryData.append("q", decodeURIComponent(query)); - queryData.append("rows", rows); - queryData.append("sort", sort); - queryData.append("fl", fl); - queryData.append("wt", "json"); - - var requestSettings = { - url: MetacatUI.appModel.get('queryServiceUrl'), - type: "POST", - contentType: false, - processData: false, - data: queryData, - dataType: "json", - success: successCallback + //Otherwise, default to GET + else { + return "GET"; } - } - else{ - //Query for the latest endDate - var requestSettings = { - url: MetacatUI.appModel.get('queryServiceUrl') + "q=" + query + - "&rows=" + rows + "&fl=" + fl + "&sort=" + sort + "&wt=" + wt, - type: "GET", - dataType: "json", - success: successCallback - } - } - - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); - }, - - /** - * Given the query or URL, determine whether this model should send GET or POST - * requests, because of URL length restrictions in browsers. - * @param {string} queryOrURLString - The full query or URL that will be sent to the query service - * @returns {string} The request type to use. Either `GET` or `POST` - */ - getRequestType: function(queryOrURLString){ - //If POSTs to the query service are disabled completely, use GET - if( MetacatUI.appModel.get("disableQueryPOSTs") ){ - return "GET"; - } - //If POSTs are enabled and the URL is over the maximum, use POST - else if( queryOrURLString && queryOrURLString.length > this.get("maxQueryLength") ){ - return "POST"; - } - //Otherwise, default to GET - else{ - return "GET"; - } - }, - - /** - * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} and {@link Stats#getDataStats} to get the formatTypes. - * This function may be removed in a future release. - */ - getFormatTypes: function(){ - this.getMetadataStats(); - this.getDataStats(); + }, + + /** + * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} and {@link Stats#getDataStats} to get the formatTypes. + * This function may be removed in a future release. + */ + getFormatTypes: function () { + this.getMetadataStats(); + this.getDataStats(); + }, + + /** + * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getDataStats} to get the formatTypes. + * This function may be removed in a future release. + */ + getDataFormatIDs: function () { + this.getDataStats(); + }, + + /** + * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} to get the formatTypes. + * This function may be removed in a future release. + */ + getMetadataFormatIDs: function () { + this.getMetadataStats(); + }, + + /** + * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} and {@link Stats#getDataStats} to get the formatTypes. + * This function may be removed in a future release. + */ + getUpdateDates: function () { + this.getMetadataStats(); + this.getDataStats(); + }, + + /** + * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} to get the formatTypes. + * This function may be removed in a future release. + */ + getCollectionYearFacets: function () { + this.getMetadataStats(); + }, }, - - /** - * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getDataStats} to get the formatTypes. - * This function may be removed in a future release. - */ - getDataFormatIDs: function(){ - this.getDataStats(); - }, - - /** - * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} to get the formatTypes. - * This function may be removed in a future release. - */ - getMetadataFormatIDs: function(){ - this.getMetadataStats(); - }, - - /** - * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} and {@link Stats#getDataStats} to get the formatTypes. - * This function may be removed in a future release. - */ - getUpdateDates: function(){ - this.getMetadataStats(); - this.getDataStats(); - }, - - /** - * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} to get the formatTypes. - * This function may be removed in a future release. - */ - getCollectionYearFacets: function(){ - this.getMetadataStats(); - } - - }); + ); return Stats; });
diff --git a/docs/docs/src_js_models_UserModel.js.html b/docs/docs/src_js_models_UserModel.js.html index b20b2c215..a713ccb96 100644 --- a/docs/docs/src_js_models_UserModel.js.html +++ b/docs/docs/src_js_models_UserModel.js.html @@ -44,1149 +44,1262 @@

Source: src/js/models/UserModel.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'jws', 'models/Search', "collections/SolrResults"],
-	function($, _, Backbone, JWS, SearchModel, SearchResults) {
-	'use strict';
-
-	/**
-  * @class UserModel
-  * @classcategory Models
-  * @extends Backbone.Model
-  * @constructor
-  */
-	var UserModel = Backbone.Model.extend(
-    /** @lends UserModel.prototype */{
-		defaults: function(){
-			return{
-				type: "person", //assume this is a person unless we are told otherwise - other possible type is a "group"
-				checked: false, //Is set to true when we have checked the account/subject info of this user
-        tokenChecked: false, //Is set to true when the uer auth token has been checked
-				basicUser: false, //Set to true to only query for basic info about this user - prevents sending queries for info that will never be displayed in the UI
-				lastName: null,
-				firstName: null,
-				fullName: null,
-				email: null,
-				logo: null,
-				description: null,
-				verified: null,
-				username: null,
-				usernameReadable: null,
-				orcid: null,
-				searchModel: null,
-				searchResults: null,
-				loggedIn: false,
-				ldapError: false, //Was there an error logging in to LDAP
-				registered: false,
-				isMemberOf: [],
-				isOwnerOf: [],
-				identities: [],
-				identitiesUsernames: [],
-        allIdentitiesAndGroups: [],
-				pending: [],
-				token: null,
-				expires: null,
-				timeoutId: null,
-				rawData: null,
-        portalQuota: -1,
-        isAuthorizedCreatePortal: null,
-        dataoneQuotas: null,
-        dataoneSubscription: null
-			}
-		},
-
-		initialize: function(options){
-			if(typeof options !== "undefined"){
-				if(options.username) this.set("username", options.username);
-				if(options.rawData)  this.set(this.parseXML(options.rawData));
-			}
-
-			this.on("change:identities", this.pluckIdentityUsernames);
-
-			this.on("change:username change:identities change:type", this.updateSearchModel);
-			this.createSearchModel();
-
-			this.on("change:username", this.createReadableUsername());
-
-			//Create a search results model for this person
-			var searchResults = new SearchResults([], { rows: 5, start: 0 });
-			this.set("searchResults", searchResults);
-
-      if( MetacatUI.appModel.get("enableBookkeeperServices") ){
-        //When the user is logged in, see if they have a DataONE subscription
-        this.on("change:loggedIn", this.fetchSubscription);
-      }
-		},
-
-		createSearchModel: function(){
-			//Create a search model that will retrieve data created by this person
-			this.set("searchModel", new SearchModel());
-			this.updateSearchModel();
-		},
-
-		updateSearchModel: function(){
-			if(this.get("type") == "node"){
-				this.get("searchModel").set("dataSource", [this.get("node").identifier]);
-				this.get("searchModel").set("username", []);
-			}
-			else{
-				//Get all the identities for this person
-				var ids = [this.get("username")];
-
-				_.each(this.get("identities"), function(equivalentUser){
-					ids.push(equivalentUser.get("username"));
-				});
-				this.get("searchModel").set("username", ids);
-			}
-
-			this.trigger("change:searchModel");
-		},
-
-		parseXML: function(data){
-			var model = this,
-				username = this.get("username");
-
-			//Reset the group list so we don't just add it to it with push()
-			this.set("isMemberOf", this.defaults().isMemberOf, {silent: true});
-			this.set("isOwnerOf", this.defaults().isOwnerOf, {silent: true});
-			//Reset the equivalent id list so we don't just add it to it with push()
-			this.set("identities", this.defaults().identities, {silent: true});
-
-			//Find this person's node in the XML
-			var userNode = null;
-			if(!username)
-				var username = $(data).children("subject").text();
-			if(username){
-				var subjects = $(data).find("subject");
-				for(var i=0; i<subjects.length; i++){
-					if($(subjects[i]).text().toLowerCase() == username.toLowerCase()){
-						userNode = $(subjects[i]).parent();
-						break;
-					}
-				}
-			}
-			if(!userNode)
-				userNode = $(data).first();
-
-			//Get the type of user - either a person or group
-			var type = $(userNode).prop("tagName");
-			if(type) type = type.toLowerCase();
-
-			if(type == "group"){
-				var fullName = $(userNode).find("groupName").first().text();
-			}
-			else if(type){
-				//Find the person's info
-				var	firstName  = $(userNode).find("givenName").first().text(),
-					lastName   = $(userNode).find("familyName").first().text(),
-					email      = $(userNode).find("email").first().text(),
-					verified   = $(userNode).find("verified").first().text(),
-					memberOf   = this.get("isMemberOf"),
-					ownerOf	   = this.get("isOwnerOf"),
-					identities = this.get("identities"),
-          equivUsernames = [];
-
-				//Sometimes names are saved as "NA" when they are not available - translate these to false values
-				if(firstName == "NA")
-					firstName = null;
-				if(lastName == "NA")
-					lastName = null;
-
-				//Construct the fullname from the first and last names, but watch out for falsely values
-				var fullName = "";
-					fullName += firstName? firstName : "";
-					fullName += lastName? (" " + lastName) : "";
-
-				if(!fullName)
-					fullName = this.getNameFromSubject(username);
-
-				//Don't get this detailed info about basic users
-				if(!this.get("basicUser")){
-					//Get all the equivalent identities for this user
-					var equivalentIds = $(userNode).find("equivalentIdentity");
-					if(equivalentIds.length > 0)
-						var allPersons = $(data).find("person subject");
-
-					_.each(equivalentIds, function(identity, i){
-						//push onto the list
-						var username = $(identity).text(),
-							equivUserNode;
-
-						//Find the matching person node in the response
-						_.each(allPersons, function(person){
-							if($(person).text().toLowerCase() == username.toLowerCase()){
-								equivUserNode = $(person).parent().first();
-								allPersons = _.without(allPersons, person);
-							}
-						});
-
-						var equivalentUser = new UserModel({ username: username, basicUser: true, rawData: equivUserNode });
-						identities.push(equivalentUser);
-            equivUsernames.push(username);
-					});
-				}
-
-				//Get each group and save
-				_.each($(data).find("group"), function(group, i){
-					//Save group ID
-					var groupId = $(group).find("subject").first().text(),
-						groupName = $(group).find("groupName").text();
-
-					memberOf.push({ groupId: groupId, name: groupName });
-
-					//Check if this person is a rightsholder
-					var allRightsHolders = [];
-					_.each($(group).children("rightsHolder"), function(rightsHolder){
-						allRightsHolders.push($(rightsHolder).text().toLowerCase());
-					});
-					if(_.contains(allRightsHolders, username.toLowerCase()))
-						ownerOf.push(groupId);
-				});
-			}
-
-      var allSubjects = _.pluck( this.get("isMemberOf"), "groupId" );
-      allSubjects.push(this.get("username"));
-      allSubjects = allSubjects.concat(equivUsernames);
-
-			return {
-				isMemberOf: memberOf,
-				isOwnerOf: ownerOf,
-				identities: identities,
-        allIdentitiesAndGroups: allSubjects,
-				verified: verified,
-				username: username,
-				firstName: firstName,
-				lastName: lastName,
-				fullName: fullName,
-				email: email,
-				registered: true,
-				type: type,
-				rawData: data
-			}
-		},
-
-		getInfo: function(){
-			var model = this;
-
-			//If the accounts service is not on, flag this user as checked/completed
-			if(!MetacatUI.appModel.get("accountsUrl")){
-				this.set("fullName", this.getNameFromSubject());
-				this.set("checked", true);
-				return;
-			}
-
-			//Only proceed if there is a username
-			if(!this.get("username")) return;
-
-			//Get the user info using the DataONE API
-			var url = MetacatUI.appModel.get("accountsUrl") + encodeURIComponent(this.get("username"));
-
-			var requestSettings = {
-				type: "GET",
-				url: url,
-				success: function(data, textStatus, xhr) {
-					//Parse the XML response to get user info
-					var userProperties = model.parseXML(data);
-					//Filter out all the falsey values
-					_.each(userProperties, function(v, k) {
-				      if(!v) {
-				        delete userProperties[k];
-				      }
-				    });
-					model.set(userProperties);
-
-					 //Trigger the change events
-					model.trigger("change:isMemberOf");
-					model.trigger("change:isOwnerOf");
-					model.trigger("change:identities");
-
-					model.set("checked", true);
-				},
-				error: function(xhr, textStatus, errorThrown){
-					// Sometimes the node info has not been received before this getInfo() is called.
-					// If the node info was received while this getInfo request was pending, and this user was determined
-					// to be a node, then we can skip any further action here.
-					if(model.get("type") == "node")
-						return;
-
-					if((xhr.status == 404) && MetacatUI.nodeModel.get("checked")){
-						model.set("fullName", model.getNameFromSubject());
-						model.set("checked", true);
-					}
-					else if((xhr.status == 404) && !MetacatUI.nodeModel.get("checked")){
-						model.listenToOnce(MetacatUI.nodeModel, "change:checked", function(){
-							if(!model.isNode()){
-								model.set("fullName", model.getNameFromSubject());
-								model.set("checked", true);
-							}
-						});
-					}
-					else{
-						//As a backup, search for this user instead
-						var requestSettings = {
-								type: "GET",
-								url: MetacatUI.appModel.get("accountsUrl") + "?query=" + encodeURIComponent(model.get("username")),
-								success: function(data, textStatus, xhr) {
-									//Parse the XML response to get user info
-									model.set(model.parseXML(data));
-
-									 //Trigger the change events
-									model.trigger("change:isMemberOf");
-									model.trigger("change:isOwnerOf");
-									model.trigger("change:identities");
-
-									model.set("checked", true);
-								},
-								error: function(){
-									//Set some blank values and flag as checked
-									//model.set("username", "");
-									//model.set("fullName", "");
-									model.set("notFound", true);
-									model.set("checked", true);
-								}
-							}
-						//Send the request
-						$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-
-					}
-				}
-			}
-
-			//Send the request
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		//Get the pending identity map requests, if the service is turned on
-		getPendingIdentities: function(){
-			if(!MetacatUI.appModel.get("pendingMapsUrl")) return false;
-
-			var model = this;
-
-			//Get the pending requests
-			var requestSettings = {
-				url: MetacatUI.appModel.get("pendingMapsUrl") + encodeURIComponent(this.get("username")),
-				success: function(data, textStatus, xhr){
-					//Reset the equivalent id list so we don't just add it to it with push()
-					model.set("pending", model.defaults().pending);
-					var pending = model.get("pending");
-					_.each($(data).find("person"), function(person, i) {
-
-						//Don't list yourself as a pending map request
-						var personsUsername = $(person).find("subject").text();
-						if(personsUsername.toLowerCase() == model.get("username").toLowerCase())
-							return;
-
-						//Create a new User Model for this pending identity
-						var pendingUser = new UserModel({ rawData: person });
-
-						if(pendingUser.isOrcid())
-							pendingUser.getInfo();
-
-						pending.push(pendingUser);
-					});
-					model.set("pending", pending);
-					model.trigger("change:pending"); //Trigger the change event
-				},
-				error: function(xhr, textStatus){
-					if(xhr.responseText.indexOf("error code 34")){
-						model.set("pending", model.defaults().pending);
-						model.trigger("change:pending"); //Trigger the change event
-					}
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		getNameFromSubject: function(username){
-			var username  = username || this.get("username"),
-				fullName = "";
-
-			if(!username) return;
-
-			if((username.indexOf("uid=") > -1) && (username.indexOf(",") > -1))
-				fullName = username.substring(username.indexOf("uid=") + 4, username.indexOf(","));
-			else if((username.indexOf("CN=") > -1) && (username.indexOf(",") > -1))
-				fullName = username.substring(username.indexOf("CN=") + 3, username.indexOf(","));
-
-			//Cut off the last string after the name when it contains digits - not part of this person's names
-			if(fullName.lastIndexOf(" ") > fullName.indexOf(" ")){
-				var lastWord = fullName.substring(fullName.lastIndexOf(" "));
-				if(lastWord.search(/\d/) > -1)
-					fullName = fullName.substring(0, fullName.lastIndexOf(" "));
-			}
-
-			//Default to the username
-			if(!fullName) fullName = this.get("fullname") || username;
-
-			return fullName;
-		},
-
-		isOrcid: function(orcid){
-			var username = (typeof orcid === "string")? orcid : this.get("username");
-
-			//Have we already verified this?
-			if((typeof orcid == "undefined") && (username == this.get("orcid"))) return true;
-
-			//Checks for ORCIDs using the orcid base URL as a prefix
-			if(username.indexOf("orcid.org/") > -1){
-				return true;
-			}
-
-			//If the ORCID base url is not present, we will check if this is a 19-digit ORCID ID
-			//A simple and fast check first
-			//ORCiDs are 16 digits and 3 dashes - 19 characters
-			if(username.length != 19) return false;
-
-			/* The ORCID checksum algorithm to determine is a character string is an ORCiD
-			 * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
-			 */
-			var total = 0,
-				baseDigits = username.replace(/-/g, "").substr(0, 15);
-
-			for(var i=0; i<baseDigits.length; i++){
-				var digit = parseInt(baseDigits.charAt(i));
-				total = (total + digit) * 2;
-			}
-
-			var remainder = total % 11,
-				result = (12 - remainder) % 11,
-				checkDigit = (result == 10) ? "X" : result.toString(),
-				isOrcid = (checkDigit == username.charAt(username.length-1));
-
-			if(isOrcid)
-				this.set("orcid", username);
-
-			return isOrcid;
-		},
-
-		isNode: function(){
-			var model = this;
-			var node = _.find(MetacatUI.nodeModel.get("members"), function(nodeModel) {
-				return nodeModel.shortIdentifier.toLowerCase() == (model.get("username")).toLowerCase();
-			  });
-
-			return (node && (node !== undefined))
-		},
-
-		// Will check if this user is a Member Node. If so, it will save the MN info to the model
-		saveAsNode: function(){
-			if(!this.isNode()) return;
-
-			var model = this;
-			var node = _.find(MetacatUI.nodeModel.get("members"), function(nodeModel) {
-				return nodeModel.shortIdentifier.toLowerCase() == (model.get("username")).toLowerCase();
-			  });
-
-			this.set({
-				type: "node",
-				logo: node.logo,
-				description: node.description,
-				node: node,
-				fullName: node.name,
-				usernameReadable: this.get("username")
-			});
-			this.updateSearchModel();
-			this.set("checked", true);
-		},
-
-		loginLdap: function(formData, success, error){
-			if(!formData || !appModel.get("signInUrlLdap")) return false;
-
-			var model = this;
-
-			var requestSettings = {
-				type: "POST",
-				url: MetacatUI.appModel.get("signInUrlLdap") + window.location.href,
-				data: formData,
-				success: function(data, textStatus, xhr){
-					if(success)
-						success(this);
-
-					model.getToken();
-
-				},
-				error: function(){
-					/*if(error)
-						error(this);
-					*/
-					model.getToken();
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		logout: function(){
-
-			//Construct the sign out url and redirect
-			var signOutUrl = MetacatUI.appModel.get('signOutUrl'),
-				  target = Backbone.history.location.href;
-
-			// DO NOT include the route otherwise we have an infinite redirect
-			// target  = target.split("#")[0];
-			target = target.slice(0, -8);
-
-			// make sure to include the target
-			signOutUrl += "?target=" + target;
-
-			// do it!
-			window.location.replace(signOutUrl);
-		},
-
-		// call Metacat or the DataONE CN to validate the session and tell us the user's name
-		checkStatus: function(onSuccess, onError) {
-			var model = this;
-
-			if (!MetacatUI.appModel.get("tokenUrl")) {
-				// look up the URL
-				var metacatUrl = MetacatUI.appModel.get('metacatServiceUrl');
-
-				// ajax call to validate the session/get the user info
-				var requestSettings = {
-					type: "POST",
-					url: metacatUrl,
-					data: { action: "validatesession" },
-					success: function(data, textStatus, xhr) {
-						// the Metacat (XML) response should have a fullName element
-						var username = $(data).find("name").text();
-
-						// set in the model
-						model.set('username', username);
-
-						//Are we logged in?
-						if(username){
-							model.set("loggedIn", true);
-							model.getInfo();
-						}
-						else{
-							model.set("loggedIn", false);
-							model.trigger("change:loggedIn");
-							model.set("checked", true);
-						}
-
-						if(onSuccess) onSuccess(data);
-
-					},
-					error: function(data, textStatus, xhr){
-						//User is not logged in
-						model.reset();
-
-						if(onError) onError();
-					}
-				}
-
-				$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-			} else {
-				// use the token method for checking authentication
-				this.getToken();
-			}
-		},
-
-		getToken: function(customCallback) {
-			var tokenUrl = MetacatUI.appModel.get('tokenUrl');
-			var model = this;
-
-			if(!tokenUrl) return false;
-
-			//Set up the function that will be called when we retrieve a token
-			var callback = (typeof customCallback === "function") ? customCallback : function(data, textStatus, xhr) {
-
-				// the response should have the token
-				var payload = model.parseToken(data),
-					username = payload ? payload.userId : null,
-					fullName = payload ? payload.fullName : model.getNameFromSubject(username) || null,
-					token    = payload ? data : null,
-					loggedIn = payload ? true : false;
-
-				// set in the model
-				model.set('fullName', fullName);
-				model.set('username', username);
-				model.set("token", token);
-				model.set("loggedIn", loggedIn);
-        model.set("tokenChecked", true);
-
-				model.getTokenExpiration(payload);
-
-				if(username)
-					model.getInfo();
-				else
-					model.set("checked", true);
-			};
-
-			// ajax call to get token
-			var requestSettings = {
-				type: "GET",
-				dataType: "text",
-				xhrFields: {
-					withCredentials: true
-				},
-				url: tokenUrl,
-				data: {},
-				success: callback,
-				error: function(xhr, textStatus, errorThrown){
-					model.set("checked", true);
-				}
-			}
-
-			$.ajax(requestSettings);
-		},
-
-		getTokenExpiration: function(payload){
-			if(!payload && this.get("token")) var payload = this.parseToken(this.get("token"));
-			if(!payload) return;
-
-			//The exp claim should be standard - it is in UTC seconds
-			var expires = payload.exp? new Date(payload.exp * 1000) : null;
-
-			//Use the issuedAt and ttl as a backup (only used in d1 2.0.0 and 2.0.1)
-			if(!expires){
-				var issuedAt = payload.issuedAt? new Date(payload.issuedAt) : null,
-					lifeSpan = payload.ttl? payload.ttl : null;
-
-				if(issuedAt && lifeSpan && (lifeSpan > 99999))
-					issuedAt.setMilliseconds(lifeSpan);
-				else if(issuedAt && lifeSpan)
-					issuedAt.setSeconds(lifeSpan);
-
-				expires = issuedAt;
-			}
-
-			this.set("expires", expires);
-		},
-
-		checkToken: function(onSuccess, onError){
-
-			//First check if the token has expired
-			if(MetacatUI.appUserModel.get("expires") > new Date()){
-				if(onSuccess) onSuccess();
-
-				return;
-			}
-
-			var model = this;
-
-			var url = MetacatUI.appModel.get("tokenUrl");
-			if(!url) return;
-
-			var requestSettings = {
-					type: "GET",
-					url: url,
-          headers: {
-			      "Cache-Control": "no-cache"
-				  },
-					success: function(data, textStatus, xhr){
-						if(data){
-							// the response should have the token
-							var payload = model.parseToken(data),
-								username = payload ? payload.userId : null,
-								fullName = payload ? payload.fullName : null,
-								token    = payload ? data : null,
-								loggedIn = payload ? true : false;
-
-							// set in the model
-							model.set('fullName', fullName);
-							model.set('username', username);
-							model.set("token", token);
-							model.set("loggedIn", loggedIn);
-
-							model.getTokenExpiration(payload);
-
-							MetacatUI.appUserModel.set("checked", true);
-
-							if(onSuccess) onSuccess(data, textStatus, xhr);
-						}
-						else if(onError)
-							onError(data, textStatus, xhr);
-					},
-					error: function(data, textStatus, xhr){
-						//If this token in invalid, then reset the user model/log out
-						MetacatUI.appUserModel.reset();
-
-						if(onError) onError(data, textStatus, xhr);
-					}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		parseToken: function(token) {
-			if(typeof token == "undefined")
-				var token = this.get("token");
-
-			var jws = new KJUR.jws.JWS();
-			var result = 0;
-			try {
-				result = jws.parseJWS(token);
-			} catch (ex) {
-			    result = 0;
-			}
-
-			if(!jws.parsedJWS) return "";
-
-			var payload = $.parseJSON(jws.parsedJWS.payloadS);
-			return payload;
-		},
-
-		update: function(onSuccess, onError){
-			var model = this;
-
-			var person =
-				'<?xml version="1.0" encoding="UTF-8"?>'
-				+ '<d1:person xmlns:d1="http://ns.dataone.org/service/types/v1">'
-					+ '<subject>' + this.get("username") + '</subject>'
-					+ '<givenName>' + this.get("firstName") + '</givenName>'
-					+ '<familyName>' + this.get("lastName") + '</familyName>'
-					+ '<email>' + this.get("email") + '</email>'
-				+ '</d1:person>';
-
-			var xmlBlob = new Blob([person], {type : 'application/xml'});
-			var formData = new FormData();
-			formData.append("subject", this.get("username"));
-			formData.append("person", xmlBlob, "person");
-
-			var updateUrl = MetacatUI.appModel.get("accountsUrl") + encodeURIComponent(this.get("username"));
-
-			// ajax call to update
-			var requestSettings = {
-				type: "PUT",
-				cache: false,
-			    contentType: false,
-			    processData: false,
-				url: updateUrl,
-				data: formData,
-				success: function(data, textStatus, xhr) {
-					if(typeof onSuccess != "undefined")
-						onSuccess(data);
-
-					//model.getInfo();
-				},
-				error: function(data, textStatus, xhr) {
-					if(typeof onError != "undefined")
-						onError(data);
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		confirmMapRequest: function(otherUsername, onSuccess, onError){
-			if(!otherUsername) return;
-
-			var mapUrl = MetacatUI.appModel.get("pendingMapsUrl") + encodeURIComponent(otherUsername),
-				model = this;
-
-			if(!onSuccess)
-				var onSuccess = function(){};
-			if(!onError)
-				var onError = function(){};
-
-			// ajax call to confirm map
-			var requestSettings = {
-				type: "PUT",
-				url: mapUrl,
-				success: function(data, textStatus, xhr) {
-					if(onSuccess)
-						onSuccess(data, textStatus, xhr);
-
-					//Get updated info
-					model.getInfo();
-				},
-				error: function(xhr, textStatus, error) {
-					if(onError)
-						onError(xhr, textStatus, error);
-				}
-			}
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		denyMapRequest: function(otherUsername, onSuccess, onError){
-			if(!otherUsername) return;
-
-			var mapUrl = MetacatUI.appModel.get("pendingMapsUrl") + encodeURIComponent(otherUsername),
-				model = this;
-
-			// ajax call to reject map
-			var requestSettings = {
-				type: "DELETE",
-				url: mapUrl,
-				success: function(data, textStatus, xhr) {
-					if(typeof onSuccess == "function")
-						onSuccess(data, textStatus, xhr);
-
-					model.getInfo();
-				},
-				error: function(xhr, textStatus, error) {
-					if(typeof onError == "function")
-						onError(xhr, textStatus, error);
-				}
-			}
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		addMap: function(otherUsername, onSuccess, onError){
-			if(!otherUsername) return;
-
-			var mapUrl = MetacatUI.appModel.get("pendingMapsUrl"),
-				model = this;
-
-			if(mapUrl.charAt(mapUrl.length-1) == "/"){
-				mapUrl = mapUrl.substring(0, mapUrl.length-1)
-			}
-
-			// ajax call to map
-			var requestSettings = {
-				type: "POST",
-				xhrFields: {
-					withCredentials: true
-				},
-				headers: {
-			        "Authorization": "Bearer " + this.get("token")
-			  },
-				url: mapUrl,
-				data: {
-					subject: otherUsername
-				},
-				success: function(data, textStatus, xhr) {
-					if(typeof onSuccess == "function")
-						onSuccess(data, textStatus, xhr);
-
-					model.getInfo();
-				},
-				error: function(xhr, textStatus, error) {
-
-					//Check if the username might have been spelled or formatted incorrectly
-					//ORCIDs, in particular, have different formats that we should account for
-					if(xhr.responseText.indexOf("LDAP: error code 32 - No Such Object") > -1 && model.isOrcid(otherUsername)){
-						if(otherUsername.length == 19)
-							model.addMap("http://orcid.org/" + otherUsername, onSuccess, onError);
-						else if(otherUsername.indexOf("https://orcid.org") == 0)
-							model.addMap(otherUsername.replace("https", "http"), onSuccess, onError);
-						else if(otherUsername.indexOf("orcid.org") == 0)
-							model.addMap("http://" + otherUsername, onSuccess, onError);
-						else if(otherUsername.indexOf("www.orcid.org") == 0)
-								model.addMap(otherUsername.replace("www.", "http://"), onSuccess, onError);
-						else if(otherUsername.indexOf("http://www.orcid.org") == 0)
-								model.addMap(otherUsername.replace("www.", ""), onSuccess, onError);
-						else if(otherUsername.indexOf("https://www.orcid.org") == 0)
-								model.addMap(otherUsername.replace("https://www.", "http://"), onSuccess, onError);
-						else if(typeof onError == "function")
-							onError(xhr, textStatus, error);
-					}
-					else{
-						if(typeof onError == "function")
-							onError(xhr, textStatus, error);
-				  }
-				}
-			}
-
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		removeMap: function(otherUsername, onSuccess, onError){
-			if(!otherUsername) return;
-
-			var mapUrl = MetacatUI.appModel.get("accountsMapsUrl") + encodeURIComponent(otherUsername),
-				model = this;
-
-			// ajax call to remove mapping
-			var requestSettings = {
-				type: "DELETE",
-				url: mapUrl,
-				success: function(data, textStatus, xhr) {
-					if(typeof onSuccess == "function")
-						onSuccess(data, textStatus, xhr);
-
-					model.getInfo();
-				},
-				error: function(xhr, textStatus, error) {
-					if(typeof onError == "function")
-						onError(xhr, textStatus, error);
-				}
-			}
-			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-		},
-
-		failedLdapLogin: function(){
-			this.set("loggedIn", false);
-			this.set("checked", true);
-			this.set("ldapError", true);
-		},
-
-		pluckIdentityUsernames: function(){
-			var models = this.get("identities"),
-				usernames = [];
-
-			_.each(models, function(m){
-				usernames.push(m.get("username").toLowerCase());
-			});
-
-			this.set("identitiesUsernames", usernames);
-			this.trigger("change:identitiesUsernames");
-		},
-
-		createReadableUsername: function(){
-			if(!this.get("username")) return;
-
-			var username = this.get("username"),
-				readableUsername = username.substring(username.indexOf("=")+1, username.indexOf(",")) || username;
-
-			this.set("usernameReadable", readableUsername);
-		},
-
-		createAjaxSettings: function(){
-			if(!this.get("token")) return {}
-
-			return { xhrFields: {
-					withCredentials: true
-					},
-				  headers: {
-			        "Authorization": "Bearer " + this.get("token")
-				  }
-				}
-		},
-
-    /**
-    * Checks if this user has the quota to perform the given action
-    * @param {string} action - The action to be performed
-    * @param {string} customerGroup - The subject or identifier of the customer/membership group
-    * to use this quota against
-    */
-    checkQuota: function(action, customerGroup){
-
-      //Temporarily reset the quota so a trigger event is changed when the XHR is complete
-      this.set("portalQuota", -1, {silent: true});
-
-      //Start of temporary code
-      //TODO: Replace this function with real code once the quota service is working
-      this.set("portalQuota", 999);
-      return;
-      //End of temporary code
-
-    /*  var model = this;
-
-      var requestSettings = {
-        url: "",
-        type: "GET",
-        success: function(data, textStatus, xhr) {
-          model.set("portalQuota", data.remainingQuota);
-        },
-        error: function(xhr, textStatus, errorThrown) {
-          model.set("portalQuota", 0);
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "jws",
+  "models/Search",
+  "collections/SolrResults",
+], function ($, _, Backbone, JWS, SearchModel, SearchResults) {
+  "use strict";
+
+  /**
+   * @class UserModel
+   * @classcategory Models
+   * @extends Backbone.Model
+   * @constructor
+   */
+  var UserModel = Backbone.Model.extend(
+    /** @lends UserModel.prototype */ {
+      defaults: function () {
+        return {
+          type: "person", //assume this is a person unless we are told otherwise - other possible type is a "group"
+          checked: false, //Is set to true when we have checked the account/subject info of this user
+          tokenChecked: false, //Is set to true when the uer auth token has been checked
+          basicUser: false, //Set to true to only query for basic info about this user - prevents sending queries for info that will never be displayed in the UI
+          lastName: null,
+          firstName: null,
+          fullName: null,
+          email: null,
+          logo: null,
+          description: null,
+          verified: null,
+          username: null,
+          usernameReadable: null,
+          orcid: null,
+          searchModel: null,
+          searchResults: null,
+          loggedIn: false,
+          ldapError: false, //Was there an error logging in to LDAP
+          registered: false,
+          isMemberOf: [],
+          isOwnerOf: [],
+          identities: [],
+          identitiesUsernames: [],
+          allIdentitiesAndGroups: [],
+          pending: [],
+          token: null,
+          expires: null,
+          timeoutId: null,
+          rawData: null,
+          portalQuota: -1,
+          isAuthorizedCreatePortal: null,
+          dataoneQuotas: null,
+          dataoneSubscription: null,
+        };
+      },
+
+      initialize: function (options) {
+        if (typeof options !== "undefined") {
+          if (options.username) this.set("username", options.username);
+          if (options.rawData) this.set(this.parseXML(options.rawData));
         }
-      }
 
-      $.ajax(_.extend(requestSettings, this.createAjaxSettings()));
-*/
-    },
+        this.on("change:identities", this.pluckIdentityUsernames);
 
-    /**
-    * Checks if the user has authorization to perform the given action.
-    */
-    isAuthorizedCreatePortal: function(){
+        this.on(
+          "change:username change:identities change:type",
+          this.updateSearchModel,
+        );
+        this.createSearchModel();
 
-      //Reset the isAuthorized attribute silently so a change event is always triggered
-      this.set("isAuthorizedCreatePortal", null, {silent: true});
+        this.on("change:username", this.createReadableUsername());
 
-      //If the user isn't logged in, set authorization to false
-      if( !this.get("loggedIn") ){
-        this.set("isAuthorizedCreatePortal", false);
-        return;
-      }
+        //Create a search results model for this person
+        var searchResults = new SearchResults([], { rows: 5, start: 0 });
+        this.set("searchResults", searchResults);
 
-      //If creating portals has been disabled app-wide, then set to false
-      if( MetacatUI.appModel.get("enableCreatePortals") === false ){
-        this.set("isAuthorizedCreatePortal", false);
-        return;
-      }
-      //If creating portals has been limited to only certain subjects, check if this user is one of them
-      else if( MetacatUI.appModel.get("limitPortalsToSubjects").length ){
-        if( !this.get("allIdentitiesAndGroups").length ){
-          this.on("change:allIdentitiesAndGroups", this.isAuthorizedCreatePortal);
-          return;
+        if (MetacatUI.appModel.get("enableBookkeeperServices")) {
+          //When the user is logged in, see if they have a DataONE subscription
+          this.on("change:loggedIn", this.fetchSubscription);
         }
-
-        //Find the subjects that have access to create portals. Could be specific users or groups.
-        var subjectsThatHaveAccess = _.intersection(MetacatUI.appModel.get("limitPortalsToSubjects"), this.get("allIdentitiesAndGroups"));
-        if( !subjectsThatHaveAccess.length ){
-          //If this user is not in the whitelist, set to false
-          this.set("isAuthorizedCreatePortal", false);
+      },
+
+      createSearchModel: function () {
+        //Create a search model that will retrieve data created by this person
+        this.set("searchModel", new SearchModel());
+        this.updateSearchModel();
+      },
+
+      updateSearchModel: function () {
+        if (this.get("type") == "node") {
+          this.get("searchModel").set("dataSource", [
+            this.get("node").identifier,
+          ]);
+          this.get("searchModel").set("username", []);
+        } else {
+          //Get all the identities for this person
+          var ids = [this.get("username")];
+
+          _.each(this.get("identities"), function (equivalentUser) {
+            ids.push(equivalentUser.get("username"));
+          });
+          this.get("searchModel").set("username", ids);
         }
-        else{
-          //If this user is in the whitelist, set to true
-          this.set("isAuthorizedCreatePortal", true);
+
+        this.trigger("change:searchModel");
+      },
+
+      parseXML: function (data) {
+        var model = this,
+          username = this.get("username");
+
+        //Reset the group list so we don't just add it to it with push()
+        this.set("isMemberOf", this.defaults().isMemberOf, { silent: true });
+        this.set("isOwnerOf", this.defaults().isOwnerOf, { silent: true });
+        //Reset the equivalent id list so we don't just add it to it with push()
+        this.set("identities", this.defaults().identities, { silent: true });
+
+        //Find this person's node in the XML
+        var userNode = null;
+        if (!username) var username = $(data).children("subject").text();
+        if (username) {
+          var subjects = $(data).find("subject");
+          for (var i = 0; i < subjects.length; i++) {
+            if ($(subjects[i]).text().toLowerCase() == username.toLowerCase()) {
+              userNode = $(subjects[i]).parent();
+              break;
+            }
+          }
         }
-        return;
-      }
-      //If anyone is allowed to create a portal, check if they have the quota to create a portal
-      else if( MetacatUI.appModel.get("enableBookkeeperServices") ){
+        if (!userNode) userNode = $(data).first();
+
+        //Get the type of user - either a person or group
+        var type = $(userNode).prop("tagName");
+        if (type) type = type.toLowerCase();
+
+        if (type == "group") {
+          var fullName = $(userNode).find("groupName").first().text();
+        } else if (type) {
+          //Find the person's info
+          var firstName = $(userNode).find("givenName").first().text(),
+            lastName = $(userNode).find("familyName").first().text(),
+            email = $(userNode).find("email").first().text(),
+            verified = $(userNode).find("verified").first().text(),
+            memberOf = this.get("isMemberOf"),
+            ownerOf = this.get("isOwnerOf"),
+            identities = this.get("identities"),
+            equivUsernames = [];
+
+          //Sometimes names are saved as "NA" when they are not available - translate these to false values
+          if (firstName == "NA") firstName = null;
+          if (lastName == "NA") lastName = null;
+
+          //Construct the fullname from the first and last names, but watch out for falsely values
+          var fullName = "";
+          fullName += firstName ? firstName : "";
+          fullName += lastName ? " " + lastName : "";
+
+          if (!fullName) fullName = this.getNameFromSubject(username);
+
+          //Don't get this detailed info about basic users
+          if (!this.get("basicUser")) {
+            //Get all the equivalent identities for this user
+            var equivalentIds = $(userNode).find("equivalentIdentity");
+            if (equivalentIds.length > 0)
+              var allPersons = $(data).find("person subject");
+
+            _.each(equivalentIds, function (identity, i) {
+              //push onto the list
+              var username = $(identity).text(),
+                equivUserNode;
+
+              //Find the matching person node in the response
+              _.each(allPersons, function (person) {
+                if ($(person).text().toLowerCase() == username.toLowerCase()) {
+                  equivUserNode = $(person).parent().first();
+                  allPersons = _.without(allPersons, person);
+                }
+              });
+
+              var equivalentUser = new UserModel({
+                username: username,
+                basicUser: true,
+                rawData: equivUserNode,
+              });
+              identities.push(equivalentUser);
+              equivUsernames.push(username);
+            });
+          }
 
-        //Get the Quotas for this user
-        var quotas = this.get("dataoneQuotas"),
-            portalQuotas;
+          //Get each group and save
+          _.each($(data).find("group"), function (group, i) {
+            //Save group ID
+            var groupId = $(group).find("subject").first().text(),
+              groupName = $(group).find("groupName").text();
+
+            memberOf.push({ groupId: groupId, name: groupName });
 
-        //If the Quotas are still being fetched,
-        if(quotas == this.defaults().dataoneQuotas && !quotas){
-          this.on("change:dataoneQuotas", this.isAuthorizedCreatePortal);
+            //Check if this person is a rightsholder
+            var allRightsHolders = [];
+            _.each($(group).children("rightsHolder"), function (rightsHolder) {
+              allRightsHolders.push($(rightsHolder).text().toLowerCase());
+            });
+            if (_.contains(allRightsHolders, username.toLowerCase()))
+              ownerOf.push(groupId);
+          });
+        }
+
+        var allSubjects = _.pluck(this.get("isMemberOf"), "groupId");
+        allSubjects.push(this.get("username"));
+        allSubjects = allSubjects.concat(equivUsernames);
+
+        return {
+          isMemberOf: memberOf,
+          isOwnerOf: ownerOf,
+          identities: identities,
+          allIdentitiesAndGroups: allSubjects,
+          verified: verified,
+          username: username,
+          firstName: firstName,
+          lastName: lastName,
+          fullName: fullName,
+          email: email,
+          registered: true,
+          type: type,
+          rawData: data,
+        };
+      },
+
+      getInfo: function () {
+        var model = this;
+
+        //If the accounts service is not on, flag this user as checked/completed
+        if (!MetacatUI.appModel.get("accountsUrl")) {
+          this.set("fullName", this.getNameFromSubject());
+          this.set("checked", true);
           return;
         }
-        else{
-          portalQuotas = quotas.where({ quotaType: "portal" });
+
+        //Only proceed if there is a username
+        if (!this.get("username")) return;
+
+        //Get the user info using the DataONE API
+        var url =
+          MetacatUI.appModel.get("accountsUrl") +
+          encodeURIComponent(this.get("username"));
+
+        var requestSettings = {
+          type: "GET",
+          url: url,
+          success: function (data, textStatus, xhr) {
+            //Parse the XML response to get user info
+            var userProperties = model.parseXML(data);
+            //Filter out all the falsey values
+            _.each(userProperties, function (v, k) {
+              if (!v) {
+                delete userProperties[k];
+              }
+            });
+            model.set(userProperties);
+
+            //Trigger the change events
+            model.trigger("change:isMemberOf");
+            model.trigger("change:isOwnerOf");
+            model.trigger("change:identities");
+
+            model.set("checked", true);
+          },
+          error: function (xhr, textStatus, errorThrown) {
+            // Sometimes the node info has not been received before this getInfo() is called.
+            // If the node info was received while this getInfo request was pending, and this user was determined
+            // to be a node, then we can skip any further action here.
+            if (model.get("type") == "node") return;
+
+            if (xhr.status == 404 && MetacatUI.nodeModel.get("checked")) {
+              model.set("fullName", model.getNameFromSubject());
+              model.set("checked", true);
+            } else if (
+              xhr.status == 404 &&
+              !MetacatUI.nodeModel.get("checked")
+            ) {
+              model.listenToOnce(
+                MetacatUI.nodeModel,
+                "change:checked",
+                function () {
+                  if (!model.isNode()) {
+                    model.set("fullName", model.getNameFromSubject());
+                    model.set("checked", true);
+                  }
+                },
+              );
+            } else {
+              //As a backup, search for this user instead
+              var requestSettings = {
+                type: "GET",
+                url:
+                  MetacatUI.appModel.get("accountsUrl") +
+                  "?query=" +
+                  encodeURIComponent(model.get("username")),
+                success: function (data, textStatus, xhr) {
+                  //Parse the XML response to get user info
+                  model.set(model.parseXML(data));
+
+                  //Trigger the change events
+                  model.trigger("change:isMemberOf");
+                  model.trigger("change:isOwnerOf");
+                  model.trigger("change:identities");
+
+                  model.set("checked", true);
+                },
+                error: function () {
+                  //Set some blank values and flag as checked
+                  //model.set("username", "");
+                  //model.set("fullName", "");
+                  model.set("notFound", true);
+                  model.set("checked", true);
+                },
+              };
+              //Send the request
+              $.ajax(
+                _.extend(
+                  requestSettings,
+                  MetacatUI.appUserModel.createAjaxSettings(),
+                ),
+              );
+            }
+          },
+        };
+
+        //Send the request
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      //Get the pending identity map requests, if the service is turned on
+      getPendingIdentities: function () {
+        if (!MetacatUI.appModel.get("pendingMapsUrl")) return false;
+
+        var model = this;
+
+        //Get the pending requests
+        var requestSettings = {
+          url:
+            MetacatUI.appModel.get("pendingMapsUrl") +
+            encodeURIComponent(this.get("username")),
+          success: function (data, textStatus, xhr) {
+            //Reset the equivalent id list so we don't just add it to it with push()
+            model.set("pending", model.defaults().pending);
+            var pending = model.get("pending");
+            _.each($(data).find("person"), function (person, i) {
+              //Don't list yourself as a pending map request
+              var personsUsername = $(person).find("subject").text();
+              if (
+                personsUsername.toLowerCase() ==
+                model.get("username").toLowerCase()
+              )
+                return;
+
+              //Create a new User Model for this pending identity
+              var pendingUser = new UserModel({ rawData: person });
+
+              if (pendingUser.isOrcid()) pendingUser.getInfo();
+
+              pending.push(pendingUser);
+            });
+            model.set("pending", pending);
+            model.trigger("change:pending"); //Trigger the change event
+          },
+          error: function (xhr, textStatus) {
+            if (xhr.responseText.indexOf("error code 34")) {
+              model.set("pending", model.defaults().pending);
+              model.trigger("change:pending"); //Trigger the change event
+            }
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      getNameFromSubject: function (username) {
+        var username = username || this.get("username"),
+          fullName = "";
+
+        if (!username) return;
+
+        if (username.indexOf("uid=") > -1 && username.indexOf(",") > -1)
+          fullName = username.substring(
+            username.indexOf("uid=") + 4,
+            username.indexOf(","),
+          );
+        else if (username.indexOf("CN=") > -1 && username.indexOf(",") > -1)
+          fullName = username.substring(
+            username.indexOf("CN=") + 3,
+            username.indexOf(","),
+          );
+
+        //Cut off the last string after the name when it contains digits - not part of this person's names
+        if (fullName.lastIndexOf(" ") > fullName.indexOf(" ")) {
+          var lastWord = fullName.substring(fullName.lastIndexOf(" "));
+          if (lastWord.search(/\d/) > -1)
+            fullName = fullName.substring(0, fullName.lastIndexOf(" "));
         }
 
-        //If this user has no portal Quota at all, they are not auth to create a portal
-        if( !portalQuotas ){
-          this.set("isAuthorizedCreatePortal", false);
+        //Default to the username
+        if (!fullName) fullName = this.get("fullname") || username;
+
+        return fullName;
+      },
+
+      isOrcid: function (orcid) {
+        var username = typeof orcid === "string" ? orcid : this.get("username");
+
+        //Have we already verified this?
+        if (typeof orcid == "undefined" && username == this.get("orcid"))
+          return true;
+
+        //Checks for ORCIDs using the orcid base URL as a prefix
+        if (username.indexOf("orcid.org/") > -1) {
+          return true;
         }
-        else{
 
-          //Check that there is at least one Quota where the totalUsage < softLimit
-          var hasRemainingUsage = _.some(portalQuotas, function(quota){
-            return quota.get("totalUsage") < quota.get("softLimit");
-          });
+        //If the ORCID base url is not present, we will check if this is a 19-digit ORCID ID
+        //A simple and fast check first
+        //ORCiDs are 16 digits and 3 dashes - 19 characters
+        if (username.length != 19) return false;
 
-          //If there is remaining usage left in at least one Quota, then the user can create a portal
-          if( hasRemainingUsage ){
-            this.set("isAuthorizedCreatePortal", true);
-          }
-          //Otherwise they cannot create a new portal
-          else{
-            this.set("isAuthorizedCreatePortal", false);
-          }
+        /* The ORCID checksum algorithm to determine is a character string is an ORCiD
+         * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
+         */
+        var total = 0,
+          baseDigits = username.replace(/-/g, "").substr(0, 15);
+
+        for (var i = 0; i < baseDigits.length; i++) {
+          var digit = parseInt(baseDigits.charAt(i));
+          total = (total + digit) * 2;
         }
 
-        //@todoGet the admin group and force admins to have at least one quota left
+        var remainder = total % 11,
+          result = (12 - remainder) % 11,
+          checkDigit = result == 10 ? "X" : result.toString(),
+          isOrcid = checkDigit == username.charAt(username.length - 1);
+
+        if (isOrcid) this.set("orcid", username);
+
+        return isOrcid;
+      },
+
+      isNode: function () {
+        var model = this;
+        var node = _.find(
+          MetacatUI.nodeModel.get("members"),
+          function (nodeModel) {
+            return (
+              nodeModel.shortIdentifier.toLowerCase() ==
+              model.get("username").toLowerCase()
+            );
+          },
+        );
+
+        return node && node !== undefined;
+      },
+
+      // Will check if this user is a Member Node. If so, it will save the MN info to the model
+      saveAsNode: function () {
+        if (!this.isNode()) return;
+
+        var model = this;
+        var node = _.find(
+          MetacatUI.nodeModel.get("members"),
+          function (nodeModel) {
+            return (
+              nodeModel.shortIdentifier.toLowerCase() ==
+              model.get("username").toLowerCase()
+            );
+          },
+        );
+
+        this.set({
+          type: "node",
+          logo: node.logo,
+          description: node.description,
+          node: node,
+          fullName: node.name,
+          usernameReadable: this.get("username"),
+        });
+        this.updateSearchModel();
+        this.set("checked", true);
+      },
+
+      loginLdap: function (formData, success, error) {
+        if (!formData || !appModel.get("signInUrlLdap")) return false;
+
+        var model = this;
+
+        var requestSettings = {
+          type: "POST",
+          url: MetacatUI.appModel.get("signInUrlLdap") + window.location.href,
+          data: formData,
+          success: function (data, textStatus, xhr) {
+            if (success) success(this);
+
+            model.getToken();
+          },
+          error: function () {
+            /*if(error)
+						error(this);
+					*/
+            model.getToken();
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      logout: function () {
+        //Construct the sign out url and redirect
+        var signOutUrl = MetacatUI.appModel.get("signOutUrl"),
+          target = Backbone.history.location.href;
+
+        // DO NOT include the route otherwise we have an infinite redirect
+        // target  = target.split("#")[0];
+        target = target.slice(0, -8);
+
+        // make sure to include the target
+        signOutUrl += "?target=" + target;
+
+        // do it!
+        window.location.replace(signOutUrl);
+      },
+
+      // call Metacat or the DataONE CN to validate the session and tell us the user's name
+      checkStatus: function (onSuccess, onError) {
+        var model = this;
+
+        if (!MetacatUI.appModel.get("tokenUrl")) {
+          // look up the URL
+          var metacatUrl = MetacatUI.appModel.get("metacatServiceUrl");
+
+          // ajax call to validate the session/get the user info
+          var requestSettings = {
+            type: "POST",
+            url: metacatUrl,
+            data: { action: "validatesession" },
+            success: function (data, textStatus, xhr) {
+              // the Metacat (XML) response should have a fullName element
+              var username = $(data).find("name").text();
+
+              // set in the model
+              model.set("username", username);
+
+              //Are we logged in?
+              if (username) {
+                model.set("loggedIn", true);
+                model.getInfo();
+              } else {
+                model.set("loggedIn", false);
+                model.trigger("change:loggedIn");
+                model.set("checked", true);
+              }
+
+              if (onSuccess) onSuccess(data);
+            },
+            error: function (data, textStatus, xhr) {
+              //User is not logged in
+              model.reset();
+
+              if (onError) onError();
+            },
+          };
+
+          $.ajax(
+            _.extend(
+              requestSettings,
+              MetacatUI.appUserModel.createAjaxSettings(),
+            ),
+          );
+        } else {
+          // use the token method for checking authentication
+          this.getToken();
+        }
+      },
+
+      getToken: function (customCallback) {
+        var tokenUrl = MetacatUI.appModel.get("tokenUrl");
+        var model = this;
+
+        if (!tokenUrl) return false;
+
+        //Set up the function that will be called when we retrieve a token
+        var callback =
+          typeof customCallback === "function"
+            ? customCallback
+            : function (data, textStatus, xhr) {
+                // the response should have the token
+                var payload = model.parseToken(data),
+                  username = payload ? payload.userId : null,
+                  fullName = payload
+                    ? payload.fullName
+                    : model.getNameFromSubject(username) || null,
+                  token = payload ? data : null,
+                  loggedIn = payload ? true : false;
+
+                // set in the model
+                model.set("fullName", fullName);
+                model.set("username", username);
+                model.set("token", token);
+                model.set("loggedIn", loggedIn);
+                model.set("tokenChecked", true);
+
+                model.getTokenExpiration(payload);
+
+                if (username) model.getInfo();
+                else model.set("checked", true);
+              };
+
+        // ajax call to get token
+        var requestSettings = {
+          type: "GET",
+          dataType: "text",
+          xhrFields: {
+            withCredentials: true,
+          },
+          url: tokenUrl,
+          data: {},
+          success: callback,
+          error: function (xhr, textStatus, errorThrown) {
+            model.set("checked", true);
+          },
+        };
+
+        $.ajax(requestSettings);
+      },
+
+      getTokenExpiration: function (payload) {
+        if (!payload && this.get("token"))
+          var payload = this.parseToken(this.get("token"));
+        if (!payload) return;
+
+        //The exp claim should be standard - it is in UTC seconds
+        var expires = payload.exp ? new Date(payload.exp * 1000) : null;
+
+        //Use the issuedAt and ttl as a backup (only used in d1 2.0.0 and 2.0.1)
+        if (!expires) {
+          var issuedAt = payload.issuedAt ? new Date(payload.issuedAt) : null,
+            lifeSpan = payload.ttl ? payload.ttl : null;
+
+          if (issuedAt && lifeSpan && lifeSpan > 99999)
+            issuedAt.setMilliseconds(lifeSpan);
+          else if (issuedAt && lifeSpan) issuedAt.setSeconds(lifeSpan);
+
+          expires = issuedAt;
+        }
 
-      }
-      else{
-        //Default to letting people create portals
-        this.set("isAuthorizedCreatePortal", true);
-      }
+        this.set("expires", expires);
+      },
 
-    },
+      checkToken: function (onSuccess, onError) {
+        //First check if the token has expired
+        if (MetacatUI.appUserModel.get("expires") > new Date()) {
+          if (onSuccess) onSuccess();
 
-    /**
-    * Given a list of user and/or group subjects, this function checks if this user
-    * has an equivalent identity in that list, or is a member of a group in the list.
-    * A single subject string can be passed instead of an array of subjects.
-    * TODO: This needs to support nested group membership.
-    * @param {string|string[]} subjects
-    * @returns {boolean}
-    */
-    hasIdentityOverlap: function(subjects){
-
-      try{
-        //If only a single subject is given, put it in an array
-        if( typeof subjects == "string" ){
-          subjects = [subjects];
+          return;
         }
-        //If the subjects are not a string or an array, or if it's an empty array, exit this function.
-        else if( !Array.isArray(subjects) || !subjects.length ){
-          return false;
+
+        var model = this;
+
+        var url = MetacatUI.appModel.get("tokenUrl");
+        if (!url) return;
+
+        var requestSettings = {
+          type: "GET",
+          url: url,
+          headers: {
+            "Cache-Control": "no-cache",
+          },
+          success: function (data, textStatus, xhr) {
+            if (data) {
+              // the response should have the token
+              var payload = model.parseToken(data),
+                username = payload ? payload.userId : null,
+                fullName = payload ? payload.fullName : null,
+                token = payload ? data : null,
+                loggedIn = payload ? true : false;
+
+              // set in the model
+              model.set("fullName", fullName);
+              model.set("username", username);
+              model.set("token", token);
+              model.set("loggedIn", loggedIn);
+
+              model.getTokenExpiration(payload);
+
+              MetacatUI.appUserModel.set("checked", true);
+
+              if (onSuccess) onSuccess(data, textStatus, xhr);
+            } else if (onError) onError(data, textStatus, xhr);
+          },
+          error: function (data, textStatus, xhr) {
+            //If this token in invalid, then reset the user model/log out
+            MetacatUI.appUserModel.reset();
+
+            if (onError) onError(data, textStatus, xhr);
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      parseToken: function (token) {
+        if (typeof token == "undefined") var token = this.get("token");
+
+        var jws = new KJUR.jws.JWS();
+        var result = 0;
+        try {
+          result = jws.parseJWS(token);
+        } catch (ex) {
+          result = 0;
         }
 
-        return _.intersection(this.get("allIdentitiesAndGroups"), subjects).length;
-      }
-      catch(e){
-        console.error(e);
-        return false;
-      }
+        if (!jws.parsedJWS) return "";
+
+        var payload = $.parseJSON(jws.parsedJWS.payloadS);
+        return payload;
+      },
+
+      update: function (onSuccess, onError) {
+        var model = this;
+
+        var person =
+          '<?xml version="1.0" encoding="UTF-8"?>' +
+          '<d1:person xmlns:d1="http://ns.dataone.org/service/types/v1">' +
+          "<subject>" +
+          this.get("username") +
+          "</subject>" +
+          "<givenName>" +
+          this.get("firstName") +
+          "</givenName>" +
+          "<familyName>" +
+          this.get("lastName") +
+          "</familyName>" +
+          "<email>" +
+          this.get("email") +
+          "</email>" +
+          "</d1:person>";
+
+        var xmlBlob = new Blob([person], { type: "application/xml" });
+        var formData = new FormData();
+        formData.append("subject", this.get("username"));
+        formData.append("person", xmlBlob, "person");
+
+        var updateUrl =
+          MetacatUI.appModel.get("accountsUrl") +
+          encodeURIComponent(this.get("username"));
+
+        // ajax call to update
+        var requestSettings = {
+          type: "PUT",
+          cache: false,
+          contentType: false,
+          processData: false,
+          url: updateUrl,
+          data: formData,
+          success: function (data, textStatus, xhr) {
+            if (typeof onSuccess != "undefined") onSuccess(data);
+
+            //model.getInfo();
+          },
+          error: function (data, textStatus, xhr) {
+            if (typeof onError != "undefined") onError(data);
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      confirmMapRequest: function (otherUsername, onSuccess, onError) {
+        if (!otherUsername) return;
+
+        var mapUrl =
+            MetacatUI.appModel.get("pendingMapsUrl") +
+            encodeURIComponent(otherUsername),
+          model = this;
+
+        if (!onSuccess) var onSuccess = function () {};
+        if (!onError) var onError = function () {};
+
+        // ajax call to confirm map
+        var requestSettings = {
+          type: "PUT",
+          url: mapUrl,
+          success: function (data, textStatus, xhr) {
+            if (onSuccess) onSuccess(data, textStatus, xhr);
+
+            //Get updated info
+            model.getInfo();
+          },
+          error: function (xhr, textStatus, error) {
+            if (onError) onError(xhr, textStatus, error);
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      denyMapRequest: function (otherUsername, onSuccess, onError) {
+        if (!otherUsername) return;
+
+        var mapUrl =
+            MetacatUI.appModel.get("pendingMapsUrl") +
+            encodeURIComponent(otherUsername),
+          model = this;
+
+        // ajax call to reject map
+        var requestSettings = {
+          type: "DELETE",
+          url: mapUrl,
+          success: function (data, textStatus, xhr) {
+            if (typeof onSuccess == "function")
+              onSuccess(data, textStatus, xhr);
+
+            model.getInfo();
+          },
+          error: function (xhr, textStatus, error) {
+            if (typeof onError == "function") onError(xhr, textStatus, error);
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      addMap: function (otherUsername, onSuccess, onError) {
+        if (!otherUsername) return;
+
+        var mapUrl = MetacatUI.appModel.get("pendingMapsUrl"),
+          model = this;
+
+        if (mapUrl.charAt(mapUrl.length - 1) == "/") {
+          mapUrl = mapUrl.substring(0, mapUrl.length - 1);
+        }
 
-    },
+        // ajax call to map
+        var requestSettings = {
+          type: "POST",
+          xhrFields: {
+            withCredentials: true,
+          },
+          headers: {
+            Authorization: "Bearer " + this.get("token"),
+          },
+          url: mapUrl,
+          data: {
+            subject: otherUsername,
+          },
+          success: function (data, textStatus, xhr) {
+            if (typeof onSuccess == "function")
+              onSuccess(data, textStatus, xhr);
+
+            model.getInfo();
+          },
+          error: function (xhr, textStatus, error) {
+            //Check if the username might have been spelled or formatted incorrectly
+            //ORCIDs, in particular, have different formats that we should account for
+            if (
+              xhr.responseText.indexOf("LDAP: error code 32 - No Such Object") >
+                -1 &&
+              model.isOrcid(otherUsername)
+            ) {
+              if (otherUsername.length == 19)
+                model.addMap(
+                  "http://orcid.org/" + otherUsername,
+                  onSuccess,
+                  onError,
+                );
+              else if (otherUsername.indexOf("https://orcid.org") == 0)
+                model.addMap(
+                  otherUsername.replace("https", "http"),
+                  onSuccess,
+                  onError,
+                );
+              else if (otherUsername.indexOf("orcid.org") == 0)
+                model.addMap("http://" + otherUsername, onSuccess, onError);
+              else if (otherUsername.indexOf("www.orcid.org") == 0)
+                model.addMap(
+                  otherUsername.replace("www.", "http://"),
+                  onSuccess,
+                  onError,
+                );
+              else if (otherUsername.indexOf("http://www.orcid.org") == 0)
+                model.addMap(
+                  otherUsername.replace("www.", ""),
+                  onSuccess,
+                  onError,
+                );
+              else if (otherUsername.indexOf("https://www.orcid.org") == 0)
+                model.addMap(
+                  otherUsername.replace("https://www.", "http://"),
+                  onSuccess,
+                  onError,
+                );
+              else if (typeof onError == "function")
+                onError(xhr, textStatus, error);
+            } else {
+              if (typeof onError == "function") onError(xhr, textStatus, error);
+            }
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      removeMap: function (otherUsername, onSuccess, onError) {
+        if (!otherUsername) return;
+
+        var mapUrl =
+            MetacatUI.appModel.get("accountsMapsUrl") +
+            encodeURIComponent(otherUsername),
+          model = this;
+
+        // ajax call to remove mapping
+        var requestSettings = {
+          type: "DELETE",
+          url: mapUrl,
+          success: function (data, textStatus, xhr) {
+            if (typeof onSuccess == "function")
+              onSuccess(data, textStatus, xhr);
+
+            model.getInfo();
+          },
+          error: function (xhr, textStatus, error) {
+            if (typeof onError == "function") onError(xhr, textStatus, error);
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      failedLdapLogin: function () {
+        this.set("loggedIn", false);
+        this.set("checked", true);
+        this.set("ldapError", true);
+      },
+
+      pluckIdentityUsernames: function () {
+        var models = this.get("identities"),
+          usernames = [];
+
+        _.each(models, function (m) {
+          usernames.push(m.get("username").toLowerCase());
+        });
+
+        this.set("identitiesUsernames", usernames);
+        this.trigger("change:identitiesUsernames");
+      },
+
+      createReadableUsername: function () {
+        if (!this.get("username")) return;
+
+        var username = this.get("username"),
+          readableUsername =
+            username.substring(
+              username.indexOf("=") + 1,
+              username.indexOf(","),
+            ) || username;
+
+        this.set("usernameReadable", readableUsername);
+      },
 
-    /**
-    * Retrieve all the info about this user's DataONE Subscription
-    */
-    fetchSubscription: function(){
+      createAjaxSettings: function () {
+        if (!this.get("token")) return {};
 
-      //If Bookkeeper services are disabled, exit
-      if( !MetacatUI.appModel.get("enableBookkeeperServices") ){
+        return {
+          xhrFields: {
+            withCredentials: true,
+          },
+          headers: {
+            Authorization: "Bearer " + this.get("token"),
+          },
+        };
+      },
+
+      /**
+       * Checks if this user has the quota to perform the given action
+       * @param {string} action - The action to be performed
+       * @param {string} customerGroup - The subject or identifier of the customer/membership group
+       * to use this quota against
+       */
+      checkQuota: function (action, customerGroup) {
+        //Temporarily reset the quota so a trigger event is changed when the XHR is complete
+        this.set("portalQuota", -1, { silent: true });
+
+        //Start of temporary code
+        //TODO: Replace this function with real code once the quota service is working
+        this.set("portalQuota", 999);
         return;
+        //End of temporary code
+
+        /*  var model = this;
+
+      var requestSettings = {
+        url: "",
+        type: "GET",
+        success: function(data, textStatus, xhr) {
+          model.set("portalQuota", data.remainingQuota);
+        },
+        error: function(xhr, textStatus, errorThrown) {
+          model.set("portalQuota", 0);
+        }
       }
 
-      try{
-        var thisUser = this;
-        require(["collections/bookkeeper/Quotas", "models/bookkeeper/Subscription"], function(Quotas, Subscription){
-
-          //Create a Quotas collection
-          var quotas = new Quotas();
-
-          //Create a Subscription model
-          var subscription = new Subscription();
-
-          if( MetacatUI.appModel.get("dataonePlusPreviewMode") ){
-            //Create Quota models for preview mode
-            quotas.add({
-              softLimit: MetacatUI.appModel.get("portalLimit"),
-              hardLimit: MetacatUI.appModel.get("portalLimit"),
-              quotaType: "portal",
-              unit: "portal",
-              subject: thisUser.get("username"),
-              subscription: subscription
-            });
+      $.ajax(_.extend(requestSettings, this.createAjaxSettings()));
+*/
+      },
 
-            //Default to all people being in trial mode
-            subscription.set("status", "trialing");
+      /**
+       * Checks if the user has authorization to perform the given action.
+       */
+      isAuthorizedCreatePortal: function () {
+        //Reset the isAuthorized attribute silently so a change event is always triggered
+        this.set("isAuthorizedCreatePortal", null, { silent: true });
 
-            //Save a reference to the Quotas on this UserModel
-            thisUser.set("dataoneQuotas", quotas);
+        //If the user isn't logged in, set authorization to false
+        if (!this.get("loggedIn")) {
+          this.set("isAuthorizedCreatePortal", false);
+          return;
+        }
 
-            //Save a reference to the Subscriptioin on this UserModel
-            thisUser.set("dataoneSubscription", subscription);
+        //If creating portals has been disabled app-wide, then set to false
+        if (MetacatUI.appModel.get("enableCreatePortals") === false) {
+          this.set("isAuthorizedCreatePortal", false);
+          return;
+        }
+        //If creating portals has been limited to only certain subjects, check if this user is one of them
+        else if (MetacatUI.appModel.get("limitPortalsToSubjects").length) {
+          if (!this.get("allIdentitiesAndGroups").length) {
+            this.on(
+              "change:allIdentitiesAndGroups",
+              this.isAuthorizedCreatePortal,
+            );
+            return;
+          }
 
+          //Find the subjects that have access to create portals. Could be specific users or groups.
+          var subjectsThatHaveAccess = _.intersection(
+            MetacatUI.appModel.get("limitPortalsToSubjects"),
+            this.get("allIdentitiesAndGroups"),
+          );
+          if (!subjectsThatHaveAccess.length) {
+            //If this user is not in the whitelist, set to false
+            this.set("isAuthorizedCreatePortal", false);
+          } else {
+            //If this user is in the whitelist, set to true
+            this.set("isAuthorizedCreatePortal", true);
           }
-          else{
-            thisUser.listenToOnce(quotas, "reset", function(){
-              //Save a reference to the Quotas on this UserModel
-              thisUser.set("dataoneQuotas", quotas);
-            });
+          return;
+        }
+        //If anyone is allowed to create a portal, check if they have the quota to create a portal
+        else if (MetacatUI.appModel.get("enableBookkeeperServices")) {
+          //Get the Quotas for this user
+          var quotas = this.get("dataoneQuotas"),
+            portalQuotas;
 
-            thisUser.listenToOnce(subscription, "sync", function(){
-              //Save a reference to the Subscriptioin on this UserModel
-              thisUser.set("dataoneSubscription", subscription);
+          //If the Quotas are still being fetched,
+          if (quotas == this.defaults().dataoneQuotas && !quotas) {
+            this.on("change:dataoneQuotas", this.isAuthorizedCreatePortal);
+            return;
+          } else {
+            portalQuotas = quotas.where({ quotaType: "portal" });
+          }
+
+          //If this user has no portal Quota at all, they are not auth to create a portal
+          if (!portalQuotas) {
+            this.set("isAuthorizedCreatePortal", false);
+          } else {
+            //Check that there is at least one Quota where the totalUsage < softLimit
+            var hasRemainingUsage = _.some(portalQuotas, function (quota) {
+              return quota.get("totalUsage") < quota.get("softLimit");
             });
 
-            //Fetch the Quotas
-            quotas.fetch({ subscriber: thisUser.get("username") });
+            //If there is remaining usage left in at least one Quota, then the user can create a portal
+            if (hasRemainingUsage) {
+              this.set("isAuthorizedCreatePortal", true);
+            }
+            //Otherwise they cannot create a new portal
+            else {
+              this.set("isAuthorizedCreatePortal", false);
+            }
+          }
 
-            //Fetch the Subscriptioin
-            subscription.fetch();
+          //@todoGet the admin group and force admins to have at least one quota left
+        } else {
+          //Default to letting people create portals
+          this.set("isAuthorizedCreatePortal", true);
+        }
+      },
+
+      /**
+       * Given a list of user and/or group subjects, this function checks if this user
+       * has an equivalent identity in that list, or is a member of a group in the list.
+       * A single subject string can be passed instead of an array of subjects.
+       * TODO: This needs to support nested group membership.
+       * @param {string|string[]} subjects
+       * @returns {boolean}
+       */
+      hasIdentityOverlap: function (subjects) {
+        try {
+          //If only a single subject is given, put it in an array
+          if (typeof subjects == "string") {
+            subjects = [subjects];
+          }
+          //If the subjects are not a string or an array, or if it's an empty array, exit this function.
+          else if (!Array.isArray(subjects) || !subjects.length) {
+            return false;
           }
 
-        });
-      }
-      catch(e){
-        console.error("Couldn't get DataONE Subscription info. Proceeding as an unsubscribed user. ", e);
-      }
+          return _.intersection(this.get("allIdentitiesAndGroups"), subjects)
+            .length;
+        } catch (e) {
+          console.error(e);
+          return false;
+        }
+      },
+
+      /**
+       * Retrieve all the info about this user's DataONE Subscription
+       */
+      fetchSubscription: function () {
+        //If Bookkeeper services are disabled, exit
+        if (!MetacatUI.appModel.get("enableBookkeeperServices")) {
+          return;
+        }
 
-    },
+        try {
+          var thisUser = this;
+          require([
+            "collections/bookkeeper/Quotas",
+            "models/bookkeeper/Subscription",
+          ], function (Quotas, Subscription) {
+            //Create a Quotas collection
+            var quotas = new Quotas();
+
+            //Create a Subscription model
+            var subscription = new Subscription();
+
+            if (MetacatUI.appModel.get("dataonePlusPreviewMode")) {
+              //Create Quota models for preview mode
+              quotas.add({
+                softLimit: MetacatUI.appModel.get("portalLimit"),
+                hardLimit: MetacatUI.appModel.get("portalLimit"),
+                quotaType: "portal",
+                unit: "portal",
+                subject: thisUser.get("username"),
+                subscription: subscription,
+              });
+
+              //Default to all people being in trial mode
+              subscription.set("status", "trialing");
 
-    /**
-    * Gets the already-fetched Quotas for the User, filters down to the type given, and returns them.
-    * @param {string} [type] - The Quota type to return
-    * @returns {Quota[]} The filtered array of Quota models or an empty array, if none are found
-    */
-    getQuotas: function(type){
-      var quotas = this.get("dataoneQuotas");
+              //Save a reference to the Quotas on this UserModel
+              thisUser.set("dataoneQuotas", quotas);
 
-      if( quotas && type ){
-        return quotas.where({ quotaType: type });
-      }
-      else if( quotas && !type ){
-        return quotas;
-      }
-      else{
-        return [];
-      }
+              //Save a reference to the Subscriptioin on this UserModel
+              thisUser.set("dataoneSubscription", subscription);
+            } else {
+              thisUser.listenToOnce(quotas, "reset", function () {
+                //Save a reference to the Quotas on this UserModel
+                thisUser.set("dataoneQuotas", quotas);
+              });
+
+              thisUser.listenToOnce(subscription, "sync", function () {
+                //Save a reference to the Subscriptioin on this UserModel
+                thisUser.set("dataoneSubscription", subscription);
+              });
+
+              //Fetch the Quotas
+              quotas.fetch({ subscriber: thisUser.get("username") });
+
+              //Fetch the Subscriptioin
+              subscription.fetch();
+            }
+          });
+        } catch (e) {
+          console.error(
+            "Couldn't get DataONE Subscription info. Proceeding as an unsubscribed user. ",
+            e,
+          );
+        }
+      },
+
+      /**
+       * Gets the already-fetched Quotas for the User, filters down to the type given, and returns them.
+       * @param {string} [type] - The Quota type to return
+       * @returns {Quota[]} The filtered array of Quota models or an empty array, if none are found
+       */
+      getQuotas: function (type) {
+        var quotas = this.get("dataoneQuotas");
+
+        if (quotas && type) {
+          return quotas.where({ quotaType: type });
+        } else if (quotas && !type) {
+          return quotas;
+        } else {
+          return [];
+        }
+      },
+
+      reset: function () {
+        var defaults = _.omit(this.defaults(), [
+          "searchModel",
+          "searchResults",
+        ]);
+        this.set(defaults);
+      },
     },
+  );
 
-		reset: function(){
-			var defaults = _.omit(this.defaults(), ["searchModel", "searchResults"]);
-			this.set(defaults);
-		}
-	});
-
-	return UserModel;
+  return UserModel;
 });
 
diff --git a/docs/docs/src_js_models_analytics_Analytics.js.html b/docs/docs/src_js_models_analytics_Analytics.js.html index b70f4b53a..60b59acee 100644 --- a/docs/docs/src_js_models_analytics_Analytics.js.html +++ b/docs/docs/src_js_models_analytics_Analytics.js.html @@ -44,8 +44,7 @@

Source: src/js/models/analytics/Analytics.js

-
/* global define */
-define(["backbone"], function (Backbone) {
+            
define(["backbone"], function (Backbone) {
   /**
    * @class Analytics
    * @classdesc A model that connects with an analytics service to record user
@@ -172,7 +171,7 @@ 

Source: src/js/models/analytics/Analytics.js

trackPageView: function (path, title) { return; }, - } + }, ); return Analytics; diff --git a/docs/docs/src_js_models_analytics_GoogleAnalytics.js.html b/docs/docs/src_js_models_analytics_GoogleAnalytics.js.html index 948265a4e..79b2d7d4d 100644 --- a/docs/docs/src_js_models_analytics_GoogleAnalytics.js.html +++ b/docs/docs/src_js_models_analytics_GoogleAnalytics.js.html @@ -44,8 +44,7 @@

Source: src/js/models/analytics/GoogleAnalytics.js

-
/* global define */
-define(["models/analytics/Analytics"], function (Analytics) {
+            
define(["models/analytics/Analytics"], function (Analytics) {
   /**
    * @class GoogleAnalytics
    * @classdesc A model that connects with an analytics service to record user
@@ -159,7 +158,7 @@ 

Source: src/js/models/analytics/GoogleAnalytics.js

page_title: title, }); }, - } + }, ); return GoogleAnalytics; diff --git a/docs/docs/src_js_models_bookkeeper_Quota.js.html b/docs/docs/src_js_models_bookkeeper_Quota.js.html index 4d328b72b..7c00aaa21 100644 --- a/docs/docs/src_js_models_bookkeeper_Quota.js.html +++ b/docs/docs/src_js_models_bookkeeper_Quota.js.html @@ -44,51 +44,46 @@

Source: src/js/models/bookkeeper/Quota.js

-
/* global define */
-define(["jquery",
-        "underscore",
-        "backbone"],
-  function($, _, Backbone) {
-    /**
-     * @classdesc A Quota Model represents a single instance of a Quota object from the
-     * DataONE Bookkeeper data model. Quotas are limits set
-     * for a particular Product, such as the number of portals allowed, disk space
-     * allowed, etc. Quotas have a soft and hard limit per unit to help with communicating limit warnings.
-     * See https://github.com/DataONEorg/bookkeeper for documentation on the
-     * DataONE Bookkeeper service and data model.
-     * @classcategory Models/Bookkeeper
-     * @class Quota
-     * @name Quota
-     * @since 2.14.0
-     * @constructor
-     * @extends Backbone.Model
-    */
-    var Quota = Backbone.Model.extend(
-      /** @lends Quota.prototype */ {
-
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @classdesc A Quota Model represents a single instance of a Quota object from the
+   * DataONE Bookkeeper data model. Quotas are limits set
+   * for a particular Product, such as the number of portals allowed, disk space
+   * allowed, etc. Quotas have a soft and hard limit per unit to help with communicating limit warnings.
+   * See https://github.com/DataONEorg/bookkeeper for documentation on the
+   * DataONE Bookkeeper service and data model.
+   * @classcategory Models/Bookkeeper
+   * @class Quota
+   * @name Quota
+   * @since 2.14.0
+   * @constructor
+   * @extends Backbone.Model
+   */
+  var Quota = Backbone.Model.extend(
+    /** @lends Quota.prototype */ {
       /**
-      * The name of this type of model
-      * @type {string}
-      */
+       * The name of this type of model
+       * @type {string}
+       */
       type: "Quota",
 
       /**
-      * Default attributes for Quota models
-      * @name Quota#defaults
-      * @type {Object}
-      * @property {string} id  The unique id of this Quota, from Bookkeeper
-      * @property {string} quotaType  The quotaType of this Quota type
-      * @property {string[]} quotaTypeOptions The controlled list of `quotaType` values that can be set on a Quota model
-      * @property {string} object  The name of this type of Bookkeeper object, which will always be "quota"
-      * @property {number} softLimit  The soft quota limit, which may be surpassed under certain conditions
-      * @property {number} hardLimit  The hard quota limit, which cannot be surpassed
-      * @property {string} unit  The unit of each Usage of this Quota (e.g. bytes, portals)
-      * @property {string[]} unitOptions The controlled list of `unit` values that can be set on a Quota model
-      * @property {string} customerId  The id of the Customer associated with this Quota
-      * @property {string} subject  The user or group subject associated with this Quota
-      * @property {number} totalUsage  The total or sum of usage of this Quota
-      */
-      defaults: function(){
+       * Default attributes for Quota models
+       * @name Quota#defaults
+       * @type {Object}
+       * @property {string} id  The unique id of this Quota, from Bookkeeper
+       * @property {string} quotaType  The quotaType of this Quota type
+       * @property {string[]} quotaTypeOptions The controlled list of `quotaType` values that can be set on a Quota model
+       * @property {string} object  The name of this type of Bookkeeper object, which will always be "quota"
+       * @property {number} softLimit  The soft quota limit, which may be surpassed under certain conditions
+       * @property {number} hardLimit  The hard quota limit, which cannot be surpassed
+       * @property {string} unit  The unit of each Usage of this Quota (e.g. bytes, portals)
+       * @property {string[]} unitOptions The controlled list of `unit` values that can be set on a Quota model
+       * @property {string} customerId  The id of the Customer associated with this Quota
+       * @property {string} subject  The user or group subject associated with this Quota
+       * @property {number} totalUsage  The total or sum of usage of this Quota
+       */
+      defaults: function () {
         return {
           id: "",
           quotaType: "",
@@ -100,11 +95,11 @@ 

Source: src/js/models/bookkeeper/Quota.js

unitOptions: ["portal", "byte"], customerId: "", subject: "", - totalUsage: 0 - } - } - - }); + totalUsage: 0, + }; + }, + }, + ); return Quota; }); diff --git a/docs/docs/src_js_models_bookkeeper_Subscription.js.html b/docs/docs/src_js_models_bookkeeper_Subscription.js.html index 91f322f29..3ddebed94 100644 --- a/docs/docs/src_js_models_bookkeeper_Subscription.js.html +++ b/docs/docs/src_js_models_bookkeeper_Subscription.js.html @@ -44,54 +44,49 @@

Source: src/js/models/bookkeeper/Subscription.js

-
/* global define */
-define(["jquery",
-        "underscore",
-        "backbone"],
-  function($, _, Backbone) {
-    /**
-     * @classdesc A Subscription Model represents a single instance of a Subscription object from the
-     * DataONE Bookkeeper data model.
-     * Subscriptions represent a Product that has been ordered by a Customer
-     * and is paid for on a recurring basis or is in a free trial period.
-     * See https://github.com/DataONEorg/bookkeeper for documentation on the
-     * DataONE Bookkeeper service and data model.
-     * @classcategory Models/Bookkeeper
-     * @class Subscription
-     * @name Subscription
-     * @since 2.14.0
-     * @constructor
-     * @extends Backbone.Model
-    */
-    var Subscription = Backbone.Model.extend(
-      /** @lends Subscription.prototype */ {
-
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @classdesc A Subscription Model represents a single instance of a Subscription object from the
+   * DataONE Bookkeeper data model.
+   * Subscriptions represent a Product that has been ordered by a Customer
+   * and is paid for on a recurring basis or is in a free trial period.
+   * See https://github.com/DataONEorg/bookkeeper for documentation on the
+   * DataONE Bookkeeper service and data model.
+   * @classcategory Models/Bookkeeper
+   * @class Subscription
+   * @name Subscription
+   * @since 2.14.0
+   * @constructor
+   * @extends Backbone.Model
+   */
+  var Subscription = Backbone.Model.extend(
+    /** @lends Subscription.prototype */ {
       /**
-      * The name of this type of model
-      * @type {string}
-      */
+       * The name of this type of model
+       * @type {string}
+       */
       type: "Subscription",
 
       /**
-      * Default attributes for Subscription models
-      * @name Subscription#defaults
-      * @type {Object}
-      * @property {string} id  The unique identifier of this Subscription, from Bookkeeper
-      * @property {string} object The name of this type of Bookkeeper object, which will always be "subscription"
-      * @property {number} canceledAt  The timestamp of the date that this Subscription was canceled
-      * @property {string} collectionMethod  The method of payment collection for this Subscription, which is a string from a controlled vocabulary from Bookkeeper
-      * @property {number} created  The timestamp of the date that this Subscription was created
-      * @property {number} customerId  The identifier of the Customer that is associated with this Subscription
-      * @property {Object} metadata  Arbitrary metadata about this Subscription. These values should be parsed and set on this model (TODO)
-      * @property {number} productId  The identifier of a Product in this Subscription
-      * @property {number} quantity  The number of Subscriptions
-      * @property {number} startDate  The timestamp of the date that this Subscription was started
-      * @property {string} status  The status of this Subscription, which is taken from a controlled vocabulary set on this model (statusOptions)
-      * @property {string[]} statusOptions  The controlled vocabulary from which the `status` value can be from
-      * @property {number} trialEnd  The timestamp of the date that this free trial Subscription ends
-      * @property {number} trialStart  The timestamp of the date that this free trial Subscription starts
-      */
-      defaults: function(){
+       * Default attributes for Subscription models
+       * @name Subscription#defaults
+       * @type {Object}
+       * @property {string} id  The unique identifier of this Subscription, from Bookkeeper
+       * @property {string} object The name of this type of Bookkeeper object, which will always be "subscription"
+       * @property {number} canceledAt  The timestamp of the date that this Subscription was canceled
+       * @property {string} collectionMethod  The method of payment collection for this Subscription, which is a string from a controlled vocabulary from Bookkeeper
+       * @property {number} created  The timestamp of the date that this Subscription was created
+       * @property {number} customerId  The identifier of the Customer that is associated with this Subscription
+       * @property {Object} metadata  Arbitrary metadata about this Subscription. These values should be parsed and set on this model (TODO)
+       * @property {number} productId  The identifier of a Product in this Subscription
+       * @property {number} quantity  The number of Subscriptions
+       * @property {number} startDate  The timestamp of the date that this Subscription was started
+       * @property {string} status  The status of this Subscription, which is taken from a controlled vocabulary set on this model (statusOptions)
+       * @property {string[]} statusOptions  The controlled vocabulary from which the `status` value can be from
+       * @property {number} trialEnd  The timestamp of the date that this free trial Subscription ends
+       * @property {number} trialStart  The timestamp of the date that this free trial Subscription starts
+       */
+      defaults: function () {
         return {
           id: null,
           object: "subscription",
@@ -104,22 +99,30 @@ 

Source: src/js/models/bookkeeper/Subscription.js

quantity: 0, startDate: null, status: null, - statusOptions: ["trialing", "active", "past_due", "canceled", "unpaid", "incomplete_expired", "incomplete"], + statusOptions: [ + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "incomplete_expired", + "incomplete", + ], trialEnd: null, - trialStart: null - } + trialStart: null, + }; }, /** - * - * Returns true if this Subscription is in a free trial period. - * @returns {boolean} - */ - isTrialing: function(){ + * + * Returns true if this Subscription is in a free trial period. + * @returns {boolean} + */ + isTrialing: function () { return this.get("status") == "trialing"; - } - - }); + }, + }, + ); return Subscription; }); diff --git a/docs/docs/src_js_models_bookkeeper_Usage.js.html b/docs/docs/src_js_models_bookkeeper_Usage.js.html index 907a81ac6..0b643a707 100644 --- a/docs/docs/src_js_models_bookkeeper_Usage.js.html +++ b/docs/docs/src_js_models_bookkeeper_Usage.js.html @@ -44,49 +44,44 @@

Source: src/js/models/bookkeeper/Usage.js

-
/* global define */
-define(["jquery",
-        "underscore",
-        "backbone"],
-  function($, _, Backbone) {
-    /**
-     * @classdesc A Usage Model represents a single instance of a Usage object from the
-     * DataONE Bookkeeper data model. A Usage tracks which objects use a portion of a Quota.
-     * A Quota can be associated with multiple Usages.
-     * See https://github.com/DataONEorg/bookkeeper for documentation on the
-     * DataONE Bookkeeper service and data model.
-     * @classcategory Models/Bookkeeper
-     * @class Usage
-     * @name Usage
-     * @since 2.14.0
-     * @extends Backbone.Model
-     * @constructor
-    */
-    var Usage = Backbone.Model.extend(
-      /** @lends Usage.prototype */ {
-
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @classdesc A Usage Model represents a single instance of a Usage object from the
+   * DataONE Bookkeeper data model. A Usage tracks which objects use a portion of a Quota.
+   * A Quota can be associated with multiple Usages.
+   * See https://github.com/DataONEorg/bookkeeper for documentation on the
+   * DataONE Bookkeeper service and data model.
+   * @classcategory Models/Bookkeeper
+   * @class Usage
+   * @name Usage
+   * @since 2.14.0
+   * @extends Backbone.Model
+   * @constructor
+   */
+  var Usage = Backbone.Model.extend(
+    /** @lends Usage.prototype */ {
       /**
-      * The name of this type of model
-      * @type {string}
-      */
+       * The name of this type of model
+       * @type {string}
+       */
       type: "Usage",
 
       /**
-      * Default attributes for Usage models
-      * @name Usage#defaults
-      * @type {Object}
-      * @property {string} id   The unique id of this Usage, from Bookkeeper
-      * @property {string} object  The name of this type of Bookkeeper object, which will always be "usage"
-      * @property {number} quotaId  The id of the Quota object that this Usage is associated with, from Bookkeeper. This is a match to {@link Quota#defaults#id}
-      * @property {string} instanceId  The id of the {@link DataONEObject} that makes up this Usage
-      * @property {number} quantity  The quantity of the {@link Quota} that this Usage uses, expressed as {@link Quota#defaults#unit}
-      * @property {string} status  The status of this Usage
-      * @property {string[]} statusOptions  The controlled list of `status` values that can be set on a Usage model
-      * @property {string} nodeId The Member Node ID that the object is from
-      * @property {DataONEObject} DataONEObject A reference to the DataONEObject that has the id/seriesId of this Usage instanceId
-      * @property {SolrResult} SolrResult A reference to the SolrResult that has the id/seriesId of this Usage instanceId
-      */
-      defaults: function(){
+       * Default attributes for Usage models
+       * @name Usage#defaults
+       * @type {Object}
+       * @property {string} id   The unique id of this Usage, from Bookkeeper
+       * @property {string} object  The name of this type of Bookkeeper object, which will always be "usage"
+       * @property {number} quotaId  The id of the Quota object that this Usage is associated with, from Bookkeeper. This is a match to {@link Quota#defaults#id}
+       * @property {string} instanceId  The id of the {@link DataONEObject} that makes up this Usage
+       * @property {number} quantity  The quantity of the {@link Quota} that this Usage uses, expressed as {@link Quota#defaults#unit}
+       * @property {string} status  The status of this Usage
+       * @property {string[]} statusOptions  The controlled list of `status` values that can be set on a Usage model
+       * @property {string} nodeId The Member Node ID that the object is from
+       * @property {DataONEObject} DataONEObject A reference to the DataONEObject that has the id/seriesId of this Usage instanceId
+       * @property {SolrResult} SolrResult A reference to the SolrResult that has the id/seriesId of this Usage instanceId
+       */
+      defaults: function () {
         return {
           id: null,
           object: "usage",
@@ -97,11 +92,11 @@ 

Source: src/js/models/bookkeeper/Usage.js

statusOptions: ["active", "inactive"], nodeId: "", DataONEObject: null, - SolrResult: null - } - } - - }); + SolrResult: null, + }; + }, + }, + ); return Usage; }); diff --git a/docs/docs/src_js_models_connectors_Filters-Map.js.html b/docs/docs/src_js_models_connectors_Filters-Map.js.html index d6ba224b9..cf27a6489 100644 --- a/docs/docs/src_js_models_connectors_Filters-Map.js.html +++ b/docs/docs/src_js_models_connectors_Filters-Map.js.html @@ -44,11 +44,10 @@

Source: src/js/models/connectors/Filters-Map.js

-
/*global define */
-define(["backbone", "collections/Filters", "models/maps/Map"], function (
+            
define(["backbone", "collections/Filters", "models/maps/Map"], function (
   Backbone,
   Filters,
-  Map
+  Map,
 ) {
   "use strict";
 
@@ -115,7 +114,7 @@ 

Source: src/js/models/connectors/Filters-Map.js

initialize: function (attr, options) { try { if (!this.get("filters")) { - this.set("filters", new Filters([], { catalogSearch: true })); + this.set("filters", new Filters([], { catalogSearch: true })); } if (!this.get("map")) { this.set("map", new Map()); @@ -164,7 +163,7 @@

Source: src/js/models/connectors/Filters-Map.js

this.listenToOnce( this.get("filters"), "add remove", - this.findAndSetSpatialFilters + this.findAndSetSpatialFilters, ); }, @@ -195,7 +194,7 @@

Source: src/js/models/connectors/Filters-Map.js

this.stopListening( this.get("filters"), "add remove", - this.findAndSetSpatialFilters + this.findAndSetSpatialFilters, ); spatialFilters.forEach((filter) => { filter.collection.remove(filter); @@ -293,7 +292,7 @@

Source: src/js/models/connectors/Filters-Map.js

console.log("Error updating spatial filters: ", e); } }, - } + }, ); });
diff --git a/docs/docs/src_js_models_connectors_Filters-Search.js.html b/docs/docs/src_js_models_connectors_Filters-Search.js.html index 7a91933b1..595e5ea9b 100644 --- a/docs/docs/src_js_models_connectors_Filters-Search.js.html +++ b/docs/docs/src_js_models_connectors_Filters-Search.js.html @@ -44,8 +44,7 @@

Source: src/js/models/connectors/Filters-Search.js

-
/*global define */
-define([
+            
define([
   "backbone",
   "collections/Filters",
   "collections/SolrResults",
@@ -110,13 +109,13 @@ 

Source: src/js/models/connectors/Filters-Search.js

search.trigger("changing"); }); - this.listenTo(search, "change:sort change:facet", () => this.triggerSearch()); + this.listenTo(search, "change:sort change:facet", () => + this.triggerSearch(), + ); // If the logged-in status changes, send a new search - this.listenTo( - MetacatUI.appUserModel, - "change:loggedIn", - () => this.triggerSearch() + this.listenTo(MetacatUI.appUserModel, "change:loggedIn", () => + this.triggerSearch(), ); this.set("isConnected", true); @@ -173,7 +172,7 @@

Source: src/js/models/connectors/Filters-Search.js

// Send the query to the server via the SolrResults collection searchResults.toPage(page); }, - } + }, ); });
diff --git a/docs/docs/src_js_models_connectors_GeoPoints-Cesium.js.html b/docs/docs/src_js_models_connectors_GeoPoints-Cesium.js.html index 8449ee299..bc4fc7356 100644 --- a/docs/docs/src_js_models_connectors_GeoPoints-Cesium.js.html +++ b/docs/docs/src_js_models_connectors_GeoPoints-Cesium.js.html @@ -46,7 +46,6 @@

Source: src/js/models/connectors/GeoPoints-Cesium.js

"use strict";
 
-/*global define */
 define([
   "backbone",
   "cesium",
@@ -208,7 +207,7 @@ 

Source: src/js/models/connectors/GeoPoints-Cesium.js

console.warn('Error handling a "' + eventName + '" event.', e); } }, - } + }, ); });
diff --git a/docs/docs/src_js_models_connectors_GeoPoints-CesiumPoints.js.html b/docs/docs/src_js_models_connectors_GeoPoints-CesiumPoints.js.html index 7a02f985c..401a9cac9 100644 --- a/docs/docs/src_js_models_connectors_GeoPoints-CesiumPoints.js.html +++ b/docs/docs/src_js_models_connectors_GeoPoints-CesiumPoints.js.html @@ -46,10 +46,9 @@

Source: src/js/models/connectors/GeoPoints-CesiumPoints.j
"use strict";
 
-/*global define */
 define(["cesium", "models/connectors/GeoPoints-Cesium"], function (
   Cesium,
-  GeoPointsCesiumConnector
+  GeoPointsCesiumConnector,
 ) {
   /**
    * @class GeoPointsCesiumPointsConnector
@@ -210,7 +209,7 @@ 

Source: src/js/models/connectors/GeoPoints-CesiumPoints.j console.log("Failed to remove a point from a CesiumVectorData.", e); } }, - } + }, ); });

diff --git a/docs/docs/src_js_models_connectors_GeoPoints-CesiumPolygon.js.html b/docs/docs/src_js_models_connectors_GeoPoints-CesiumPolygon.js.html index a15f105ad..a3b146892 100644 --- a/docs/docs/src_js_models_connectors_GeoPoints-CesiumPolygon.js.html +++ b/docs/docs/src_js_models_connectors_GeoPoints-CesiumPolygon.js.html @@ -46,10 +46,9 @@

Source: src/js/models/connectors/GeoPoints-CesiumPolygon.
"use strict";
 
-/*global define */
 define(["cesium", "models/connectors/GeoPoints-Cesium"], function (
   Cesium,
-  GeoPointsCesiumConnector
+  GeoPointsCesiumConnector,
 ) {
   /**
    * @class GeoPointsCesiumPolygonConnector
@@ -116,7 +115,7 @@ 

Source: src/js/models/connectors/GeoPoints-CesiumPolygon. this.get("polygon") || this.addPolygon(); this.get("layer").updateAppearance(); }, - } + }, ); });

diff --git a/docs/docs/src_js_models_connectors_Map-Search-Filters.js.html b/docs/docs/src_js_models_connectors_Map-Search-Filters.js.html index 22d43e063..787d080a6 100644 --- a/docs/docs/src_js_models_connectors_Map-Search-Filters.js.html +++ b/docs/docs/src_js_models_connectors_Map-Search-Filters.js.html @@ -44,8 +44,7 @@

Source: src/js/models/connectors/Map-Search-Filters.js
-
/*global define */
-define([
+            
define([
   "backbone",
   "models/maps/Map",
   "collections/SolrResults",
@@ -62,7 +61,7 @@ 

Source: src/js/models/connectors/Map-Search-Filters.jsSource: src/js/models/connectors/Map-Search-Filters.jsSource: src/js/models/connectors/Map-Search-Filters.jsSource: src/js/models/connectors/Map-Search-Filters.js

diff --git a/docs/docs/src_js_models_connectors_Map-Search.js.html b/docs/docs/src_js_models_connectors_Map-Search.js.html index cc706dbd7..b0bd2f3da 100644 --- a/docs/docs/src_js_models_connectors_Map-Search.js.html +++ b/docs/docs/src_js_models_connectors_Map-Search.js.html @@ -44,12 +44,11 @@

Source: src/js/models/connectors/Map-Search.js

-
/*global define */
-define([
-  "backbone",
-  "models/maps/Map",
-  "collections/SolrResults",
-], function (Backbone, Map, SearchResults) {
+            
define(["backbone", "models/maps/Map", "collections/SolrResults"], function (
+  Backbone,
+  Map,
+  SearchResults,
+) {
   "use strict";
 
   /**
@@ -63,13 +62,12 @@ 

Source: src/js/models/connectors/Map-Search.js

*/ return Backbone.Model.extend( /** @lends MapSearchConnector.prototype */ { - /** * The type of Backbone.Model this is. * @type {string} * @since 2.25.0 * @default "MapSearchConnector" - */ + */ type: "MapSearchConnector", /** @@ -84,7 +82,7 @@

Source: src/js/models/connectors/Map-Search.js

return { searchResults: null, map: null, - onMoveEnd: this.onMoveEnd + onMoveEnd: this.onMoveEnd, }; }, @@ -127,13 +125,17 @@

Source: src/js/models/connectors/Map-Search.js

// TODO: Since only the first Geohash is needed, create a getFirst // function in MapAssets. - let geohashes = _.reduce(layerGroups, (memo, layers) => { - const geohashes = layers.getAll("CesiumGeohash"); - if (geohashes && geohashes.length) { - memo.push(...geohashes); - } - return memo; - }, []); + let geohashes = _.reduce( + layerGroups, + (memo, layers) => { + const geohashes = layers.getAll("CesiumGeohash"); + if (geohashes && geohashes.length) { + memo.push(...geohashes); + } + return memo; + }, + [], + ); if (!geohashes || !geohashes.length) { return null; } else { @@ -197,7 +199,7 @@

Source: src/js/models/connectors/Map-Search.js

} // If there is still no Geohash layer, then we should wait for one to // be added to the Layers collection, then try to find it again. - _.each(layerGroups, layers => { + _.each(layerGroups, (layers) => { this.stopListening(layers, "add", this.findAndSetGeohashLayer); if (!geohash) { this.listenTo(layers, "add", this.findAndSetGeohashLayer); @@ -237,7 +239,11 @@

Source: src/js/models/connectors/Map-Search.js

// When the user is panning/zooming in the map, hide the GeoHash layer // to indicate that the map is not up to date with the search results, // which are about to be updated. - this.listenTo(interactions, "moveStartAndChanged", this.hideGeoHashLayer); + this.listenTo( + interactions, + "moveStartAndChanged", + this.hideGeoHashLayer, + ); // When the user is done panning/zooming in the map, show the GeoHash // layer again and update the search results (thereby updating the @@ -336,7 +342,7 @@

Source: src/js/models/connectors/Map-Search.js

const facetCounts = searchResults?.facetCounts; if (!facetCounts) return null; const geohashFacets = Object.keys(facetCounts).filter((key) => - key.startsWith("geohash_") + key.startsWith("geohash_"), ); return geohashFacets.flatMap((key) => facetCounts[key]); }, @@ -386,7 +392,7 @@

Source: src/js/models/connectors/Map-Search.js

const geohashes9 = searchResult.get("geohash_9"); this.get("geohashLayer").selectGeohashes(geohashes9); }, - } + }, ); });
diff --git a/docs/docs/src_js_models_filters_BooleanFilter.js.html b/docs/docs/src_js_models_filters_BooleanFilter.js.html index 46e85661f..e63e40ce4 100644 --- a/docs/docs/src_js_models_filters_BooleanFilter.js.html +++ b/docs/docs/src_js_models_filters_BooleanFilter.js.html @@ -44,110 +44,105 @@

Source: src/js/models/filters/BooleanFilter.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/filters/Filter'],
-    function($, _, Backbone, Filter) {
-
+            
define(["jquery", "underscore", "backbone", "models/filters/Filter"], function (
+  $,
+  _,
+  Backbone,
+  Filter,
+) {
   /**
-  * @class BooleanFilter
-  * @classdesc A search filter that only has `true` or `false` as a search term
-  * @classcategory Models/Filters
-  * @name BooleanFilter
-  * @extends Filter
-  * @constructs
-  */
-	var BooleanFilter = Filter.extend(
+   * @class BooleanFilter
+   * @classdesc A search filter that only has `true` or `false` as a search term
+   * @classcategory Models/Filters
+   * @name BooleanFilter
+   * @extends Filter
+   * @constructs
+   */
+  var BooleanFilter = Filter.extend(
     /** @lends BooleanFilter.prototype */
     {
+      /** @inheritdoc */
+      type: "BooleanFilter",
+
+      /** @inheritdoc */
+      defaults: function () {
+        return _.extend(Filter.prototype.defaults(), {
+          //Boolean filters can't match substrings
+          matchSubstring: false,
+          nodeName: "booleanFilter",
+        });
+      },
+
+      /**
+       * Parses the booleanFilter XML node into JSON
+       *
+       * @param {Element} xml - The XML Element that contains all the BooleanFilter elements
+       * @return {JSON} - The JSON object literal to be set on the model
+       */
+      parse: function (xml) {
+        var modelJSON = Filter.prototype.parse.call(this, xml);
+
+        //If this Filter is in a filter group, don't parse the value
+        if (!this.get("isUIFilterType")) {
+          //Parse the boolean value
+          modelJSON.values = this.parseTextNode(xml, "value");
+
+          if (modelJSON.values === "true") {
+            modelJSON.values = [true];
+          } else if (modelJSON.values === "false") {
+            modelJSON.values = [false];
+          }
+        }
 
-    /** @inheritdoc */
-    type: "BooleanFilter",
-
-    /** @inheritdoc */
-    defaults: function(){
-      return _.extend(Filter.prototype.defaults(), {
-        //Boolean filters can't match substrings
-        matchSubstring: false,
-        nodeName: "booleanFilter",
-      });
-    },
+        return modelJSON;
+      },
+
+      /**
+       * Updates the XML DOM with the new values from the model
+       *  @inheritdoc
+       *  @return {Element} An updated booleanFilter XML element from a portal document
+       */
+      updateDOM: function (options) {
+        if (typeof options == "undefined") {
+          var options = {};
+        }
 
-    /**
-    * Parses the booleanFilter XML node into JSON
-    *
-    * @param {Element} xml - The XML Element that contains all the BooleanFilter elements
-    * @return {JSON} - The JSON object literal to be set on the model
-    */
-    parse: function(xml){
+        //Update the DOM using the parent Filter model
+        var objectDOM = Filter.prototype.updateDOM.call(this);
 
-      var modelJSON = Filter.prototype.parse.call(this, xml);
+        //Boolean Filters don't use matchSubstring and operator nodes
+        $(objectDOM).children("matchSubstring, operator").remove();
 
-      //If this Filter is in a filter group, don't parse the value
-      if( !this.get("isUIFilterType") ){
+        // Get the new boolean value
+        var value = this.get("value");
 
-        //Parse the boolean value
-        modelJSON.values = this.parseTextNode(xml, "value");
+        // Make a <value> node with the new boolean value and append it to DOM
+        if (value || value === false) {
+          var valueSerialized = objectDOM.ownerDocument.createElement("value");
+          $(valueSerialized).text(value);
+          $(objectDOM).append(valueSerialized);
+        }
 
-        if(modelJSON.values === "true"){
-          modelJSON.values = [true];
+        if (this.get("isUIFilterType")) {
+          //Make sure the filterOptions are listed last
+          //Get the filterOptions element
+          var filterOptions = $(objectDOM).children("filterOptions");
+          //If the filterOptions exist
+          if (filterOptions.length) {
+            //Detach from their current position and append to the end
+            filterOptions.detach();
+            $(objectDOM).append(filterOptions);
+          }
         }
-        else if(modelJSON.values === "false"){
-          modelJSON.values = [false];
+        //Remove filter options if this is for a collection definition
+        else {
+          $(objectDOM).children("filterOptions").remove();
         }
 
-      }
-
-      return modelJSON;
+        return objectDOM;
+      },
     },
-
-    /**
-     * Updates the XML DOM with the new values from the model
-     *  @inheritdoc
-     *  @return {Element} An updated booleanFilter XML element from a portal document
-    */
-    updateDOM: function(options) {
-
-      if( typeof options == "undefined" ){
-        var options = {};
-      }
-
-      //Update the DOM using the parent Filter model
-      var objectDOM = Filter.prototype.updateDOM.call(this);
-
-      //Boolean Filters don't use matchSubstring and operator nodes
-      $(objectDOM).children("matchSubstring, operator").remove();
-
-      // Get the new boolean value
-      var value = this.get("value");
-
-      // Make a <value> node with the new boolean value and append it to DOM
-      if( value || value === false){
-        var valueSerialized = objectDOM.ownerDocument.createElement("value");
-        $(valueSerialized).text(value);
-        $(objectDOM).append(valueSerialized);
-      }
-
-      if( this.get("isUIFilterType") ){
-        //Make sure the filterOptions are listed last
-        //Get the filterOptions element
-        var filterOptions = $(objectDOM).children("filterOptions");
-        //If the filterOptions exist
-        if( filterOptions.length ){
-          //Detach from their current position and append to the end
-          filterOptions.detach();
-          $(objectDOM).append(filterOptions);
-        }
-      }
-      //Remove filter options if this is for a collection definition
-      else{
-        $(objectDOM).children("filterOptions").remove();
-      }
-
-      return objectDOM
-
-    }
-
-  });
+  );
 
   return BooleanFilter;
 });
diff --git a/docs/docs/src_js_models_filters_ChoiceFilter.js.html b/docs/docs/src_js_models_filters_ChoiceFilter.js.html
index b2b19dfcb..1042953e7 100644
--- a/docs/docs/src_js_models_filters_ChoiceFilter.js.html
+++ b/docs/docs/src_js_models_filters_ChoiceFilter.js.html
@@ -44,217 +44,214 @@ 

Source: src/js/models/filters/ChoiceFilter.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/filters/Filter'],
-    function($, _, Backbone, Filter) {
-
+            
define(["jquery", "underscore", "backbone", "models/filters/Filter"], function (
+  $,
+  _,
+  Backbone,
+  Filter,
+) {
   /**
-  * @class ChoiceFilter
-  * @classdesc A Filter whose search term is one or more choices from a defined list
-  * @classcategory Models/Filters
-  * @name ChoiceFilter
-  * @constructs ChoiceFilter
-  * @extends Filter
-  */
-	var ChoiceFilter = Filter.extend(
-    /** @lends ChoiceFilter.prototype */{
-
-    /**
-    * @inheritdoc
-    * @type {string}
-    */
-    type: "ChoiceFilter",
-
-    /**
-    * The Backbone Model attributes set on this ChoiceFilter
-    * @type {object}
-    * @extends Filter#defaultts
-    * @property {boolean} chooseMultiple - If true, this ChoiceFilter can have multiple choices set as the search term
-    * @property {string[]} choices - The list of search terms that can possibly be set on this Filter
-    * @property {string} nodeName - The XML node name to use when serializing this model into XML
-    */
-    defaults: function(){
-      return _.extend(Filter.prototype.defaults(), {
-        chooseMultiple: true,
-        //@type {object} - A literal JS object with a "label" and "value" attribute
-        choices: [],
-        nodeName: "choiceFilter"
-      });
-    },
-
-    /**
-    * Parses the choiceFilter XML node into JSON
-    *
-    * @param {Element} xml - The XML Element that contains all the ChoiceFilter elements
-    * @return {JSON} - The JSON object literal to be set on the model
-    */
-    parse: function(xml){
-
-      var modelJSON = Filter.prototype.parse.call(this, xml);
-
-      //Parse the chooseMultiple boolean field
-      modelJSON.chooseMultiple = (this.parseTextNode(xml, "chooseMultiple") === "true")? true : false;
-
-      //Start an array for the choices
-      modelJSON.choices = [];
-
-      //Iterate over each choice and parse it
-      var self = this;
-      $(xml).find("choice").each(function(i, choiceNode){
+   * @class ChoiceFilter
+   * @classdesc A Filter whose search term is one or more choices from a defined list
+   * @classcategory Models/Filters
+   * @name ChoiceFilter
+   * @constructs ChoiceFilter
+   * @extends Filter
+   */
+  var ChoiceFilter = Filter.extend(
+    /** @lends ChoiceFilter.prototype */ {
+      /**
+       * @inheritdoc
+       * @type {string}
+       */
+      type: "ChoiceFilter",
+
+      /**
+       * The Backbone Model attributes set on this ChoiceFilter
+       * @type {object}
+       * @extends Filter#defaultts
+       * @property {boolean} chooseMultiple - If true, this ChoiceFilter can have multiple choices set as the search term
+       * @property {string[]} choices - The list of search terms that can possibly be set on this Filter
+       * @property {string} nodeName - The XML node name to use when serializing this model into XML
+       */
+      defaults: function () {
+        return _.extend(Filter.prototype.defaults(), {
+          chooseMultiple: true,
+          //@type {object} - A literal JS object with a "label" and "value" attribute
+          choices: [],
+          nodeName: "choiceFilter",
+        });
+      },
 
-        //Parse the label and value nodes into a literal object
-        var choiceObject = {
-          label: self.parseTextNode(choiceNode, "label"),
-          value: self.parseTextNode(choiceNode, "value")
-        }
+      /**
+       * Parses the choiceFilter XML node into JSON
+       *
+       * @param {Element} xml - The XML Element that contains all the ChoiceFilter elements
+       * @return {JSON} - The JSON object literal to be set on the model
+       */
+      parse: function (xml) {
+        var modelJSON = Filter.prototype.parse.call(this, xml);
+
+        //Parse the chooseMultiple boolean field
+        modelJSON.chooseMultiple =
+          this.parseTextNode(xml, "chooseMultiple") === "true" ? true : false;
+
+        //Start an array for the choices
+        modelJSON.choices = [];
+
+        //Iterate over each choice and parse it
+        var self = this;
+        $(xml)
+          .find("choice")
+          .each(function (i, choiceNode) {
+            //Parse the label and value nodes into a literal object
+            var choiceObject = {
+              label: self.parseTextNode(choiceNode, "label"),
+              value: self.parseTextNode(choiceNode, "value"),
+            };
+
+            //Check that there is a label and value (value can be boolean false or 0, so just check for null or undefined)
+            if (
+              choiceObject.label &&
+              choiceObject.value !== null &&
+              typeof choiceObject.value !== "undefined"
+            ) {
+              modelJSON.choices.push(choiceObject);
+            }
+          });
 
-        //Check that there is a label and value (value can be boolean false or 0, so just check for null or undefined)
-        if(choiceObject.label && choiceObject.value !== null && typeof choiceObject.value !== "undefined"){
-          modelJSON.choices.push(choiceObject);
-        }
+        return modelJSON;
+      },
 
-      });
+      /**
+       * Updates the XML DOM with the new values from the model
+       *  @inheritdoc
+       *  @return {XMLElement} An updated choiceFilter XML element from a portal document
+       */
+      updateDOM: function (options) {
+        try {
+          var objectDOM = Filter.prototype.updateDOM.call(this);
+
+          if (typeof options != "object") {
+            var options = {};
+          }
 
-      return modelJSON;
-    },
+          if (this.get("isUIFilterType")) {
+            // Serialize <choice> elements
+            var choices = this.get("choices");
+
+            if (choices) {
+              //Remove all the choice elements
+              $(objectDOM).children("choice").remove();
+
+              //Make a new choice element for each choice in the model
+              _.each(choices, function (choice) {
+                // Make new <choice> node
+                choiceSerialized =
+                  objectDOM.ownerDocument.createElement("choice");
+                // Make choice subnodes <label> and <value>
+                _.map(choice, function (value, nodeName) {
+                  if (value || value === false) {
+                    var nodeSerialized =
+                      objectDOM.ownerDocument.createElement(nodeName);
+                    $(nodeSerialized).text(value);
+                    $(choiceSerialized).append(nodeSerialized);
+                  }
+                });
+                // append subnodes
+                $(objectDOM).append(choiceSerialized);
+              });
+            }
 
-    /**
-     * Updates the XML DOM with the new values from the model
-     *  @inheritdoc
-     *  @return {XMLElement} An updated choiceFilter XML element from a portal document
-    */
-    updateDOM:function(options){
+            //Get the chooseMultiple value from the model
+            var chooseMultiple = this.get("chooseMultiple");
+            //Remove the chooseMultiple element
+            $(objectDOM).children("chooseMultiple").remove();
+            //If the model value is a boolean, create a chooseMultiple element and add it to the DOM
+            if (chooseMultiple === true || chooseMultiple === false) {
+              chooseMultipleSerialized =
+                objectDOM.ownerDocument.createElement("chooseMultiple");
+              $(chooseMultipleSerialized).text(chooseMultiple);
+              $(objectDOM).append(chooseMultipleSerialized);
+            }
+          } else {
+            //Remove the filterOptions
+            $(objectDOM).find("filterOptions").remove();
 
-      try{
+            //Change the root element into a <filter> element
+            var newFilterEl = objectDOM.ownerDocument.createElement("filter");
+            $(newFilterEl).html($(objectDOM).children());
 
-        var objectDOM = Filter.prototype.updateDOM.call(this);
+            //Return this node
+            return newFilterEl;
+          }
 
-        if(typeof options != "object"){
-          var options = {};
+          return objectDOM;
+        } catch (e) {
+          //If there's an error, return the original DOM or an empty string
+          return this.get("objectDOM") || "";
         }
+      },
 
-        if( this.get("isUIFilterType") ){
-          // Serialize <choice> elements
-          var choices = this.get("choices");
-
-          if(choices){
-            //Remove all the choice elements
-            $(objectDOM).children("choice").remove();
-
-            //Make a new choice element for each choice in the model
-            _.each(choices, function(choice){
-              // Make new <choice> node
-              choiceSerialized = objectDOM.ownerDocument.createElement("choice");
-              // Make choice subnodes <label> and <value>
-              _.map(choice, function(value, nodeName){
+      /**
+       * Checks if the values set on this model are valid and expected
+       * @return {object} - Returns a literal object with the invalid attributes and their
+       * corresponding error message
+       */
+      validate: function () {
+        try {
+          // Validate most of the ChoiceFilter attributes using the parent validate
+          // function
+          var errors = Filter.prototype.validate.call(this);
+
+          // If everything is valid so far, then we have to create a new object to store
+          // errors
+          if (typeof errors != "object") {
+            errors = {};
+          }
 
-                if(value || value === false){
-                  var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
-                  $(nodeSerialized).text(value);
-                  $(choiceSerialized).append(nodeSerialized);
-                }
+          // Delete error messages for the attributes that are going to be validated
+          // specially for the ChoiceFilter
+          delete errors.choices;
 
-              });
-            // append subnodes
-            $(objectDOM).append(choiceSerialized);
+          // Save a reference to the choices
+          var choices = this.get("choices");
 
+          // Ensure that there is at least one choice
+          if (!choices || choices.length === 0) {
+            errors.choices = "At least one search term option is required.";
+          } else {
+            // Remove any empty choices
+            choices = choices.filter(function (choice) {
+              return choice.value || choice.label;
+            });
+            // If there is no value but there is a label, then set the search value to the
+            // label. If there is a value but no label, set the label to the value.
+            choices.forEach(function (choice) {
+              if (!choice.value) {
+                choice.value = choice.label;
+              }
+              if (!choice.label) {
+                choice.label = choice.value;
+              }
             });
-
           }
 
-          //Get the chooseMultiple value from the model
-          var chooseMultiple = this.get("chooseMultiple");
-          //Remove the chooseMultiple element
-          $(objectDOM).children("chooseMultiple").remove();
-          //If the model value is a boolean, create a chooseMultiple element and add it to the DOM
-          if(chooseMultiple === true || chooseMultiple === false){
-            chooseMultipleSerialized = objectDOM.ownerDocument.createElement("chooseMultiple");
-            $(chooseMultipleSerialized).text(chooseMultiple);
-            $(objectDOM).append(chooseMultipleSerialized);
-          };
-        }
-        else{
-          //Remove the filterOptions
-          $(objectDOM).find("filterOptions").remove();
-
-          //Change the root element into a <filter> element
-          var newFilterEl = objectDOM.ownerDocument.createElement("filter");
-          $(newFilterEl).html( $(objectDOM).children() );
-
-          //Return this node
-          return newFilterEl;
+          // Return the errors, if there are any
+          if (Object.keys(errors).length) return errors;
+          else {
+            return;
+          }
+        } catch (error) {
+          console.log(
+            "There was an error validating a ChoiceFilter" +
+              ". Error details: " +
+              error,
+          );
         }
-
-        return objectDOM;
-      }
-      //If there's an error, return the original DOM or an empty string
-      catch(e){
-        return this.get("objectDOM") || "";
-      }
-
       },
-    
-    /**
-    * Checks if the values set on this model are valid and expected
-    * @return {object} - Returns a literal object with the invalid attributes and their
-    * corresponding error message
-    */
-    validate : function(){
-      try {
-
-        // Validate most of the ChoiceFilter attributes using the parent validate
-        // function
-        var errors = Filter.prototype.validate.call(this);
-
-        // If everything is valid so far, then we have to create a new object to store
-        // errors
-        if (typeof errors != "object") {
-          errors = {};
-        }
-
-        // Delete error messages for the attributes that are going to be validated
-        // specially for the ChoiceFilter
-        delete errors.choices;
-
-        // Save a reference to the choices
-        var choices = this.get("choices")
-
-        // Ensure that there is at least one choice
-        if (!choices || choices.length === 0) {
-          errors.choices = "At least one search term option is required."
-        } else {
-          // Remove any empty choices
-          choices = choices.filter(function (choice) {
-            return (choice.value || choice.label)
-          })
-          // If there is no value but there is a label, then set the search value to the
-          // label. If there is a value but no label, set the label to the value.
-          choices.forEach(function (choice) {
-            if (!choice.value) {
-              choice.value = choice.label
-            }
-            if (!choice.label) {
-              choice.label = choice.value
-            }
-          })
-        }
-
-        // Return the errors, if there are any
-        if (Object.keys(errors).length)
-          return errors;
-        else {
-          return;
-        }
-      }
-      catch (error) {
-        console.log(
-          'There was an error validating a ChoiceFilter' +
-          '. Error details: ' + error
-        );
-      }
     },
-
-  });
+  );
 
   return ChoiceFilter;
 });
diff --git a/docs/docs/src_js_models_filters_DateFilter.js.html b/docs/docs/src_js_models_filters_DateFilter.js.html
index 43e968970..2fad7a0b7 100644
--- a/docs/docs/src_js_models_filters_DateFilter.js.html
+++ b/docs/docs/src_js_models_filters_DateFilter.js.html
@@ -44,442 +44,467 @@ 

Source: src/js/models/filters/DateFilter.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/filters/Filter'],
-    function($, _, Backbone, Filter) {
-
+            
define(["jquery", "underscore", "backbone", "models/filters/Filter"], function (
+  $,
+  _,
+  Backbone,
+  Filter,
+) {
   /**
-  * @class DateFilter
-  * @classdesc A search filter whose search term is an exact date or date range
-  * @classcategory Models/Filters
-  * @constructs DateFilter
-  * @extends Filter
-  */
-	var DateFilter = Filter.extend(
-    /** @lends DateFilter.prototype */{
-
-    type: "DateFilter",
-
-    /**
-    * The Backbone Model attributes set on this DateFilter
-    * @type {object}
-    * @extends Filter#defaultts
-    * @property {Date} min - The minimum Date to use in the query for this filter
-    * @property {Date} max - The maximum Date to use in the query for this filter
-    * @property {Date} rangeMin - The earliest possible Date that 'min' can be
-    * @property {Date} rangeMax - The latest possible Date that 'max' can be
-    * @property {Boolean} matchSubstring - Will always be stet to false, since Dates don't have substrings
-    * @property {string} nodeName - The XML node name to use when serializing this model into XML
-    */
-    defaults: function(){
-      return _.extend(Filter.prototype.defaults(), {
-        min: 0,
-        max: (new Date()).getUTCFullYear(),
-        rangeMin: 1800,
-        rangeMax: (new Date()).getUTCFullYear(),
-        matchSubstring: false,
-        nodeName: "dateFilter"
-      });
-    },
-
-    /**
-    * Parses the dateFilter XML node into JSON
-    *
-    * @param {Element} xml - The XML Element that contains all the DateFilter elements
-    * @return {JSON} - The JSON object literal to be set on the model
-    */
-    parse: function(xml){
-
-      try{
-        var modelJSON = Filter.prototype.parse.call(this, xml);
-
-        //Get the rangeMin and rangeMax nodes
-        var rangeMinNode = $(xml).find("rangeMin"),
+   * @class DateFilter
+   * @classdesc A search filter whose search term is an exact date or date range
+   * @classcategory Models/Filters
+   * @constructs DateFilter
+   * @extends Filter
+   */
+  var DateFilter = Filter.extend(
+    /** @lends DateFilter.prototype */ {
+      type: "DateFilter",
+
+      /**
+       * The Backbone Model attributes set on this DateFilter
+       * @type {object}
+       * @extends Filter#defaultts
+       * @property {Date} min - The minimum Date to use in the query for this filter
+       * @property {Date} max - The maximum Date to use in the query for this filter
+       * @property {Date} rangeMin - The earliest possible Date that 'min' can be
+       * @property {Date} rangeMax - The latest possible Date that 'max' can be
+       * @property {Boolean} matchSubstring - Will always be stet to false, since Dates don't have substrings
+       * @property {string} nodeName - The XML node name to use when serializing this model into XML
+       */
+      defaults: function () {
+        return _.extend(Filter.prototype.defaults(), {
+          min: 0,
+          max: new Date().getUTCFullYear(),
+          rangeMin: 1800,
+          rangeMax: new Date().getUTCFullYear(),
+          matchSubstring: false,
+          nodeName: "dateFilter",
+        });
+      },
+
+      /**
+       * Parses the dateFilter XML node into JSON
+       *
+       * @param {Element} xml - The XML Element that contains all the DateFilter elements
+       * @return {JSON} - The JSON object literal to be set on the model
+       */
+      parse: function (xml) {
+        try {
+          var modelJSON = Filter.prototype.parse.call(this, xml);
+
+          //Get the rangeMin and rangeMax nodes
+          var rangeMinNode = $(xml).find("rangeMin"),
             rangeMaxNode = $(xml).find("rangeMax");
 
-        //Parse the range min
-        if( rangeMinNode.length ){
-          modelJSON.rangeMin = new Date(rangeMinNode[0].textContent).getUTCFullYear();
-        }
-        //Parse the range max
-        if( rangeMaxNode.length ){
-          modelJSON.rangeMax = new Date(rangeMaxNode[0].textContent).getUTCFullYear();
-        }
+          //Parse the range min
+          if (rangeMinNode.length) {
+            modelJSON.rangeMin = new Date(
+              rangeMinNode[0].textContent,
+            ).getUTCFullYear();
+          }
+          //Parse the range max
+          if (rangeMaxNode.length) {
+            modelJSON.rangeMax = new Date(
+              rangeMaxNode[0].textContent,
+            ).getUTCFullYear();
+          }
 
-        //If this Filter is in a filter group, don't parse the values
-        if( !this.get("isUIFilterType") ){
-          //Get the min, max, and value nodes
-          var minNode = $(xml).find("min"),
+          //If this Filter is in a filter group, don't parse the values
+          if (!this.get("isUIFilterType")) {
+            //Get the min, max, and value nodes
+            var minNode = $(xml).find("min"),
               maxNode = $(xml).find("max"),
               valueNode = $(xml).find("value");
 
-          //Parse the min value
-          if( minNode.length ){
-            modelJSON.min = new Date(minNode[0].textContent).getUTCFullYear();
-          }
-          //Parse the max value
-          if( maxNode.length ){
-            modelJSON.max = new Date(maxNode[0].textContent).getUTCFullYear();
-          }
-          //Parse the value
-          if( valueNode.length ){
-            modelJSON.values = [new Date(valueNode[0].textContent).getUTCFullYear()];
+            //Parse the min value
+            if (minNode.length) {
+              modelJSON.min = new Date(minNode[0].textContent).getUTCFullYear();
+            }
+            //Parse the max value
+            if (maxNode.length) {
+              modelJSON.max = new Date(maxNode[0].textContent).getUTCFullYear();
+            }
+            //Parse the value
+            if (valueNode.length) {
+              modelJSON.values = [
+                new Date(valueNode[0].textContent).getUTCFullYear(),
+              ];
+            }
           }
-        }
 
-        //If a range min and max was given, or if a min and max value was given,
-        // then this DateFilter should be presented as a date range (rather than
-       // an exact date value).
-        if( rangeMinNode.length || rangeMinNode.length || minNode || maxNode ){
-          //Set the range attribute on the JSON
-          modelJSON.range = true;
-        }
-        else{
-          //Set the range attribute on the JSON
-          modelJSON.range = false;
+          //If a range min and max was given, or if a min and max value was given,
+          // then this DateFilter should be presented as a date range (rather than
+          // an exact date value).
+          if (
+            rangeMinNode.length ||
+            rangeMinNode.length ||
+            minNode ||
+            maxNode
+          ) {
+            //Set the range attribute on the JSON
+            modelJSON.range = true;
+          } else {
+            //Set the range attribute on the JSON
+            modelJSON.range = false;
+          }
+        } catch (e) {
+          //If an error occured while parsing the XML, return a blank JS object
+          //(i.e. this model will just have the default values).
+          return {};
         }
-      }
-      catch(e){
-        //If an error occured while parsing the XML, return a blank JS object
-        //(i.e. this model will just have the default values).
-        return {};
-      }
-
-      return modelJSON;
-
-    },
-
-    /**
-     * Builds a query string that represents this filter.
-     *
-     * @return {string} The query string to send to Solr
-     */
-    getQuery: function(){
-
-      //Start the query string
-      var queryString = "";
-
-      //Only construct the query if the min or max is different than the default
-      if( ((this.get("min") != this.defaults().min) && (this.get("min") != this.get("rangeMin"))) ||
-           ((this.get("max") != this.defaults().max)) && (this.get("max") != this.get("rangeMax")) ){
-
-        //Iterate over each filter field and add to the query string
-        _.each(this.get("fields"), function(field, i, allFields){
 
-          //Add the date range for this field to the query string
-          queryString += field + ":" + this.getRangeQuery().replace(/ /g, "%20");
-
-          //If there is another field, add an operator
-          if( allFields[i+1] ){
-            queryString += "%20" + this.get("fieldsOperator") + "%20";
+        return modelJSON;
+      },
+
+      /**
+       * Builds a query string that represents this filter.
+       *
+       * @return {string} The query string to send to Solr
+       */
+      getQuery: function () {
+        //Start the query string
+        var queryString = "";
+
+        //Only construct the query if the min or max is different than the default
+        if (
+          (this.get("min") != this.defaults().min &&
+            this.get("min") != this.get("rangeMin")) ||
+          (this.get("max") != this.defaults().max &&
+            this.get("max") != this.get("rangeMax"))
+        ) {
+          //Iterate over each filter field and add to the query string
+          _.each(
+            this.get("fields"),
+            function (field, i, allFields) {
+              //Add the date range for this field to the query string
+              queryString +=
+                field + ":" + this.getRangeQuery().replace(/ /g, "%20");
+
+              //If there is another field, add an operator
+              if (allFields[i + 1]) {
+                queryString += "%20" + this.get("fieldsOperator") + "%20";
+              }
+            },
+            this,
+          );
+
+          //If there is more than one field, wrap the query in parenthesis
+          if (this.get("fields").length > 1) {
+            queryString = "(" + queryString + ")";
           }
-
-        }, this);
-
-        //If there is more than one field, wrap the query in parenthesis
-        if( this.get("fields").length > 1 ){
-          queryString = "(" + queryString + ")";
         }
 
-      }
-
-      return queryString;
-
-    },
+        return queryString;
+      },
 
-    /**
-    * Constructs a subquery string from the minimum and maximum dates.
-    * @return {string} - THe subquery string
-    */
-    getRangeQuery: function(){
-      //Get the minimum and maximum values
-      var max = this.get("max"),
+      /**
+       * Constructs a subquery string from the minimum and maximum dates.
+       * @return {string} - THe subquery string
+       */
+      getRangeQuery: function () {
+        //Get the minimum and maximum values
+        var max = this.get("max"),
           min = this.get("min");
 
-      //If no min or max was set, but there is a value, construct an exact value match query
-      if( !min && min !== 0 && !max && max !== 0 &&
-               (this.get("values")[0] || this.get("values")[0] === 0) ){
-        return this.get("values")[0];
-      }
-      //If there is no min or max or value, set an empty query string
-      else if( !min && min !== 0 && !max && max !== 0 &&
-               ( !this.get("values")[0] && this.get("values")[0] !== 0) ){
-         return "";
-      }
-      //If there is at least a min or max
-      else{
-        //If there's a min but no max, set the max to a wildcard (unbounded)
-        if( (min || min === 0) && !max ){
-          max = "*";
-        }
-        //If there's a max but no min, set the min to a wildcard (unbounded)
-        else if ( !min && min !== 0 && max ){
-          min = "*";
-        }
-        //If the max is higher than the min, set the max to a wildcard (unbounded)
-        else if( (max || max === 0) && (min || min === 0) && (max < min) ){
-          max = "*";
-        }
-
-        if(min != "*"){
-          min = min + "-01-01T00:00:00Z";
+        //If no min or max was set, but there is a value, construct an exact value match query
+        if (
+          !min &&
+          min !== 0 &&
+          !max &&
+          max !== 0 &&
+          (this.get("values")[0] || this.get("values")[0] === 0)
+        ) {
+          return this.get("values")[0];
         }
-        if(max != "*"){
-          max = max + "-12-31T23:59:59Z";
+        //If there is no min or max or value, set an empty query string
+        else if (
+          !min &&
+          min !== 0 &&
+          !max &&
+          max !== 0 &&
+          !this.get("values")[0] &&
+          this.get("values")[0] !== 0
+        ) {
+          return "";
         }
+        //If there is at least a min or max
+        else {
+          //If there's a min but no max, set the max to a wildcard (unbounded)
+          if ((min || min === 0) && !max) {
+            max = "*";
+          }
+          //If there's a max but no min, set the min to a wildcard (unbounded)
+          else if (!min && min !== 0 && max) {
+            min = "*";
+          }
+          //If the max is higher than the min, set the max to a wildcard (unbounded)
+          else if ((max || max === 0) && (min || min === 0) && max < min) {
+            max = "*";
+          }
 
-        //Add the range for this field to the query string
-        return "[" + min + "%20TO%20" + max + "]";
-      }
-
-    },
-
-    /**
-     * Updates the XML DOM with the new values from the model
-     *
-     *  @inheritdoc
-    */
-    updateDOM: function(options){
-
-      var objectDOM = Filter.prototype.updateDOM.call(this, options);
-
-      //Date Filters don't use matchSubstring nodes, and the value node will be recreated later
-      $(objectDOM).children("matchSubstring, value").remove();
-
-      //Get a clone of the original DOM
-      var originalDOM;
-      if( this.get("objectDOM") ){
-        originalDOM = this.get("objectDOM").cloneNode(true);
-      }
-
-      if( typeof options == "undefined" ){
-        var options = {};
-      }
-
-      // Get min and max dates
-      var dateData = {
-        min: this.get("min"),
-        max: this.get("max"),
-        value: this.get("values") ? this.get("values")[0] : null
-      };
-
-      var isRange = false;
-
-      // Make subnodes <min> and <max> and append to DOM
-      _.map(dateData, function(value, nodeName){
-        
-        // dateFilters don't have a min or max when the values should range from
-        // a min to infinity, or from a max to infinity (e.g. "date is before...") 
-        if(!value){
-          return
-        }
+          if (min != "*") {
+            min = min + "-01-01T00:00:00Z";
+          }
+          if (max != "*") {
+            max = max + "-12-31T23:59:59Z";
+          }
 
-        if( nodeName == "min" ){
-          var dateTime = "-01-01T00:00:00Z";
+          //Add the range for this field to the query string
+          return "[" + min + "%20TO%20" + max + "]";
         }
-        else{
-          var dateTime = "-12-31T23:59:59Z";
+      },
+
+      /**
+       * Updates the XML DOM with the new values from the model
+       *
+       *  @inheritdoc
+       */
+      updateDOM: function (options) {
+        var objectDOM = Filter.prototype.updateDOM.call(this, options);
+
+        //Date Filters don't use matchSubstring nodes, and the value node will be recreated later
+        $(objectDOM).children("matchSubstring, value").remove();
+
+        //Get a clone of the original DOM
+        var originalDOM;
+        if (this.get("objectDOM")) {
+          originalDOM = this.get("objectDOM").cloneNode(true);
         }
 
-        //If this value is the same as the default value, but it wasn't previously serialized,
-        if( (value == this.defaults()[nodeName]) &&
-            ( !$(originalDOM).children(nodeName).length ||
-              ($(originalDOM).children(nodeName).text() != value + dateTime) )){
-            return;
+        if (typeof options == "undefined") {
+          var options = {};
         }
 
-        //Create an XML node
-        var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
-
-        //Add the date string to the XML node
-        $(nodeSerialized).text( value + dateTime );
-
-        $(objectDOM).append(nodeSerialized);
-
-        //If either a min or max was serialized, then mark this as a range
-        isRange = true;
-
-      }, this);
-
-      //If a value is set on this model,
-      if( !isRange && this.get("values").length ){
-
-        //Create a value XML node
-        var valueNode = $(objectDOM.ownerDocument.createElement("value"));
-        //Get a Date object for this value
-        var date = new Date();
-        date.setUTCFullYear(this.get("values")[0] + "-12-31T23:59:59Z");
-        //Set the text of the XML node to the date string
-        valueNode.text( date.toISOString() );
-        $(objectDOM).append( valueNode );
-
-      }
-
-
-      if( this.get("isUIFilterType") ){
-
-        // Get new date data
+        // Get min and max dates
         var dateData = {
-          rangeMin: this.get("rangeMin"),
-          rangeMax: this.get("rangeMax")
+          min: this.get("min"),
+          max: this.get("max"),
+          value: this.get("values") ? this.get("values")[0] : null,
         };
 
-        // Make subnodes <min> and <max> and append to DOM
-        _.map(dateData, function(value, nodeName){
+        var isRange = false;
 
-          if( nodeName == "rangeMin" ){
-            var dateTime = "-01-01T00:00:00Z";
-          }
-          else{
-            var dateTime = "-12-31T23:59:59Z";
-          }
-
-          //If this value is the same as the default value, but it wasn't previously serialized,
-          if( (value == this.defaults()[nodeName]) &&
-              ( !$(originalDOM).children(nodeName).length ||
-                ($(originalDOM).children(nodeName).text() != value + dateTime) )){
+        // Make subnodes <min> and <max> and append to DOM
+        _.map(
+          dateData,
+          function (value, nodeName) {
+            // dateFilters don't have a min or max when the values should range from
+            // a min to infinity, or from a max to infinity (e.g. "date is before...")
+            if (!value) {
               return;
-          }
-
-          //Create an XML node
-          var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
+            }
+
+            if (nodeName == "min") {
+              var dateTime = "-01-01T00:00:00Z";
+            } else {
+              var dateTime = "-12-31T23:59:59Z";
+            }
+
+            //If this value is the same as the default value, but it wasn't previously serialized,
+            if (
+              value == this.defaults()[nodeName] &&
+              (!$(originalDOM).children(nodeName).length ||
+                $(originalDOM).children(nodeName).text() != value + dateTime)
+            ) {
+              return;
+            }
+
+            //Create an XML node
+            var nodeSerialized =
+              objectDOM.ownerDocument.createElement(nodeName);
+
+            //Add the date string to the XML node
+            $(nodeSerialized).text(value + dateTime);
+
+            $(objectDOM).append(nodeSerialized);
+
+            //If either a min or max was serialized, then mark this as a range
+            isRange = true;
+          },
+          this,
+        );
+
+        //If a value is set on this model,
+        if (!isRange && this.get("values").length) {
+          //Create a value XML node
+          var valueNode = $(objectDOM.ownerDocument.createElement("value"));
+          //Get a Date object for this value
+          var date = new Date();
+          date.setUTCFullYear(this.get("values")[0] + "-12-31T23:59:59Z");
+          //Set the text of the XML node to the date string
+          valueNode.text(date.toISOString());
+          $(objectDOM).append(valueNode);
+        }
 
-          //Add the date string to the XML node
-          $(nodeSerialized).text( value + dateTime );
+        if (this.get("isUIFilterType")) {
+          // Get new date data
+          var dateData = {
+            rangeMin: this.get("rangeMin"),
+            rangeMax: this.get("rangeMax"),
+          };
+
+          // Make subnodes <min> and <max> and append to DOM
+          _.map(
+            dateData,
+            function (value, nodeName) {
+              if (nodeName == "rangeMin") {
+                var dateTime = "-01-01T00:00:00Z";
+              } else {
+                var dateTime = "-12-31T23:59:59Z";
+              }
+
+              //If this value is the same as the default value, but it wasn't previously serialized,
+              if (
+                value == this.defaults()[nodeName] &&
+                (!$(originalDOM).children(nodeName).length ||
+                  $(originalDOM).children(nodeName).text() != value + dateTime)
+              ) {
+                return;
+              }
+
+              //Create an XML node
+              var nodeSerialized =
+                objectDOM.ownerDocument.createElement(nodeName);
+
+              //Add the date string to the XML node
+              $(nodeSerialized).text(value + dateTime);
+
+              //Remove existing nodes and add the new one
+              $(objectDOM).children(nodeName).remove();
+              $(objectDOM).append(nodeSerialized);
+            },
+            this,
+          );
+
+          //Move the filterOptions node to the end of the filter node
+          var filterOptionsNode = $(objectDOM).find("filterOptions");
+          filterOptionsNode.detach();
+          $(objectDOM).append(filterOptionsNode);
+        }
+        //Remove filterOptions for Date filters in collection definitions
+        else {
+          $(objectDOM).find("filterOptions").remove();
+        }
 
-          //Remove existing nodes and add the new one
-          $(objectDOM).children(nodeName).remove();
-          $(objectDOM).append(nodeSerialized);
+        return objectDOM;
+      },
 
-        }, this);
+      /**
+       * Creates a human-readable string that represents the value set on this model
+       * @return {string}
+       */
+      getReadableValue: function () {
+        var readableValue = "";
 
-        //Move the filterOptions node to the end of the filter node
-        var filterOptionsNode = $(objectDOM).find("filterOptions");
-        filterOptionsNode.detach();
-        $(objectDOM).append(filterOptionsNode);
-      }
-      //Remove filterOptions for Date filters in collection definitions
-      else{
-        $(objectDOM).find("filterOptions").remove();
-      }
+        var min = this.get("min"),
+          max = this.get("max"),
+          value = this.get("values")[0];
 
-      return objectDOM;
-    },
+        if (!value && value !== 0) {
+          //If there is a min and max
+          if ((min || min === 0) && (max || max === 0)) {
+            readableValue = min + " to " + max;
+          }
+          //If there is only a max
+          else if (max || max === 0) {
+            readableValue = "Before " + max;
+          } else {
+            readableValue = "After " + min;
+          }
+        } else {
+          readableValue = value;
+        }
 
-    /**
-    * Creates a human-readable string that represents the value set on this model
-    * @return {string}
-    */
-    getReadableValue: function(){
+        return readableValue;
+      },
+
+      /**
+       * @inheritdoc
+       */
+      hasChangedValues: function () {
+        return (
+          (this.get("min") > this.get("rangeMin") &&
+            this.get("min") !== this.defaults().min) ||
+          (this.get("max") < this.get("rangeMax") &&
+            this.get("max") !== this.defaults().max)
+        );
+      },
+
+      /**
+       * Checks if the values set on this model are valid and expected
+       * @return {object} - Returns a literal object with the invalid attributes and their corresponding error message
+       */
+      validate: function () {
+        //Validate most of the DateFilter attributes using the parent validate function
+        var errors = Filter.prototype.validate.call(this);
+
+        //If everything is valid so far, then we have to create a new object to store errors
+        if (typeof errors != "object") {
+          errors = {};
+        }
 
-      var readableValue = "";
+        //Delete error messages for the attributes that are going to be validated specially for the DateFilter
+        delete errors.values;
+        delete errors.min;
+        delete errors.max;
 
-      var min = this.get("min"),
-          max = this.get("max"),
-          value = this.get("values")[0];
+        // Check that there is a rangeMin and a rangeMax. If there isn't, then just set to
+        // the default rather than creating an error.
+        if (!this.get("rangeMin") && this.get("rangeMin") !== 0) {
+          this.set("rangeMin", this.defaults().rangeMin);
+        }
+        if (!this.get("rangeMax") && this.get("rangeMax") !== 0) {
+          this.set("rangeMax", this.defaults().rangeMax);
+        }
 
-      if( !value && value !== 0 ){
-        //If there is a min and max
-        if( (min || min === 0) && (max || max === 0) ){
-          readableValue = min + " to " + max;
+        //Check that there aren't any negative numbers
+        if (this.get("min") < 0) {
+          errors.min = "The minimum year cannot be a negative number.";
         }
-        //If there is only a max
-        else if(max || max === 0){
-          readableValue = "Before " + max;
+        if (this.get("max") < 0) {
+          errors.max = "The maximum year cannot be a negative number.";
         }
-        else{
-          readableValue = "After " + min;
+        if (this.get("rangeMin") < 0) {
+          errors.rangeMin =
+            "The range minimum year cannot be a negative number.";
+        }
+        if (this.get("rangeMax") < 0) {
+          errors.rangeMax =
+            "The range maximum year cannot be a negative number.";
         }
-      }
-      else{
-        readableValue = value;
-      }
-
-      return readableValue;
 
-    },
+        //Check that the min and max values are in order, if the minimum is not the default value of 0
+        if (this.get("min") > this.get("max") && this.get("min") != 0) {
+          errors.min =
+            "The minimum year is after the maximum year. The minimum year must be a year before the maximum year of " +
+            this.get("max");
+        }
 
-    /**
-    * @inheritdoc
-    */
-    hasChangedValues: function(){
+        //Check that all the values are numbers
+        if (!errors.min && typeof this.get("min") != "number") {
+          errors.min = "The minimum year must be a number.";
+        }
+        if (!errors.max && typeof this.get("max") != "number") {
+          errors.max = "The maximum year must be a number.";
+        }
+        if (!errors.rangeMax && typeof this.get("rangeMax") != "number") {
+          errors.rangeMax =
+            "The maximum year in the date slider must be a number.";
+        }
 
-      return ((this.get("min") > this.get("rangeMin") && this.get("min") !== this.defaults().min) ||
-              (this.get("max") < this.get("rangeMax") && this.get("max") !== this.defaults().max))
+        if (!errors.rangeMin && typeof this.get("rangeMin") != "number") {
+          errors.rangeMin =
+            "The minimum year in the date slider must be a number.";
+        }
 
+        if (Object.keys(errors).length) return errors;
+        else {
+          return;
+        }
+      },
     },
-
-    /**
-    * Checks if the values set on this model are valid and expected
-    * @return {object} - Returns a literal object with the invalid attributes and their corresponding error message
-    */
-    validate: function(){
-
-      //Validate most of the DateFilter attributes using the parent validate function
-      var errors = Filter.prototype.validate.call(this);
-
-      //If everything is valid so far, then we have to create a new object to store errors
-      if (typeof errors != "object") {
-        errors = {};
-      }
-
-      //Delete error messages for the attributes that are going to be validated specially for the DateFilter
-      delete errors.values;
-      delete errors.min;
-      delete errors.max;
-
-      // Check that there is a rangeMin and a rangeMax. If there isn't, then just set to
-      // the default rather than creating an error.
-      if (!this.get("rangeMin") && this.get("rangeMin") !== 0) {
-        this.set("rangeMin", this.defaults().rangeMin)
-      }
-      if (!this.get("rangeMax") && this.get("rangeMax") !== 0) {
-        this.set("rangeMax", this.defaults().rangeMax)
-      }
-
-      //Check that there aren't any negative numbers
-      if( this.get("min") < 0 ){
-        errors.min = "The minimum year cannot be a negative number."
-      }
-      if( this.get("max") < 0 ){
-        errors.max = "The maximum year cannot be a negative number."
-      }
-      if( this.get("rangeMin") < 0 ){
-        errors.rangeMin = "The range minimum year cannot be a negative number."
-      }
-      if( this.get("rangeMax") < 0 ){
-        errors.rangeMax = "The range maximum year cannot be a negative number."
-      }
-
-      //Check that the min and max values are in order, if the minimum is not the default value of 0
-      if( this.get("min") > this.get("max") && this.get("min") != 0 ){
-        errors.min = "The minimum year is after the maximum year. The minimum year must be a year before the maximum year of " + this.get("max");
-      }
-
-      //Check that all the values are numbers
-      if( !errors.min && typeof this.get("min") != "number" ){
-        errors.min = "The minimum year must be a number.";
-      }
-      if( !errors.max && typeof this.get("max") != "number" ){
-        errors.max = "The maximum year must be a number.";
-      }
-      if( !errors.rangeMax && typeof this.get("rangeMax") != "number" ){
-        errors.rangeMax = "The maximum year in the date slider must be a number.";
-      }
-      
-      if( !errors.rangeMin && typeof this.get("rangeMin") != "number" ){
-        errors.rangeMin = "The minimum year in the date slider must be a number.";
-      }
-
-      if (Object.keys(errors).length)
-        return errors;
-      else {
-        return;
-      }
-
-    }
-
-  });
+  );
 
   return DateFilter;
 });
diff --git a/docs/docs/src_js_models_filters_Filter.js.html b/docs/docs/src_js_models_filters_Filter.js.html
index 810ecaeb3..97b3c98d3 100644
--- a/docs/docs/src_js_models_filters_Filter.js.html
+++ b/docs/docs/src_js_models_filters_Filter.js.html
@@ -44,1013 +44,1057 @@ 

Source: src/js/models/filters/Filter.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone'],
-    function($, _, Backbone) {
-
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
   /**
-  * @class Filter
-  * @classdesc A single search filter that is used in queries sent to the DataONE search service.
-  * @classcategory Models/Filters
-  * @extends Backbone.Model
-  * @constructs
-  */
+   * @class Filter
+   * @classdesc A single search filter that is used in queries sent to the DataONE search service.
+   * @classcategory Models/Filters
+   * @extends Backbone.Model
+   * @constructs
+   */
   var FilterModel = Backbone.Model.extend(
     /** @lends Filter.prototype */
     {
+      /**
+       * The name of this Model
+       * @name Filter#type
+       * @type {string}
+       * @readonly
+       */
+      type: "Filter",
+
+      /**
+       * Default attributes for this model
+       * @type {object}
+       * @returns {object}
+       * @property {Element} objectDOM - The XML DOM for this filter
+       * @property {string} nodeName - The XML node name for this filter's XML DOM
+       * @property {string[]} fields - The search index fields to search
+       * @property {string[]} values - The values to search for in the given search fields
+       * @property {object} valueLabels - Optional human-readable labels for the elements of
+       * values. Keys are the value and the human-readable label is the value at that key.
+       * @property {string} operator - The operator to use between values set on this model.
+       * "AND" or "OR"
+       * @property {string} fieldsOperator - The operator to use between fields set on this
+       * model. "AND" or "OR"
+       * @property {string} queryGroup - Deprecated: Add this filter along with other the
+       * other associated query group filters to a FilterGroup model instead. Old definition:
+       * The name of the group this Filter is a part of, which is primarily used when
+       * creating a query string from multiple Filter models. Filters in the same group will
+       * be wrapped in parenthesis in the query.
+       * @property {boolean} exclude - If true, search index docs matching this filter will
+       * be excluded from the search results
+       * @property {boolean} matchSubstring - If true, the search values will be wrapped in
+       * wildcard characters to match substrings
+       * @property {string} label - A human-readable short label for this Filter
+       * @property {string} placeholder - A short example or description of this Filter
+       * @property {string} icon - A term that identifies a single icon in a supported icon
+       * library
+       * @property {string} description - A longer description of this Filter's function
+       * @property {boolean} isInvisible - If true, this filter will be added to the query
+       * but will act in the "background", like a default filter
+       * @property {boolean} inFilterGroup - Deprecated: use isUIFilterType instead.
+       * @property {boolean} isUIFilterType - If true, this filter is one of the
+       * UIFilterTypes, belongs to a UIFilterGroupType model, and is used to create a custom
+       * Portal search filters. This changes how the XML is parsed and how the model is
+       * validated and serialized.
+       */
+      defaults: function () {
+        return {
+          objectDOM: null,
+          nodeName: "filter",
+          fields: [],
+          values: [],
+          valueLabels: {},
+          operator: "AND",
+          fieldsOperator: "AND",
+          exclude: false,
+          matchSubstring: false,
+          label: null,
+          placeholder: null,
+          icon: null,
+          description: null,
+          isInvisible: false,
+          isUIFilterType: false,
+        };
+      },
 
-    /**
-    * The name of this Model
-    * @name Filter#type
-    * @type {string}
-    * @readonly
-    */
-    type: "Filter",
-
-    /**
-    * Default attributes for this model
-    * @type {object}
-    * @returns {object}
-    * @property {Element} objectDOM - The XML DOM for this filter
-    * @property {string} nodeName - The XML node name for this filter's XML DOM
-    * @property {string[]} fields - The search index fields to search
-    * @property {string[]} values - The values to search for in the given search fields
-    * @property {object} valueLabels - Optional human-readable labels for the elements of
-    * values. Keys are the value and the human-readable label is the value at that key.
-    * @property {string} operator - The operator to use between values set on this model.
-    * "AND" or "OR"
-    * @property {string} fieldsOperator - The operator to use between fields set on this
-    * model. "AND" or "OR"
-    * @property {string} queryGroup - Deprecated: Add this filter along with other the
-    * other associated query group filters to a FilterGroup model instead. Old definition:
-    * The name of the group this Filter is a part of, which is primarily used when
-    * creating a query string from multiple Filter models. Filters in the same group will
-    * be wrapped in parenthesis in the query.
-    * @property {boolean} exclude - If true, search index docs matching this filter will
-    * be excluded from the search results
-    * @property {boolean} matchSubstring - If true, the search values will be wrapped in
-    * wildcard characters to match substrings
-    * @property {string} label - A human-readable short label for this Filter
-    * @property {string} placeholder - A short example or description of this Filter
-    * @property {string} icon - A term that identifies a single icon in a supported icon
-    * library
-    * @property {string} description - A longer description of this Filter's function
-    * @property {boolean} isInvisible - If true, this filter will be added to the query
-    * but will act in the "background", like a default filter
-    * @property {boolean} inFilterGroup - Deprecated: use isUIFilterType instead.
-    * @property {boolean} isUIFilterType - If true, this filter is one of the
-    * UIFilterTypes, belongs to a UIFilterGroupType model, and is used to create a custom
-    * Portal search filters. This changes how the XML is parsed and how the model is
-    * validated and serialized.
-    */
-    defaults: function(){
-      return{
-        objectDOM: null,
-        nodeName: "filter",
-        fields: [],
-        values: [],
-        valueLabels: {},
-        operator: "AND",
-        fieldsOperator: "AND",
-        exclude: false,
-        matchSubstring: false,
-        label: null,
-        placeholder: null,
-        icon: null,
-        description: null,
-        isInvisible: false,
-        isUIFilterType: false
-      }
-    },
-
-    /**
-    * Creates a new Filter model
-    */
-    initialize: function(attributes){
-
-      if( this.get("objectDOM") ){
-        this.set( this.parse(this.get("objectDOM")) );
-      }
-
-      if (attributes && attributes.isUIFilterType){
-        this.set("isUIFilterType", true)
-      }
-
-      //If this is an isPartOf filter, then add a label and description. Make it invisible
-      //depending on how MetacatUI is configured.
-      if( this.get("fields").length == 1 && this.get("fields").includes("isPartOf") ){
-        this.set({
-          label: "Datasets added manually",
-          description: "Datasets added to this collection manually by dataset owners",
-          isInvisible: MetacatUI.appModel.get("hideIsPartOfFilter") === true ? true : false,
-        });
-      }
-
-      // Operator must be AND or OR
-      ["fieldsOperator", "operator"].forEach(function(op){
-        if( !["AND", "OR"].includes(this.get(op)) ){
-          // Set the value to the default
-          this.set(op, this.defaults()[op])
+      /**
+       * Creates a new Filter model
+       */
+      initialize: function (attributes) {
+        if (this.get("objectDOM")) {
+          this.set(this.parse(this.get("objectDOM")));
         }
-      }, this);
 
-    },
-
-    /**
-    * Parses the given XML node into a JSON object to be set on the model
-    *
-    * @param {Element} xml - The XML element that contains all the filter elements
-    * @return {JSON} - The JSON object of all the filter attributes
-    */
-    parse: function(xml){
-
-      //If an XML element wasn't sent as a parameter, get it from the model
-      if(!xml){
-        var xml = this.get("objectDOM");
-
-        //Return an empty JSON object if there is no objectDOM saved in the model
-        if(!xml)
-          return {};
-      }
-
-      var modelJSON = {};
-
-      if( $(xml).children("field").length ){
-        //Parse the field(s)
-        modelJSON.fields = this.parseTextNode(xml, "field", true);
-      }
-
-      if( $(xml).children("label").length ){
-        //Parse the label
-        modelJSON.label = this.parseTextNode(xml, "label");
-      }
-
-      // Check if this filter contains one of the Id fields - we use OR by default for the
-      // operator for these fields.
-      var idFields = MetacatUI.appModel.get("queryIdentifierFields");
-      var isIdFilter = false;
-      if(modelJSON.fields){
-        isIdFilter = _.some( idFields, function(idField) {
-          return modelJSON.fields.includes(idField)
-        });
-      }
-
-      //Parse the operators, if they exist
-      if( $(xml).find("operator").length ){
-        modelJSON.operator = this.parseTextNode(xml, "operator");
-      }
-      else{
-        if( isIdFilter ){
-          modelJSON.operator = "OR";
-        }
-      }
-
-      if( $(xml).find("fieldsOperator").length ){
-        modelJSON.fieldsOperator = this.parseTextNode(xml, "fieldsOperator");
-      }
-      else{
-        if( isIdFilter ){
-          modelJSON.fieldsOperator = "OR";
+        if (attributes && attributes.isUIFilterType) {
+          this.set("isUIFilterType", true);
         }
-      }
-
-      //Parse the exclude, if it exists
-      if( $(xml).find("exclude").length ){
-        modelJSON.exclude = (this.parseTextNode(xml, "exclude") === "true")? true : false;
-      }
-
-      //Parse the matchSubstring
-      if( $(xml).find("matchSubstring").length ){
-        modelJSON.matchSubstring = (this.parseTextNode(xml, "matchSubstring") === "true")? true : false;
-      }
-
-      var filterOptionsNode = $(xml).children("filterOptions");
-      if( filterOptionsNode.length ){
-        //Parse the filterOptions XML node
-        modelJSON = _.extend(this.parseFilterOptions(filterOptionsNode), modelJSON);
-      }
-
-      //If this Filter is in a filter group, don't parse the values
-      if( !this.get("isUIFilterType") ){
-        if( $(xml).children("value").length ){
-          //Parse the value(s)
-          modelJSON.values = this.parseTextNode(xml, "value", true);
-        }
-      }
-
-      return modelJSON;
-
-    },
-
-    /**
-    * Gets the text content of the XML node matching the given node name
-    *
-    * @param {Element} parentNode - The parent node to select from
-    * @param {string} nodeName - The name of the XML node to parse
-    * @param {boolean} isMultiple - If true, parses the nodes into an array
-    * @return {(string|Array)} - Returns a string or array of strings of the text content
-    */
-    parseTextNode: function( parentNode, nodeName, isMultiple ){
-      var node = $(parentNode).children(nodeName);
-
-      //If no matching nodes were found, return falsey values
-      if( !node || !node.length ){
-
-        //Return an empty array if the isMultiple flag is true
-        if( isMultiple )
-          return [];
-        //Return null if the isMultiple flag is false
-        else
-          return null;
-      }
-      //If exactly one node is found and we are only expecting one, return the text content
-      else if( node.length == 1 && !isMultiple ){
-        if( !node[0].textContent )
-          return null;
-        else
-          return node[0].textContent;
-      }
-      //If more than one node is found, parse into an array
-      else{
-
-        var allContents = [];
-
-         _.each(node, function(node){
-           if(node.textContent || node.textContent === 0)
-             allContents.push( node.textContent );
-        });
-
-        return allContents;
 
-      }
-    },
-
-    /**
-    * Parses the filterOptions XML node into a literal object
-    * @param {Element} filterOptionsNode - The filterOptions XML element to parse
-    * @return {Object} - The parsed filter options, in literal object form
-    */
-    parseFilterOptions: function(filterOptionsNode){
-
-      if( typeof filterOptionsNode == "undefined" ){
-        return {};
-      }
-
-      var modelJSON = {};
-
-      try{
-        //The list of options to parse
-        var options = ["placeholder", "icon", "description"];
+        //If this is an isPartOf filter, then add a label and description. Make it invisible
+        //depending on how MetacatUI is configured.
+        if (
+          this.get("fields").length == 1 &&
+          this.get("fields").includes("isPartOf")
+        ) {
+          this.set({
+            label: "Datasets added manually",
+            description:
+              "Datasets added to this collection manually by dataset owners",
+            isInvisible:
+              MetacatUI.appModel.get("hideIsPartOfFilter") === true
+                ? true
+                : false,
+          });
+        }
 
-        //Parse the text nodes for each filter option
-        _.each(options, function(option){
-          if( $(filterOptionsNode).children(option).length ){
-            modelJSON[option] = this.parseTextNode(filterOptionsNode, option, false);
+        // Operator must be AND or OR
+        ["fieldsOperator", "operator"].forEach(function (op) {
+          if (!["AND", "OR"].includes(this.get(op))) {
+            // Set the value to the default
+            this.set(op, this.defaults()[op]);
           }
         }, this);
+      },
 
-        //Parse the generic option name and value pairs and set on the model JSON
-        $(filterOptionsNode).children("option").each(function(i, optionNode){
-          var optName = $(optionNode).children("optionName").text();
-          var optValue = $(optionNode).children("optionValue").text();
+      /**
+       * Parses the given XML node into a JSON object to be set on the model
+       *
+       * @param {Element} xml - The XML element that contains all the filter elements
+       * @return {JSON} - The JSON object of all the filter attributes
+       */
+      parse: function (xml) {
+        //If an XML element wasn't sent as a parameter, get it from the model
+        if (!xml) {
+          var xml = this.get("objectDOM");
+
+          //Return an empty JSON object if there is no objectDOM saved in the model
+          if (!xml) return {};
+        }
 
-          modelJSON[optName] = optValue;
-        });
+        var modelJSON = {};
 
-        //Return the JSON to be set on this model
-        return modelJSON;
+        if ($(xml).children("field").length) {
+          //Parse the field(s)
+          modelJSON.fields = this.parseTextNode(xml, "field", true);
+        }
 
-      }
-      catch(e){
-        return {};
-      }
+        if ($(xml).children("label").length) {
+          //Parse the label
+          modelJSON.label = this.parseTextNode(xml, "label");
+        }
 
-    },
+        // Check if this filter contains one of the Id fields - we use OR by default for the
+        // operator for these fields.
+        var idFields = MetacatUI.appModel.get("queryIdentifierFields");
+        var isIdFilter = false;
+        if (modelJSON.fields) {
+          isIdFilter = _.some(idFields, function (idField) {
+            return modelJSON.fields.includes(idField);
+          });
+        }
 
-    /**
-     * Builds a query string that represents this filter.
-     *
-     * @return {string} The query string to send to Solr
-     * @param {string} [groupLevelOperator] - "AND" or "OR". The operator used in the
-     * parent Filters collection to combine the filter query fragments together. If the
-     * group level operator is "OR" and this filter has exclude set to TRUE, then a
-     * positive clause is added.
-     */
-    getQuery: function(groupLevelOperator){
-
-      //Get the values of this filter in Array format
-      var values = this.get("values");
-      if( !Array.isArray(values) ){
-        values = [values];
-      }
-
-      //Check that there are actually values to serialize
-      if( !values.length ){
-        return "";
-      }
-
-      //Filter out any invalid values (can't use _.compact() because we want to keep 'false' values)
-      values = _.reject(values, function(value){
-                return (value === null || typeof value == "undefined" ||
-                        value === NaN || value === "" || (Array.isArray(value) && !value.length));
-              });
-
-      if(!values.length){
-        return "";
-      }
-
-      //Start a query string for this model and get the fields
-      var queryString = "",
-          fields = this.get("fields");
+        //Parse the operators, if they exist
+        if ($(xml).find("operator").length) {
+          modelJSON.operator = this.parseTextNode(xml, "operator");
+        } else {
+          if (isIdFilter) {
+            modelJSON.operator = "OR";
+          }
+        }
 
-      //If the fields are not an array, convert it to an array
-      if( !Array.isArray(fields) ){
-        fields = [fields];
-      }
+        if ($(xml).find("fieldsOperator").length) {
+          modelJSON.fieldsOperator = this.parseTextNode(xml, "fieldsOperator");
+        } else {
+          if (isIdFilter) {
+            modelJSON.fieldsOperator = "OR";
+          }
+        }
 
-      //Iterate over each field
-      _.each( fields, function(field, i){
+        //Parse the exclude, if it exists
+        if ($(xml).find("exclude").length) {
+          modelJSON.exclude =
+            this.parseTextNode(xml, "exclude") === "true" ? true : false;
+        }
 
-        //Add the query string for this field to the overall model query string
-        queryString += field + ":" + this.getValueQuerySubstring(values);
+        //Parse the matchSubstring
+        if ($(xml).find("matchSubstring").length) {
+          modelJSON.matchSubstring =
+            this.parseTextNode(xml, "matchSubstring") === "true" ? true : false;
+        }
 
-        //Add the OR operator between field names
-        if( fields.length > i+1 && queryString.length ){
-          queryString += "%20" + this.get("fieldsOperator") + "%20";
+        var filterOptionsNode = $(xml).children("filterOptions");
+        if (filterOptionsNode.length) {
+          //Parse the filterOptions XML node
+          modelJSON = _.extend(
+            this.parseFilterOptions(filterOptionsNode),
+            modelJSON,
+          );
         }
 
-      }, this);
-
-      //If there is more than one field, wrap the multiple fields in parenthesis
-      if( fields.length > 1 ){
-        queryString = "(" + queryString + ")"
-      }
-
-      //If this filter should be excluding matches from the results,
-      // then add a hyphen in front
-      if( queryString && this.get("exclude") ){
-        queryString = "-" + queryString;
-        if (this.requiresPositiveClause(groupLevelOperator)){
-          queryString = queryString + "%20AND%20*:*";
-          if (groupLevelOperator && groupLevelOperator === "OR"){
-            queryString = "(" + queryString + ")"
+        //If this Filter is in a filter group, don't parse the values
+        if (!this.get("isUIFilterType")) {
+          if ($(xml).children("value").length) {
+            //Parse the value(s)
+            modelJSON.values = this.parseTextNode(xml, "value", true);
           }
         }
-      }
-
-      return queryString;
 
-    },
+        return modelJSON;
+      },
 
-    /**
-     * For "negative" Filters (filter models where exclude is set to true), detects
-     * whether the query requires an additional "positive" query phrase in order to avoid
-     * the problem of pure negative queries returning zero results. If this Filter is not
-     * part of a collection of Filters, assume it needs the positive clause. If this
-     * Filter is part of a collection of Filters, detect whether there are other,
-     * "positive" filters in the same query (i.e. filter models where exclude is set to
-     * false). If there are other positive queries, then an additional clause is not
-     * required. If the Filter is part of a pure negative query, but it is not the last
-     * filter, then don't add a clause since it will be added to the last, and only one
-     * is required. When looking for other positive and negative filters, exclude empty
-     * filters and filters that use any of the identifier fields, as these are appended to
-     * the end of the query.
-     * @see {@link https://github.com/NCEAS/metacatui/issues/1600}
-     * @see {@link https://cwiki.apache.org/confluence/display/SOLR/NegativeQueryProblems}
-     * @param {string} [groupLevelOperator] - "AND" or "OR". The operator used in the
-     * parent Filters collection to combine the filter query fragments together. If the
-     * group level operator is "OR" and this filter has exclude set to TRUE, then a
-     * positive clause is required.
-     * @return {boolean} returns true of this Filter needs a positive clause, false
-     * otherwise
-     */
-    requiresPositiveClause: function (groupLevelOperator){
-
-      try {
-
-        // Only negative queries require the additional clause
-        if(this.get("exclude") == false ){
-          return false
+      /**
+       * Gets the text content of the XML node matching the given node name
+       *
+       * @param {Element} parentNode - The parent node to select from
+       * @param {string} nodeName - The name of the XML node to parse
+       * @param {boolean} isMultiple - If true, parses the nodes into an array
+       * @return {(string|Array)} - Returns a string or array of strings of the text content
+       */
+      parseTextNode: function (parentNode, nodeName, isMultiple) {
+        var node = $(parentNode).children(nodeName);
+
+        //If no matching nodes were found, return falsey values
+        if (!node || !node.length) {
+          //Return an empty array if the isMultiple flag is true
+          if (isMultiple) return [];
+          //Return null if the isMultiple flag is false
+          else return null;
         }
-        // If this Filter is not part of a collection of Filters, assume it needs the
-        // positive clause.
-        if(!this.collection){
-          return true
+        //If exactly one node is found and we are only expecting one, return the text content
+        else if (node.length == 1 && !isMultiple) {
+          if (!node[0].textContent) return null;
+          else return node[0].textContent;
         }
-        // If this Filter is the only one in the group, assume it needs a positive clause
-        if(this.collection.length === 1){
-          return true
-        }
-        // If this filter is being "OR"'ed together with other filters, then assume it
-        // needs the additional clause.
-        if (groupLevelOperator && groupLevelOperator === "OR"){
-          return true
+        //If more than one node is found, parse into an array
+        else {
+          var allContents = [];
+
+          _.each(node, function (node) {
+            if (node.textContent || node.textContent === 0)
+              allContents.push(node.textContent);
+          });
+
+          return allContents;
         }
-        // Get all of the other filters in the same collection that are not ID filters.
-        // These filters are always appended to the end of the query as a separated group.
-        var nonIDFilters = this.collection.getNonIdFilters();
-        // Exclude filters that would give an empty query string (e.g. because value is
-        // missing)
-        var filters = _.reject(nonIDFilters, function(filterModel){
-          if(filterModel === this){
-            return false
-          }
-          return !filterModel.isValid()
-        })
+      },
 
-        // If at least one filter in the collection is positive (exclude = false), then we
-        // don't need to add anything
-        var positiveFilters = _.find(filters, function(filterModel){
-          return filterModel.get("exclude") != true;
-        });
-        if(positiveFilters){
-          return false
+      /**
+       * Parses the filterOptions XML node into a literal object
+       * @param {Element} filterOptionsNode - The filterOptions XML element to parse
+       * @return {Object} - The parsed filter options, in literal object form
+       */
+      parseFilterOptions: function (filterOptionsNode) {
+        if (typeof filterOptionsNode == "undefined") {
+          return {};
         }
-        // Assuming that all the non-ID filters are negative, check if this is the first
-        // last the list. Since we only need one additional positive query phrase to avoid
-        // the pure negative query problem, by convention, only add the positive phrase at
-        // the end of the filter group
-        if(this === _.last(filters)){
-          return true
-        } else {
-          return false
+
+        var modelJSON = {};
+
+        try {
+          //The list of options to parse
+          var options = ["placeholder", "icon", "description"];
+
+          //Parse the text nodes for each filter option
+          _.each(
+            options,
+            function (option) {
+              if ($(filterOptionsNode).children(option).length) {
+                modelJSON[option] = this.parseTextNode(
+                  filterOptionsNode,
+                  option,
+                  false,
+                );
+              }
+            },
+            this,
+          );
+
+          //Parse the generic option name and value pairs and set on the model JSON
+          $(filterOptionsNode)
+            .children("option")
+            .each(function (i, optionNode) {
+              var optName = $(optionNode).children("optionName").text();
+              var optValue = $(optionNode).children("optionValue").text();
+
+              modelJSON[optName] = optValue;
+            });
+
+          //Return the JSON to be set on this model
+          return modelJSON;
+        } catch (e) {
+          return {};
         }
-      } catch (error) {
-        console.log("There was a problem detecting whether a Filter required a positive" +
-        " clause. Assuming that it needs one. Error details: " + error
-        );
-        return true
-      }
-    },
+      },
 
-    /**
-    * Constructs a query substring for each of the values set on this model
-    *
-    * @example
-    *   values: ["walker", "jones"]
-    *   Returns: "(walker%20OR%20jones)"
-    *
-    * @param {string[]} [values] - The values to use in this query substring.
-    * If not provided, the values set on the model are used.
-    * @return {string} The query substring
-    */
-    getValueQuerySubstring: function(values){
-
-      //Start a query string for this field and get the values
-      var valuesQueryString = "",
-          values = values || this.get("values");
+      /**
+       * Builds a query string that represents this filter.
+       *
+       * @return {string} The query string to send to Solr
+       * @param {string} [groupLevelOperator] - "AND" or "OR". The operator used in the
+       * parent Filters collection to combine the filter query fragments together. If the
+       * group level operator is "OR" and this filter has exclude set to TRUE, then a
+       * positive clause is added.
+       */
+      getQuery: function (groupLevelOperator) {
+        //Get the values of this filter in Array format
+        var values = this.get("values");
+        if (!Array.isArray(values)) {
+          values = [values];
+        }
 
-      //If the values are not an array, convert it to an array
-      if( !Array.isArray(values) ){
-        values = [values];
-      }
+        //Check that there are actually values to serialize
+        if (!values.length) {
+          return "";
+        }
 
-      //Iterate over each value set on the model
-      _.each( values, function(value, i){
+        //Filter out any invalid values (can't use _.compact() because we want to keep 'false' values)
+        values = _.reject(values, function (value) {
+          return (
+            value === null ||
+            typeof value == "undefined" ||
+            value === NaN ||
+            value === "" ||
+            (Array.isArray(value) && !value.length)
+          );
+        });
 
-        //If the value is not a string, then convert it to a string
-        if( typeof value != "string" ){
-          value = value.toString();
+        if (!values.length) {
+          return "";
         }
 
-        //Trim off whitespace
-        value = value.trim();
-
-        var dateRangeRegEx = /^\[((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d*Z)|\*)( |%20)TO( |%20)((\d{4}-[01]\d-[0-3]\dT[0-2]\d(:|\\:)[0-5]\d(:|\\:)[0-5]\d\.\d*Z)|\*)\]/,
-            isDateRange = dateRangeRegEx.test(value),
-            isSearchPhrase = value.indexOf(" ") > -1,
-            isIdFilter = this.isIdFilter(),
-            //Test for ORCIDs and group subjects
-            isSubject = /^(?:https?:\/\/orcid\.org\/)?(?:\w{4}-){3}\w{4}$|^(?:CN=.{1,},DC=.{1,},DC=.{1,})$/i.test(value);
+        //Start a query string for this model and get the fields
+        var queryString = "",
+          fields = this.get("fields");
 
-        // Escape special characters
-        value = this.escapeSpecialChar(value);
+        //If the fields are not an array, convert it to an array
+        if (!Array.isArray(fields)) {
+          fields = [fields];
+        }
 
-        //URL encode the search value
-        value = encodeURIComponent(value);
+        //Iterate over each field
+        _.each(
+          fields,
+          function (field, i) {
+            //Add the query string for this field to the overall model query string
+            queryString += field + ":" + this.getValueQuerySubstring(values);
+
+            //Add the OR operator between field names
+            if (fields.length > i + 1 && queryString.length) {
+              queryString += "%20" + this.get("fieldsOperator") + "%20";
+            }
+          },
+          this,
+        );
 
-        // If the value is a search phrase (more than one word), is part of an ID filter,
-        // and not a date range string, wrap in quotes
-        if( (isSearchPhrase || isIdFilter || isSubject) && !isDateRange ){
-          value = "\"" + value + "\"";
+        //If there is more than one field, wrap the multiple fields in parenthesis
+        if (fields.length > 1) {
+          queryString = "(" + queryString + ")";
         }
 
-        if( this.get("matchSubstring") && !isDateRange ){
-          // Look for existing wildcard characters at the end of the value string, wrap
-          // the value string in wildcard characters if there aren't any yet.
-          if(! value.match( /^\*|\*$/ ) ){
-            value = "*" + value + "*"
+        //If this filter should be excluding matches from the results,
+        // then add a hyphen in front
+        if (queryString && this.get("exclude")) {
+          queryString = "-" + queryString;
+          if (this.requiresPositiveClause(groupLevelOperator)) {
+            queryString = queryString + "%20AND%20*:*";
+            if (groupLevelOperator && groupLevelOperator === "OR") {
+              queryString = "(" + queryString + ")";
+            }
           }
         }
 
-        // Add the value to the query string
-        valuesQueryString += value;
+        return queryString;
+      },
 
-        //Add the operator between values
-        if( values.length > i+1 && valuesQueryString.length ){
-          valuesQueryString += "%20" + this.get("operator") + "%20";
+      /**
+       * For "negative" Filters (filter models where exclude is set to true), detects
+       * whether the query requires an additional "positive" query phrase in order to avoid
+       * the problem of pure negative queries returning zero results. If this Filter is not
+       * part of a collection of Filters, assume it needs the positive clause. If this
+       * Filter is part of a collection of Filters, detect whether there are other,
+       * "positive" filters in the same query (i.e. filter models where exclude is set to
+       * false). If there are other positive queries, then an additional clause is not
+       * required. If the Filter is part of a pure negative query, but it is not the last
+       * filter, then don't add a clause since it will be added to the last, and only one
+       * is required. When looking for other positive and negative filters, exclude empty
+       * filters and filters that use any of the identifier fields, as these are appended to
+       * the end of the query.
+       * @see {@link https://github.com/NCEAS/metacatui/issues/1600}
+       * @see {@link https://cwiki.apache.org/confluence/display/SOLR/NegativeQueryProblems}
+       * @param {string} [groupLevelOperator] - "AND" or "OR". The operator used in the
+       * parent Filters collection to combine the filter query fragments together. If the
+       * group level operator is "OR" and this filter has exclude set to TRUE, then a
+       * positive clause is required.
+       * @return {boolean} returns true of this Filter needs a positive clause, false
+       * otherwise
+       */
+      requiresPositiveClause: function (groupLevelOperator) {
+        try {
+          // Only negative queries require the additional clause
+          if (this.get("exclude") == false) {
+            return false;
+          }
+          // If this Filter is not part of a collection of Filters, assume it needs the
+          // positive clause.
+          if (!this.collection) {
+            return true;
+          }
+          // If this Filter is the only one in the group, assume it needs a positive clause
+          if (this.collection.length === 1) {
+            return true;
+          }
+          // If this filter is being "OR"'ed together with other filters, then assume it
+          // needs the additional clause.
+          if (groupLevelOperator && groupLevelOperator === "OR") {
+            return true;
+          }
+          // Get all of the other filters in the same collection that are not ID filters.
+          // These filters are always appended to the end of the query as a separated group.
+          var nonIDFilters = this.collection.getNonIdFilters();
+          // Exclude filters that would give an empty query string (e.g. because value is
+          // missing)
+          var filters = _.reject(nonIDFilters, function (filterModel) {
+            if (filterModel === this) {
+              return false;
+            }
+            return !filterModel.isValid();
+          });
+
+          // If at least one filter in the collection is positive (exclude = false), then we
+          // don't need to add anything
+          var positiveFilters = _.find(filters, function (filterModel) {
+            return filterModel.get("exclude") != true;
+          });
+          if (positiveFilters) {
+            return false;
+          }
+          // Assuming that all the non-ID filters are negative, check if this is the first
+          // last the list. Since we only need one additional positive query phrase to avoid
+          // the pure negative query problem, by convention, only add the positive phrase at
+          // the end of the filter group
+          if (this === _.last(filters)) {
+            return true;
+          } else {
+            return false;
+          }
+        } catch (error) {
+          console.log(
+            "There was a problem detecting whether a Filter required a positive" +
+              " clause. Assuming that it needs one. Error details: " +
+              error,
+          );
+          return true;
         }
+      },
 
-      }, this);
-
-      if( values.length > 1 ){
-        valuesQueryString = "(" + valuesQueryString + ")"
-      }
-
-      return valuesQueryString;
-    },
+      /**
+       * Constructs a query substring for each of the values set on this model
+       *
+       * @example
+       *   values: ["walker", "jones"]
+       *   Returns: "(walker%20OR%20jones)"
+       *
+       * @param {string[]} [values] - The values to use in this query substring.
+       * If not provided, the values set on the model are used.
+       * @return {string} The query substring
+       */
+      getValueQuerySubstring: function (values) {
+        //Start a query string for this field and get the values
+        var valuesQueryString = "",
+          values = values || this.get("values");
 
-    /**
-     * Checks if any of the fields in this Filter match one of the
-     * {@link AppConfig#queryIdentifierFields}
-     * @since 2.17.0
-     */
-    isIdFilter: function(){
-      try {
-        let fields = this.get("fields");
-        let values = this.get("values");
-
-        if(!fields){
-          return false
+        //If the values are not an array, convert it to an array
+        if (!Array.isArray(values)) {
+          values = [values];
         }
-        let idFields = MetacatUI.appModel.get("queryIdentifierFields");
-        let match = _.some(idFields, idField => fields.includes(idField));
-        
-        //Check if the values are all identifiers by checking for uuids and dois
-        if(!match && values.length){
-          match = values.every(v => /^urn:uuid:\w{8,}-\w{4,}-\w{4,}-\w{4,}-\w{12,}$|^.{0,}doi.{1,}10.\d{4,9}\/[-._;()\/:A-Z0-9]+$/i.test(v));
-        }
-
-        return match;
 
-      } catch (error) {
-        console.log("Error checking if a Filter model is an ID filter. " +
-          "Assuming it is not. Error details:" + error );
-        return false
-      }
-    },
+        //Iterate over each value set on the model
+        _.each(
+          values,
+          function (value, i) {
+            //If the value is not a string, then convert it to a string
+            if (typeof value != "string") {
+              value = value.toString();
+            }
+
+            //Trim off whitespace
+            value = value.trim();
+
+            var dateRangeRegEx =
+                /^\[((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d*Z)|\*)( |%20)TO( |%20)((\d{4}-[01]\d-[0-3]\dT[0-2]\d(:|\\:)[0-5]\d(:|\\:)[0-5]\d\.\d*Z)|\*)\]/,
+              isDateRange = dateRangeRegEx.test(value),
+              isSearchPhrase = value.indexOf(" ") > -1,
+              isIdFilter = this.isIdFilter(),
+              //Test for ORCIDs and group subjects
+              isSubject =
+                /^(?:https?:\/\/orcid\.org\/)?(?:\w{4}-){3}\w{4}$|^(?:CN=.{1,},DC=.{1,},DC=.{1,})$/i.test(
+                  value,
+                );
+
+            // Escape special characters
+            value = this.escapeSpecialChar(value);
+
+            //URL encode the search value
+            value = encodeURIComponent(value);
+
+            // If the value is a search phrase (more than one word), is part of an ID filter,
+            // and not a date range string, wrap in quotes
+            if ((isSearchPhrase || isIdFilter || isSubject) && !isDateRange) {
+              value = '"' + value + '"';
+            }
+
+            if (this.get("matchSubstring") && !isDateRange) {
+              // Look for existing wildcard characters at the end of the value string, wrap
+              // the value string in wildcard characters if there aren't any yet.
+              if (!value.match(/^\*|\*$/)) {
+                value = "*" + value + "*";
+              }
+            }
 
-    /**
-    * Resets the values attribute on this filter
-    */
-    resetValue: function(){
-      this.set("values", this.defaults().values);
-    },
+            // Add the value to the query string
+            valuesQueryString += value;
 
-    /**
-    * Checks if this Filter has values different than the default values.
-    * @return {boolean} - Returns true if this Filter has values set on it, otherwise will return false
-    */
-    hasChangedValues: function(){
+            //Add the operator between values
+            if (values.length > i + 1 && valuesQueryString.length) {
+              valuesQueryString += "%20" + this.get("operator") + "%20";
+            }
+          },
+          this,
+        );
 
-      return (this.get("values").length > 0);
+        if (values.length > 1) {
+          valuesQueryString = "(" + valuesQueryString + ")";
+        }
 
-    },
+        return valuesQueryString;
+      },
 
-    /**
-     * isEmpty - Checks whether this Filter has any values or fields set
-     *
-     * @return {boolean}  returns true if the Filter's values and fields are empty
-     */
-    isEmpty: function(){
-      try {
-        var fields      =   this.get("fields"),
-            values      =   this.get("values"),
-            noFields    =   !fields || fields.length == 0,
-            fieldsEmpty =   _.every(fields, function(item) { return item == "" }),
-            noValues    =   !values || values.length == 0,
-            valuesEmpty =   _.every(values, function(item) { return item == "" });
-
-        var noMinNoMax = _.every(
-          [this.get("min"), this.get("max")],
-          function(num) {
-            return (typeof num === "undefined") || (!num && num !== 0);
+      /**
+       * Checks if any of the fields in this Filter match one of the
+       * {@link AppConfig#queryIdentifierFields}
+       * @since 2.17.0
+       */
+      isIdFilter: function () {
+        try {
+          let fields = this.get("fields");
+          let values = this.get("values");
+
+          if (!fields) {
+            return false;
+          }
+          let idFields = MetacatUI.appModel.get("queryIdentifierFields");
+          let match = _.some(idFields, (idField) => fields.includes(idField));
+
+          //Check if the values are all identifiers by checking for uuids and dois
+          if (!match && values.length) {
+            match = values.every((v) =>
+              /^urn:uuid:\w{8,}-\w{4,}-\w{4,}-\w{4,}-\w{12,}$|^.{0,}doi.{1,}10.\d{4,9}\/[-._;()\/:A-Z0-9]+$/i.test(
+                v,
+              ),
+            );
           }
-        );
 
-        // Values aren't required for UI filter types. Labels, icons, and descriptions are
-        // available.
-        if(this.get("isUIFilterType")){
-          noUIVals = _.every(["label", "icon", "description"], function(attrName){
-            var setValue = this.get(attrName);
-            var defaultValue = this.defaults()[attrName];
-            return !setValue || (setValue === defaultValue)
-          }, this)
-          return noUIVals && noFields && fieldsEmpty && noMinNoMax
+          return match;
+        } catch (error) {
+          console.log(
+            "Error checking if a Filter model is an ID filter. " +
+              "Assuming it is not. Error details:" +
+              error,
+          );
+          return false;
         }
+      },
 
-        // For regular search filters, just a field and some sort of search term/value is
-        // required
-        return noFields && fieldsEmpty && noValues && valuesEmpty && noMinNoMax
-
-      } catch (e) {
-        console.log("Failed to check if a Filter is empty, error message: " + e);
-      }
-    },
-
-    /**
-    * Escapes Solr query reserved characters so that search terms can include
-    *  those characters without throwing an error.
-    *
-    * @param {string} term - The search term or phrase to escape
-    * @return {string} - The search term or phrase, after special characters are escaped
-    */
-    escapeSpecialChar: function(term) {
+      /**
+       * Resets the values attribute on this filter
+       */
+      resetValue: function () {
+        this.set("values", this.defaults().values);
+      },
 
-      if(!term || typeof term != "string"){
-        return "";
-      }
+      /**
+       * Checks if this Filter has values different than the default values.
+       * @return {boolean} - Returns true if this Filter has values set on it, otherwise will return false
+       */
+      hasChangedValues: function () {
+        return this.get("values").length > 0;
+      },
 
-      // Removes all the ampersands since Metacat cannot handle escaped ampersands for some reason 
-      // See https://github.com/NCEAS/metacat/issues/1576
-      term = term.replaceAll("&", "");
+      /**
+       * isEmpty - Checks whether this Filter has any values or fields set
+       *
+       * @return {boolean}  returns true if the Filter's values and fields are empty
+       */
+      isEmpty: function () {
+        try {
+          var fields = this.get("fields"),
+            values = this.get("values"),
+            noFields = !fields || fields.length == 0,
+            fieldsEmpty = _.every(fields, function (item) {
+              return item == "";
+            }),
+            noValues = !values || values.length == 0,
+            valuesEmpty = _.every(values, function (item) {
+              return item == "";
+            });
+
+          var noMinNoMax = _.every(
+            [this.get("min"), this.get("max")],
+            function (num) {
+              return typeof num === "undefined" || (!num && num !== 0);
+            },
+          );
+
+          // Values aren't required for UI filter types. Labels, icons, and descriptions are
+          // available.
+          if (this.get("isUIFilterType")) {
+            noUIVals = _.every(
+              ["label", "icon", "description"],
+              function (attrName) {
+                var setValue = this.get(attrName);
+                var defaultValue = this.defaults()[attrName];
+                return !setValue || setValue === defaultValue;
+              },
+              this,
+            );
+            return noUIVals && noFields && fieldsEmpty && noMinNoMax;
+          }
 
-      return term.replace(/\+|-|&|\||!|\(|\)|\{|\}|\[|\]|\^|\\|\"|~|\?|:|\//g, "\\$&");
+          // For regular search filters, just a field and some sort of search term/value is
+          // required
+          return (
+            noFields && fieldsEmpty && noValues && valuesEmpty && noMinNoMax
+          );
+        } catch (e) {
+          console.log(
+            "Failed to check if a Filter is empty, error message: " + e,
+          );
+        }
+      },
 
-    },
+      /**
+       * Escapes Solr query reserved characters so that search terms can include
+       *  those characters without throwing an error.
+       *
+       * @param {string} term - The search term or phrase to escape
+       * @return {string} - The search term or phrase, after special characters are escaped
+       */
+      escapeSpecialChar: function (term) {
+        if (!term || typeof term != "string") {
+          return "";
+        }
 
-    /**
-     * Updates XML DOM with the new values from the model
-     *
-     *  @param {object} [options] A literal object with options for this serialization
-     *  @return {Element} A new XML element with the updated values
-    */
-    updateDOM: function(options){
+        // Removes all the ampersands since Metacat cannot handle escaped ampersands for some reason
+        // See https://github.com/NCEAS/metacat/issues/1576
+        term = term.replaceAll("&", "");
 
-      try{
+        return term.replace(
+          /\+|-|&|\||!|\(|\)|\{|\}|\[|\]|\^|\\|\"|~|\?|:|\//g,
+          "\\$&",
+        );
+      },
 
-        if(typeof options == "undefined"){
-          var options = {};
-        }
+      /**
+       * Updates XML DOM with the new values from the model
+       *
+       *  @param {object} [options] A literal object with options for this serialization
+       *  @return {Element} A new XML element with the updated values
+       */
+      updateDOM: function (options) {
+        try {
+          if (typeof options == "undefined") {
+            var options = {};
+          }
 
-        var objectDOM = this.get("objectDOM"),
+          var objectDOM = this.get("objectDOM"),
             filterOptionsNode;
 
-        if( typeof objectDOM == "undefined" || !objectDOM || !$(objectDOM).length ){
-          // Node name differs for different filters, all of which use this function
-          var nodeName = this.get("nodeName") || "filter";
-          // Create an XML filter element from scratch
-          var objectDOM = new DOMParser().parseFromString(
-            "<" + nodeName + "></" + nodeName + ">",
-            "text/xml"
+          if (
+            typeof objectDOM == "undefined" ||
+            !objectDOM ||
+            !$(objectDOM).length
+          ) {
+            // Node name differs for different filters, all of which use this function
+            var nodeName = this.get("nodeName") || "filter";
+            // Create an XML filter element from scratch
+            var objectDOM = new DOMParser().parseFromString(
+              "<" + nodeName + "></" + nodeName + ">",
+              "text/xml",
             );
-          var $objectDOM = $(objectDOM).find(nodeName);
-        }
-        else{
-          objectDOM = objectDOM.cloneNode(true);
-          var $objectDOM = $(objectDOM);
-
-          //Detach the filterOptions so they are saved
-          filterOptionsNode = $objectDOM.children("filterOptions");
-          filterOptionsNode.detach();
-
-          //Empty the DOM
-          $objectDOM.empty();
-        }
+            var $objectDOM = $(objectDOM).find(nodeName);
+          } else {
+            objectDOM = objectDOM.cloneNode(true);
+            var $objectDOM = $(objectDOM);
 
-        var xmlDocument = $objectDOM[0].ownerDocument;
+            //Detach the filterOptions so they are saved
+            filterOptionsNode = $objectDOM.children("filterOptions");
+            filterOptionsNode.detach();
 
-        // Get new values. Must store in an array because the order that we add each
-        // element to the DOM matters
-        var filterData = [
-          {
-            nodeName: "label",
-            value: this.get("label"),
-          },
-          {
-            nodeName: "field",
-            value: this.get("fields"),
-          },
-          {
-            nodeName: "operator",
-            value: this.get("operator"),
-          },
-          {
-            nodeName: "exclude",
-            value: this.get("exclude"),
-          },
-          {
-            nodeName: "fieldsOperator",
-            value: this.get("fieldsOperator"),
-          },
-          {
-            nodeName: "matchSubstring",
-            value: this.get("matchSubstring"),
-          },
-          {
-            nodeName: "value",
-            value: this.get("values"),
-          },
-        ]
-
-        filterData.forEach(function(element){
-          var values = element.value;
-          var nodeName = element.nodeName;
-
-          // Serialize the nodes with multiple occurrences
-          if( Array.isArray(values) ){
-            _.each(values, function(value){
-              // Don't serialize empty, null, or undefined values
-              if( value || value === false || value === 0 ){
-                var nodeSerialized = xmlDocument.createElement(nodeName);
-                $(nodeSerialized).text(value);
-                $objectDOM.append(nodeSerialized);
-              }
-            }, this);
-          }
-          // Serialize the single occurrence nodes. Don't serialize falsey or default values
-          else if((values || values === false) && values != this.defaults()[nodeName]) {
-            var nodeSerialized = xmlDocument.createElement(nodeName);
-            $(nodeSerialized).text(values);
-            $objectDOM.append(nodeSerialized);
+            //Empty the DOM
+            $objectDOM.empty();
           }
 
-        }, this);
-
-        // If this is a UIFilterType that won't be serialized into a Collection definition,
-        // then add extra XML nodes
-        if( this.get("isUIFilterType") ){
-
-          //Update the filterOptions XML DOM
-          filterOptionsNode = this.updateFilterOptionsDOM(filterOptionsNode);
-
-          //Add the filterOptions to the filter DOM
-          if( typeof filterOptionsNode != "undefined" && $(filterOptionsNode).children().length ){
-            $objectDOM.append(filterOptionsNode);
+          var xmlDocument = $objectDOM[0].ownerDocument;
+
+          // Get new values. Must store in an array because the order that we add each
+          // element to the DOM matters
+          var filterData = [
+            {
+              nodeName: "label",
+              value: this.get("label"),
+            },
+            {
+              nodeName: "field",
+              value: this.get("fields"),
+            },
+            {
+              nodeName: "operator",
+              value: this.get("operator"),
+            },
+            {
+              nodeName: "exclude",
+              value: this.get("exclude"),
+            },
+            {
+              nodeName: "fieldsOperator",
+              value: this.get("fieldsOperator"),
+            },
+            {
+              nodeName: "matchSubstring",
+              value: this.get("matchSubstring"),
+            },
+            {
+              nodeName: "value",
+              value: this.get("values"),
+            },
+          ];
+
+          filterData.forEach(function (element) {
+            var values = element.value;
+            var nodeName = element.nodeName;
+
+            // Serialize the nodes with multiple occurrences
+            if (Array.isArray(values)) {
+              _.each(
+                values,
+                function (value) {
+                  // Don't serialize empty, null, or undefined values
+                  if (value || value === false || value === 0) {
+                    var nodeSerialized = xmlDocument.createElement(nodeName);
+                    $(nodeSerialized).text(value);
+                    $objectDOM.append(nodeSerialized);
+                  }
+                },
+                this,
+              );
+            }
+            // Serialize the single occurrence nodes. Don't serialize falsey or default values
+            else if (
+              (values || values === false) &&
+              values != this.defaults()[nodeName]
+            ) {
+              var nodeSerialized = xmlDocument.createElement(nodeName);
+              $(nodeSerialized).text(values);
+              $objectDOM.append(nodeSerialized);
+            }
+          }, this);
+
+          // If this is a UIFilterType that won't be serialized into a Collection definition,
+          // then add extra XML nodes
+          if (this.get("isUIFilterType")) {
+            //Update the filterOptions XML DOM
+            filterOptionsNode = this.updateFilterOptionsDOM(filterOptionsNode);
+
+            //Add the filterOptions to the filter DOM
+            if (
+              typeof filterOptionsNode != "undefined" &&
+              $(filterOptionsNode).children().length
+            ) {
+              $objectDOM.append(filterOptionsNode);
+            }
           }
 
+          return $objectDOM[0];
+        } catch (e) {
+          console.error("Unable to serialize a Filter.", e);
+          return this.get("objectDOM") || "";
         }
+      },
 
-        return $objectDOM[0];
-      }
-      catch(e){
-        console.error("Unable to serialize a Filter.", e);
-        return this.get("objectDOM") || "";
-      }
-    },
+      /**
+       * Serializes the filter options into an XML DOM and returns it
+       * @param {Element} [filterOptionsNode] - The XML filterOptions node to update
+       * @return {Element} - The updated DOM
+       */
+      updateFilterOptionsDOM: function (filterOptionsNode) {
+        try {
+          if (
+            typeof filterOptionsNode == "undefined" ||
+            !filterOptionsNode.length
+          ) {
+            var filterOptionsNode = new DOMParser().parseFromString(
+              "<filterOptions></filterOptions>",
+              "text/xml",
+            );
+            var filterOptionsNode =
+              $(filterOptionsNode).find("filterOptions")[0];
+          }
+          //Convert the XML node into a jQuery object
+          var $filterOptionsNode = $(filterOptionsNode);
 
-    /**
-    * Serializes the filter options into an XML DOM and returns it
-    * @param {Element} [filterOptionsNode] - The XML filterOptions node to update
-    * @return {Element} - The updated DOM
-    */
-    updateFilterOptionsDOM: function(filterOptionsNode){
+          //Get the first option element
+          var firstOptionNode = $filterOptionsNode.children("option").first();
 
-      try{
+          var xmlDocument;
+          if (filterOptionsNode.length && filterOptionsNode[0]) {
+            xmlDocument = filterOptionsNode[0].ownerDocument;
+          }
+          if (!xmlDocument) {
+            xmlDocument = filterOptionsNode.ownerDocument;
+          }
+          if (!xmlDocument) {
+            xmlDocument = filterOptionsNode;
+          }
 
-        if (typeof filterOptionsNode == "undefined" || !filterOptionsNode.length) {
-          var filterOptionsNode = new DOMParser().parseFromString("<filterOptions></filterOptions>", "text/xml");
-          var filterOptionsNode = $(filterOptionsNode).find("filterOptions")[0];
+          // Update the text value of UI nodes. The following values are for
+          // UIFilterOptionsType
+          ["placeholder", "icon", "description"].forEach(function (nodeName) {
+            //Remove the existing node, if it exists
+            $filterOptionsNode.children(nodeName).remove();
+
+            // If there is a value set on the model for this attribute, then create an XML
+            // node for this attribute and set the text value
+            var value = this.get(nodeName);
+            if (value) {
+              var newNode = $(xmlDocument.createElement(nodeName)).text(value);
+
+              if (firstOptionNode.length) firstOptionNode.before(newNode);
+              else $filterOptionsNode.append(newNode);
+            }
+          }, this);
+
+          //If no options were serialized, then return an empty string
+          if (!$filterOptionsNode.children().length) {
+            return "";
+          } else {
+            return filterOptionsNode;
+          }
+        } catch (e) {
+          console.log(
+            "Error updating the FilterOptions DOM in a Filter model, " +
+              "error details: ",
+            e,
+          );
+          return "";
         }
-        //Convert the XML node into a jQuery object
-        var $filterOptionsNode = $(filterOptionsNode);
-
-        //Get the first option element
-        var firstOptionNode = $filterOptionsNode.children("option").first();
+      },
 
-        var xmlDocument;
-        if (filterOptionsNode.length && filterOptionsNode[0]) {
-          xmlDocument = filterOptionsNode[0].ownerDocument;
-        }
-        if (!xmlDocument) {
-          xmlDocument = filterOptionsNode.ownerDocument;
+      /**
+       * Returns true if the given value or value set on this filter is a date range query
+       * @param {string} value - The string to test
+       * @return {boolean}
+       */
+      isDateQuery: function (value) {
+        if (typeof value == "undefined" && this.get("values").length == 1) {
+          var value = this.get("values")[0];
         }
-        if (!xmlDocument) {
-          xmlDocument = filterOptionsNode;
-        }
-
-        // Update the text value of UI nodes. The following values are for
-        // UIFilterOptionsType
-        ["placeholder", "icon", "description"].forEach(function(nodeName){
 
-          //Remove the existing node, if it exists
-          $filterOptionsNode.children(nodeName).remove();
-
-          // If there is a value set on the model for this attribute, then create an XML
-          // node for this attribute and set the text value
-          var value = this.get(nodeName);
-          if( value ){
-            var newNode = $(xmlDocument.createElement(nodeName)).text(value);
+        if (value) {
+          return /[\d|\-|:|T]*Z TO [\d|\-|:|T]*Z/.test(value);
+        } else {
+          return false;
+        }
+      },
 
-            if( firstOptionNode.length )
-              firstOptionNode.before(newNode);
-            else
-              $filterOptionsNode.append(newNode);
+      /**
+       * Check whether a set of query fields contain only fields that specify latitude and/or
+       * longitude
+       * @param {string[]} [fields] A list of fields to check for coordinate fields. If not
+       * provided, the fields set on the model will be used.
+       * @returns {Boolean} Returns true if every field is a field that specifies latitude or
+       * longitude
+       * @since 2.21.0
+       */
+      isCoordinateQuery: function (fields) {
+        try {
+          if (!fields) {
+            fields = this.get("fields");
           }
-        }, this);
-
-        //If no options were serialized, then return an empty string
-        if( !$filterOptionsNode.children().length ){
-          return "";
-        }
-        else{
-          return filterOptionsNode;
+          const latitudeFields = MetacatUI.appModel.get("queryLatitudeFields");
+          const longitudeFields = MetacatUI.appModel.get(
+            "queryLongitudeFields",
+          );
+          const coordinateFields = latitudeFields.concat(longitudeFields);
+          return _.every(fields, function (field) {
+            return _.contains(coordinateFields, field);
+          });
+        } catch (e) {
+          console.log(
+            "Error checking if filter is a coordinate filter. Returning false. ",
+            e,
+          );
+          return false;
         }
-      }
-      catch(e){
-        console.log("Error updating the FilterOptions DOM in a Filter model, "+
-        "error details: ", e);
-        return "";
-      }
-
-    },
-
-    /**
-    * Returns true if the given value or value set on this filter is a date range query
-    * @param {string} value - The string to test
-    * @return {boolean}
-    */
-    isDateQuery: function(value){
-
-      if( typeof value == "undefined" && this.get("values").length == 1 ){
-        var value = this.get("values")[0];
-      }
-
-      if( value ){
-        return /[\d|\-|:|T]*Z TO [\d|\-|:|T]*Z/.test(value);
-      }
-      else{
-        return false;
-      }
       },
-    
-    /**
-     * Check whether a set of query fields contain only fields that specify latitude and/or
-     * longitude
-     * @param {string[]} [fields] A list of fields to check for coordinate fields. If not
-     * provided, the fields set on the model will be used.
-     * @returns {Boolean} Returns true if every field is a field that specifies latitude or
-     * longitude
-     * @since 2.21.0
-    */
-    isCoordinateQuery: function (fields) {
-      try {
-        if (!fields) {
-          fields = this.get('fields');
-        }
-        const latitudeFields = MetacatUI.appModel.get('queryLatitudeFields');
-        const longitudeFields = MetacatUI.appModel.get('queryLongitudeFields');
-        const coordinateFields = latitudeFields.concat(longitudeFields);
-        return _.every(fields, function (field) {
-          return _.contains(coordinateFields, field);
-        })
-      }
-      catch (e) {
-        console.log('Error checking if filter is a coordinate filter. Returning false. ', e);
-        return false;
-      }
-    },
-    
-    /**
-     * Check whether a set of query fields contain only fields that specify latitude
-     * @param {string[]} [fields] A list of fields to check for coordinate fields. If not
-     * provided, the fields set on the model will be used.
-     * @returns {Boolean} Returns true if every field is a field that specifies latitude
-     * @since 2.21.0
-    */
-    isLatitudeQuery: function (fields) {
-      try {
-        if (!fields) {
-          fields = this.get('fields');
-        }
-        const latitudeFields = MetacatUI.appModel.get('queryLatitudeFields');
-        return _.every(fields, function (field) {
-          return _.contains(latitudeFields, field);
-        })
-      }
-      catch (e) {
-        console.log('Error checking if filter is a latitude filter. Returning false. ', e);
-        return false;
-      }
-    },
 
-    /**
-     * Check whether a set of query fields contain only fields that specify longitude
-     * @param {string[]} [fields] A list of fields to check for longitude fields. If not
-     * provided, the fields set on the model will be used.
-     * @returns {Boolean} Returns true if every field is a field that specifies longitude
-     * @since 2.21.0
-    */
-    isLongitudeQuery: function (fields) {
-      try {
-        if (!fields) {
-          fields = this.get('fields');
+      /**
+       * Check whether a set of query fields contain only fields that specify latitude
+       * @param {string[]} [fields] A list of fields to check for coordinate fields. If not
+       * provided, the fields set on the model will be used.
+       * @returns {Boolean} Returns true if every field is a field that specifies latitude
+       * @since 2.21.0
+       */
+      isLatitudeQuery: function (fields) {
+        try {
+          if (!fields) {
+            fields = this.get("fields");
+          }
+          const latitudeFields = MetacatUI.appModel.get("queryLatitudeFields");
+          return _.every(fields, function (field) {
+            return _.contains(latitudeFields, field);
+          });
+        } catch (e) {
+          console.log(
+            "Error checking if filter is a latitude filter. Returning false. ",
+            e,
+          );
+          return false;
         }
-        const longitudeFields = MetacatUI.appModel.get('queryLongitudeFields');
-        return _.every(fields, function (field) {
-          return _.contains(longitudeFields, field);
-        })
-      }
-      catch (error) {
-        console.log('Error checking if filter is a longitude filter. Returning false. ', e);
-        return false;
-      }
-    },
-
-    /**
-    * Checks if the values set on this model are valid.
-    * Some of the attributes are changed during this process if they are found to be invalid,
-    * since there aren't any easy ways for users to fix these issues themselves in the UI.
-    * @return {object} - Returns a literal object with the invalid attributes and their corresponding error message
-    */
-    validate: function(){
-
-      try{
-
-        var errors = {};
-        // UI filter types have 
-        var isUIFilterType = this.get("isUIFilterType");
-
-        //---Validate fields----
-        var fields = this.get("fields");
-        //All fields should be strings
-        var nonStrings = _.filter(fields, function(field){
-          return (typeof field != "string" || !field.trim().length);
-        });
+      },
 
-        if( nonStrings.length ){
-          //Remove the nonstrings from the model, rather than returning an error
-          this.set("fields", _.without(fields, nonStrings));
-        }
-        //If there are no fields, set an error message
-        if( !this.get("fields").length ){
-          errors.fields = "Filters should have at least one search field.";
+      /**
+       * Check whether a set of query fields contain only fields that specify longitude
+       * @param {string[]} [fields] A list of fields to check for longitude fields. If not
+       * provided, the fields set on the model will be used.
+       * @returns {Boolean} Returns true if every field is a field that specifies longitude
+       * @since 2.21.0
+       */
+      isLongitudeQuery: function (fields) {
+        try {
+          if (!fields) {
+            fields = this.get("fields");
+          }
+          const longitudeFields = MetacatUI.appModel.get(
+            "queryLongitudeFields",
+          );
+          return _.every(fields, function (field) {
+            return _.contains(longitudeFields, field);
+          });
+        } catch (error) {
+          console.log(
+            "Error checking if filter is a longitude filter. Returning false. ",
+            e,
+          );
+          return false;
         }
+      },
 
-        //---Validate values----
-        var values = this.get("values");
-        // All values should be strings, booleans, numbers, or dates
-        var invalidValues = _.filter(values, function(value){
-          //Empty strings are invalid
-          if( typeof value == "string" && !value.trim().length ){
-            return true;
+      /**
+       * Checks if the values set on this model are valid.
+       * Some of the attributes are changed during this process if they are found to be invalid,
+       * since there aren't any easy ways for users to fix these issues themselves in the UI.
+       * @return {object} - Returns a literal object with the invalid attributes and their corresponding error message
+       */
+      validate: function () {
+        try {
+          var errors = {};
+          // UI filter types have
+          var isUIFilterType = this.get("isUIFilterType");
+
+          //---Validate fields----
+          var fields = this.get("fields");
+          //All fields should be strings
+          var nonStrings = _.filter(fields, function (field) {
+            return typeof field != "string" || !field.trim().length;
+          });
+
+          if (nonStrings.length) {
+            //Remove the nonstrings from the model, rather than returning an error
+            this.set("fields", _.without(fields, nonStrings));
           }
-          //Non-empty strings, booleans, numbers, or dates are valid
-          else if( typeof value == "string" || typeof value == "boolean" ||
-                   typeof value == "number" || Date.prototype.isPrototypeOf(value) ){
-            return false;
+          //If there are no fields, set an error message
+          if (!this.get("fields").length) {
+            errors.fields = "Filters should have at least one search field.";
           }
-        });
 
-        if( invalidValues.length ){
-          //Remove the invalid values from the model, rather than returning an error
-          this.set("values", _.without(values, invalidValues));
-        }
+          //---Validate values----
+          var values = this.get("values");
+          // All values should be strings, booleans, numbers, or dates
+          var invalidValues = _.filter(values, function (value) {
+            //Empty strings are invalid
+            if (typeof value == "string" && !value.trim().length) {
+              return true;
+            }
+            //Non-empty strings, booleans, numbers, or dates are valid
+            else if (
+              typeof value == "string" ||
+              typeof value == "boolean" ||
+              typeof value == "number" ||
+              Date.prototype.isPrototypeOf(value)
+            ) {
+              return false;
+            }
+          });
+
+          if (invalidValues.length) {
+            //Remove the invalid values from the model, rather than returning an error
+            this.set("values", _.without(values, invalidValues));
+          }
 
-        //If there are no values, and this isn't a custom search filter, set an error
-        //message.
-        if ( !isUIFilterType && !this.get("values").length ){
-          errors.values = "Filters should include at least one search term.";
-        }
+          //If there are no values, and this isn't a custom search filter, set an error
+          //message.
+          if (!isUIFilterType && !this.get("values").length) {
+            errors.values = "Filters should include at least one search term.";
+          }
 
-        //---Validate operators ----
-        //The operator must be either AND or OR
-        ["operator", "fieldsOperator"].forEach(function(op){
-          if( !["AND", "OR"].includes(this.get(op)) ){
+          //---Validate operators ----
+          //The operator must be either AND or OR
+          ["operator", "fieldsOperator"].forEach(function (op) {
+            if (!["AND", "OR"].includes(this.get(op))) {
+              //Reset the value to the default rather than return an error
+              this.set(op, this.defaults()[op]);
+            }
+          }, this);
+
+          //---Validate exclude and matchSubstring----
+          //Exclude should always be a boolean
+          if (typeof this.get("exclude") != "boolean") {
             //Reset the value to the default rather than return an error
-            this.set(op, this.defaults()[op]);
+            this.set("exclude", this.defaults().exclude);
           }
-        }, this);
-
-
-        //---Validate exclude and matchSubstring----
-        //Exclude should always be a boolean
-        if( typeof this.get("exclude") != "boolean" ){
-          //Reset the value to the default rather than return an error
-          this.set("exclude", this.defaults().exclude);
-        }
-        //matchSubstring should always be a boolean
-        if( typeof this.get("matchSubstring") != "boolean" ){
-          //Reset the value to the default rather than return an error
-          this.set("matchSubstring", this.defaults().matchSubstring);
-        }
-
-        //---Validate label, placeholder, icon, and description----
-        var textAttributes = ["label", "placeholder", "icon", "description"];
-        //These fields should be strings
-        _.each(textAttributes, function(attr){
-          if( typeof this.get(attr) != "string" ){
+          //matchSubstring should always be a boolean
+          if (typeof this.get("matchSubstring") != "boolean") {
             //Reset the value to the default rather than return an error
-            this.set(attr, this.defaults()[attr]);
+            this.set("matchSubstring", this.defaults().matchSubstring);
           }
-        }, this);
-
-        if( Object.keys(errors).length )
-          return errors;
-        else{
-          return;
-        }
-      }
-      catch(e){
-        console.error(e);
-      }
 
-    }
+          //---Validate label, placeholder, icon, and description----
+          var textAttributes = ["label", "placeholder", "icon", "description"];
+          //These fields should be strings
+          _.each(
+            textAttributes,
+            function (attr) {
+              if (typeof this.get(attr) != "string") {
+                //Reset the value to the default rather than return an error
+                this.set(attr, this.defaults()[attr]);
+              }
+            },
+            this,
+          );
 
-  });
+          if (Object.keys(errors).length) return errors;
+          else {
+            return;
+          }
+        } catch (e) {
+          console.error(e);
+        }
+      },
+    },
+  );
 
   return FilterModel;
-
 });
 
diff --git a/docs/docs/src_js_models_filters_FilterGroup.js.html b/docs/docs/src_js_models_filters_FilterGroup.js.html index cdaa78e00..d63eb9f57 100644 --- a/docs/docs/src_js_models_filters_FilterGroup.js.html +++ b/docs/docs/src_js_models_filters_FilterGroup.js.html @@ -44,616 +44,639 @@

Source: src/js/models/filters/FilterGroup.js

-
/* global define */
-define(["jquery", "underscore", "backbone", "collections/Filters", "models/filters/Filter" ],
-  function ($, _, Backbone, Filters, Filter) {
-
-    /**
-    * @class FilterGroup
-    * @classdesc A group of multiple Filters, and optionally nested Filter Groups, which
-    * may be combined to create a complex query. A FilterGroup may be a Collection
-    * FilterGroupType or a Portal UIFilterGroupType.
-    * @classcategory Models/Filters
-    * @extends Backbone.Model
-    * @constructs
-    */
-    var FilterGroup = Backbone.Model.extend(
-    /** @lends FilterGroup.prototype */{
-
-        /**
-          * The name of this Model
-          * @type {string}
-          * @readonly
-          */
-        type: "FilterGroup",
-
-        /**
-         * Default attributes for FilterGroup models
-         * @type {Object}
-         * @property {string} label - For UIFilterGroupType filter groups, a
-         * human-readable short label for this Filter Group
-         * @property {string} description - For UIFilterGroupType filter groups, a
-         * description of the Filter Group's function
-         * @property {string} icon - For UIFilterGroupType filter groups, a term that
-         * identifies a single icon in a supported icon library.
-         * @property {Filters} filters - A collection of Filter models that represent a
-         * full or partial query
-         * @property {XMLElement} objectDOM - FilterGroup XML
-         * @property {string} operator - The operator to use between filters (including
-         * filter groups) set on this model. Must be set to "AND" or "OR".
-         * @property {boolean} exclude - If true, search index docs matching the filters
-         * within this group will be excluded from the search results
-         * @property {boolean} isUIFilterType - Set to true if this group is
-         * UIFilterGroupType (aka custom Portal search filter). Otherwise, it's assumed
-         * that this model is FilterGroupType (e.g. a Collection FilterGroupType)
-         * @property {string} nodeName - the XML node name to use when serializing this
-         * model. For example, may be "filterGroup" or "definition".
-         * @property {boolean} isInvisible - If true, this filter will be added to the
-         * query but will act in the "background", like a default filter. It will not
-         * appear in the Query Builder or other UIs. If this is invisible, then the
-         * "isInvisible" property on sub-filters will be ignored.
-         * @property {boolean} mustMatchIds - If the search results must always match one
-         * of the ids in the id filters, then the id filters will be added to the query
-         * with an AND operator.
-        */
-        defaults: function () {
-          return {
-            label: null,
-            description: null,
-            icon: null,
-            filters: null,
-            objectDOM: null,
-            operator: "AND",
-            exclude: false,
-            isUIFilterType: false,
-            nodeName: "filterGroup",
-            isInvisible: false,
-            mustMatchIds: false
-            // TODO: support options for UIFilterGroupType 1.1.0 
-            // options: [],
-          }
-        },
-
-        /**
-        * This function is executed whenever a new FilterGroup model is created. Model
-        * attributes are set either by parsing attributes.objectDOM or ny extracting the
-        * properties from attributes (e.g. attributes.nodeName, attributes.operator, etc)
-        */
-        initialize: function (attributes) {
-
-          if(!attributes){
-            attributes = {}
-          }
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/Filters",
+  "models/filters/Filter",
+], function ($, _, Backbone, Filters, Filter) {
+  /**
+   * @class FilterGroup
+   * @classdesc A group of multiple Filters, and optionally nested Filter Groups, which
+   * may be combined to create a complex query. A FilterGroup may be a Collection
+   * FilterGroupType or a Portal UIFilterGroupType.
+   * @classcategory Models/Filters
+   * @extends Backbone.Model
+   * @constructs
+   */
+  var FilterGroup = Backbone.Model.extend(
+    /** @lends FilterGroup.prototype */ {
+      /**
+       * The name of this Model
+       * @type {string}
+       * @readonly
+       */
+      type: "FilterGroup",
+
+      /**
+       * Default attributes for FilterGroup models
+       * @type {Object}
+       * @property {string} label - For UIFilterGroupType filter groups, a
+       * human-readable short label for this Filter Group
+       * @property {string} description - For UIFilterGroupType filter groups, a
+       * description of the Filter Group's function
+       * @property {string} icon - For UIFilterGroupType filter groups, a term that
+       * identifies a single icon in a supported icon library.
+       * @property {Filters} filters - A collection of Filter models that represent a
+       * full or partial query
+       * @property {XMLElement} objectDOM - FilterGroup XML
+       * @property {string} operator - The operator to use between filters (including
+       * filter groups) set on this model. Must be set to "AND" or "OR".
+       * @property {boolean} exclude - If true, search index docs matching the filters
+       * within this group will be excluded from the search results
+       * @property {boolean} isUIFilterType - Set to true if this group is
+       * UIFilterGroupType (aka custom Portal search filter). Otherwise, it's assumed
+       * that this model is FilterGroupType (e.g. a Collection FilterGroupType)
+       * @property {string} nodeName - the XML node name to use when serializing this
+       * model. For example, may be "filterGroup" or "definition".
+       * @property {boolean} isInvisible - If true, this filter will be added to the
+       * query but will act in the "background", like a default filter. It will not
+       * appear in the Query Builder or other UIs. If this is invisible, then the
+       * "isInvisible" property on sub-filters will be ignored.
+       * @property {boolean} mustMatchIds - If the search results must always match one
+       * of the ids in the id filters, then the id filters will be added to the query
+       * with an AND operator.
+       */
+      defaults: function () {
+        return {
+          label: null,
+          description: null,
+          icon: null,
+          filters: null,
+          objectDOM: null,
+          operator: "AND",
+          exclude: false,
+          isUIFilterType: false,
+          nodeName: "filterGroup",
+          isInvisible: false,
+          mustMatchIds: false,
+          // TODO: support options for UIFilterGroupType 1.1.0
+          // options: [],
+        };
+      },
+
+      /**
+       * This function is executed whenever a new FilterGroup model is created. Model
+       * attributes are set either by parsing attributes.objectDOM or ny extracting the
+       * properties from attributes (e.g. attributes.nodeName, attributes.operator, etc)
+       */
+      initialize: function (attributes) {
+        if (!attributes) {
+          attributes = {};
+        }
 
-          if(attributes.isUIFilterType){
-            this.set("isUIFilterType", true);
-          }
+        if (attributes.isUIFilterType) {
+          this.set("isUIFilterType", true);
+        }
 
-          // When a Filter model within this Filter group changes, or when the Filters
-          // collection is updated, trigger a change event in this filterGroup model.
-          // Updates and Changes in the Filters collection won't trigger an event from
-          // this model otherwise. This helps when other models, collections, views are
-          // listening to this filterGroup, e.g. when the collections model updates the
-          // searchModel whenever the definition changes.
-          this.off("change:filters");
-          this.on("change:filters", function(){
+        // When a Filter model within this Filter group changes, or when the Filters
+        // collection is updated, trigger a change event in this filterGroup model.
+        // Updates and Changes in the Filters collection won't trigger an event from
+        // this model otherwise. This helps when other models, collections, views are
+        // listening to this filterGroup, e.g. when the collections model updates the
+        // searchModel whenever the definition changes.
+        this.off("change:filters");
+        this.on(
+          "change:filters",
+          function () {
             this.stopListening(this.get("filters"), "update change");
             this.listenTo(
               this.get("filters"),
               "update change",
-              function(model, record){
-                this.trigger("update", model, record)
-              }
+              function (model, record) {
+                this.trigger("update", model, record);
+              },
             );
-          }, this);
-
-          var newFiltersOptions = {};
-          var catalogSearch = false;
-
-          if(attributes.catalogSearch){
-            this.set("catalogSearch", true)
-          }
+          },
+          this,
+        );
 
-          // Set the attributes on this model by parsing XML if some was provided,
-          // or by using any attributes provided to this model
-          if (attributes.objectDOM) {
-            var groupAttrs = this.parse(attributes.objectDOM, catalogSearch);
-            this.set(groupAttrs);
-          } else{
-            ["label", "description", "icon", "operator",
-             "exclude", "nodeName", "isInvisible"].forEach(function(modelAttribute){
-              if(attributes[modelAttribute] || attributes[modelAttribute] === false){
-                this.set(modelAttribute, attributes[modelAttribute])
-              }
-            }, this);
-          }
+        var newFiltersOptions = {};
+        var catalogSearch = false;
 
-          if (attributes.filters) {
-            var filtersCollection = new Filters(null, newFiltersOptions);
-            filtersCollection.add(attributes.filters);
-            this.set("filters", filtersCollection);
-          }
+        if (attributes.catalogSearch) {
+          this.set("catalogSearch", true);
+        }
 
-          // Start a new Filters collection if no filters were provided
-          if(!this.get("filters")){
-            this.set("filters", new Filters(null, newFiltersOptions))
-          }
+        // Set the attributes on this model by parsing XML if some was provided,
+        // or by using any attributes provided to this model
+        if (attributes.objectDOM) {
+          var groupAttrs = this.parse(attributes.objectDOM, catalogSearch);
+          this.set(groupAttrs);
+        } else {
+          [
+            "label",
+            "description",
+            "icon",
+            "operator",
+            "exclude",
+            "nodeName",
+            "isInvisible",
+          ].forEach(function (modelAttribute) {
+            if (
+              attributes[modelAttribute] ||
+              attributes[modelAttribute] === false
+            ) {
+              this.set(modelAttribute, attributes[modelAttribute]);
+            }
+          }, this);
+        }
 
-          if (attributes.mustMatchIds) {
-            this.set("mustMatchIds", true);
-            this.get("filters").mustMatchIds = true;
-          }
+        if (attributes.filters) {
+          var filtersCollection = new Filters(null, newFiltersOptions);
+          filtersCollection.add(attributes.filters);
+          this.set("filters", filtersCollection);
+        }
 
-          // The operator must be AND or OR
-          if( !["AND", "OR"].includes(this.get("operator")) ){
-            // Set the value to the default
-            this.set("operator", this.defaults()["operator"])
-          }
+        // Start a new Filters collection if no filters were provided
+        if (!this.get("filters")) {
+          this.set("filters", new Filters(null, newFiltersOptions));
+        }
 
-        },
+        if (attributes.mustMatchIds) {
+          this.set("mustMatchIds", true);
+          this.get("filters").mustMatchIds = true;
+        }
 
-        /**
-        * Overrides the default Backbone.Model.parse() function to parse the filterGroup
-        * XML snippet
-        *
-        * @param {Element} xml - The XML Element that contains all the FilterGroup elements
-        * @param {boolean} catalogSearch [false] - Set to true to append a catalog search phrase
-        * to the search query created from Filters that limits the results to un-obsoleted
-        * metadata.
-        * @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
-        */
-        parse: function (xml, catalogSearch = false) {
+        // The operator must be AND or OR
+        if (!["AND", "OR"].includes(this.get("operator"))) {
+          // Set the value to the default
+          this.set("operator", this.defaults()["operator"]);
+        }
+      },
+
+      /**
+       * Overrides the default Backbone.Model.parse() function to parse the filterGroup
+       * XML snippet
+       *
+       * @param {Element} xml - The XML Element that contains all the FilterGroup elements
+       * @param {boolean} catalogSearch [false] - Set to true to append a catalog search phrase
+       * to the search query created from Filters that limits the results to un-obsoleted
+       * metadata.
+       * @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
+       */
+      parse: function (xml, catalogSearch = false) {
+        var modelJSON = {};
+
+        if (!xml) {
+          return modelJSON;
+        }
 
-          var modelJSON = {}
+        // FilterGroups can be either <definition> or <filterGroup>
+        this.set("nodeName", xml.nodeName);
 
-          if(!xml){
-            return modelJSON
+        // Parse all the text nodes. Node names and model attributes always match
+        // in this case.
+        ["label", "description", "icon", "operator"].forEach(function (
+          nodeName,
+        ) {
+          if ($(xml).find(nodeName).length) {
+            modelJSON[nodeName] = this.parseTextNode(xml, nodeName);
           }
+        }, this);
 
-          // FilterGroups can be either <definition> or <filterGroup>
-          this.set("nodeName", xml.nodeName);
+        // Parse the exclude field node (true or false)
+        if ($(xml).find("exclude").length) {
+          modelJSON.exclude =
+            this.parseTextNode(xml, "exclude") === "true" ? true : false;
+        }
 
-          // Parse all the text nodes. Node names and model attributes always match
-          // in this case.
-          ["label", "description", "icon", "operator"].forEach(
-            function(nodeName){
-              if($(xml).find(nodeName).length){
-                modelJSON[nodeName] = this.parseTextNode(xml, nodeName)
-              }
-            },
-            this
-          );
+        // Remove any nodes that aren't filters or filter groups from the XML
+        var filterNodeNames = [
+          "filter",
+          "booleanFilter",
+          "dateFilter",
+          "numericFilter",
+          "filterGroup",
+          "choiceFilter",
+          "toggleFilter",
+        ];
+        filterXML = xml.cloneNode(true);
+        $(filterXML).children().not(filterNodeNames.join(", ")).remove();
+
+        // Add the filters and nested filter groups to this filters model
+        // TODO: Add isNested property for filterGroups that are within filterGroups?
+        var filtersOptions = {
+          objectDOM: filterXML,
+          isUIFilterType: this.get("isUIFilterType"),
+        };
+
+        if (catalogSearch) {
+          filtersOptions.catalogSearch = true;
+        }
 
-          // Parse the exclude field node (true or false)
-          if( $(xml).find("exclude").length ){
-            modelJSON.exclude = (this.parseTextNode(xml, "exclude") === "true") ? true : false;
-          }
-          
-          // Remove any nodes that aren't filters or filter groups from the XML
-          var filterNodeNames = [
-            "filter", "booleanFilter", "dateFilter", "numericFilter", "filterGroup",
-            "choiceFilter", "toggleFilter"
-          ]
-          filterXML = xml.cloneNode(true)
-          $(filterXML)
-            .children()
-            .not(filterNodeNames.join(", "))
-            .remove();
-
-          // Add the filters and nested filter groups to this filters model
-          // TODO: Add isNested property for filterGroups that are within filterGroups?
-          var filtersOptions = {
-            objectDOM: filterXML,
-            isUIFilterType: this.get("isUIFilterType"),
+        modelJSON.filters = new Filters(null, filtersOptions);
+
+        return modelJSON;
+      },
+
+      /**
+       * Gets the text content of the XML node matching the given node name
+       *
+       * @param {Element} parentNode - The parent node to select from
+       * @param {string} nodeName - The name of the XML node to parse
+       * @param {boolean} isMultiple - If true, parses the nodes into an array
+       * @return {(string|Array)} - Returns a string or array of strings of the text content
+       */
+      parseTextNode: function (parentNode, nodeName, isMultiple) {
+        var node = $(parentNode).children(nodeName);
+
+        //If no matching nodes were found, return falsey values
+        if (!node || !node.length) {
+          //Return an empty array if the isMultiple flag is true
+          if (isMultiple) return [];
+          //Return null if the isMultiple flag is false
+          else return null;
+        }
+        //If exactly one node is found and we are only expecting one, return the text content
+        else if (node.length == 1 && !isMultiple) {
+          return node[0].textContent.trim();
+        }
+        //If more than one node is found, parse into an array
+        else {
+          return _.map(node, function (node) {
+            return node.textContent.trim() || null;
+          });
+        }
+      },
+
+      /**
+       * Builds the query string to send to the query engine. Iterates over each filter
+       * in the filter group and adds to the query string.
+       *
+       * @return {string} The query string to send to Solr
+       */
+      getQuery: function () {
+        try {
+          // Although the logic used in this function is very similar to the getQuery()
+          // function in the Filters collection, we can't just use
+          // this.get("filters").getQuery(operator), because there are some subtle
+          // differences with how queries are built using the information from
+          // filterGroups, especially when the exclude attribute is set to true.
+
+          var queryString = "";
+          if (this.isEmpty()) {
+            return queryString;
           }
 
-          if(catalogSearch){
-            filtersOptions.catalogSearch = true
-          }
+          // The operator to use between queries from filters/sub-filterGroups
+          var operator = this.get("operator");
+
+          // Helper function that adds URI encoded spaces to either side of a string
+          var padString = function (string) {
+            return "%20" + string + "%20";
+          };
+          // Helper function that appends a new part to a query fragment, using an
+          // operator if the initial fragment is not empty. Returns the string as-is if
+          // the newFragment is empty.
+          var addQueryFragment = function (string, newFragment, operator) {
+            if (
+              !newFragment ||
+              (newFragment && newFragment.trim().length == 0)
+            ) {
+              return string;
+            }
+            if (string && string.trim().length) {
+              string += padString(operator);
+            }
+            string += newFragment;
+            return string;
+          };
+          // Helper function that wraps a string in parentheses
+          var wrapInParentheses = function (string) {
+            if (!string || (string && string.trim().length == 0)) {
+              return string;
+            }
+            // TODO: We still want to wrap in parentheses in cases like "(a) OR (b)" and
+            // "a OR (b) or c" but not in cases like (a OR b)
 
-          modelJSON.filters = new Filters(null, filtersOptions);
+            // var alreadyWrapped = /^\(.*\)$/.test(string);
+            // if (alreadyWrapped) {
+            //   return string
+            // }
 
-          return modelJSON;
-        },
-
-        /**
-        * Gets the text content of the XML node matching the given node name
-        *
-        * @param {Element} parentNode - The parent node to select from
-        * @param {string} nodeName - The name of the XML node to parse
-        * @param {boolean} isMultiple - If true, parses the nodes into an array
-        * @return {(string|Array)} - Returns a string or array of strings of the text content
-        */
-        parseTextNode: function (parentNode, nodeName, isMultiple) {
-          var node = $(parentNode).children(nodeName);
-
-          //If no matching nodes were found, return falsey values
-          if (!node || !node.length) {
-
-            //Return an empty array if the isMultiple flag is true
-            if (isMultiple)
-              return [];
-            //Return null if the isMultiple flag is false
-            else
-              return null;
-          }
-          //If exactly one node is found and we are only expecting one, return the text content
-          else if (node.length == 1 && !isMultiple) {
-            return node[0].textContent.trim();
+            return "(" + string + ")";
+          };
+
+          // Get the list of filters that use id fields since these are used differently.
+          var idFilters = this.get("filters").getIdFilters();
+          // Get the remaining filters that don't contain any ID fields
+          var mainFilters = this.get("filters").getNonIdFilters();
+
+          // If the filterGroup should be excluded from the results, then don't include
+          // the isPartOf filter in the part of the query that gets excluded. The
+          // isPartOf filter is only meant to *include* additional results, never
+          // exclude any.
+          if (this.get("exclude")) {
+            var isPartOfFilter = null;
+            idFilters.forEach(function (filterModel, index) {
+              if (filterModel.get("fields")[0] == "isPartOf") {
+                idFilters.splice(index, 1);
+                isPartOfFilter = filterModel;
+              }
+            }, this);
           }
-          //If more than one node is found, parse into an array
-          else {
-
-            return _.map(node, function (node) {
-              return node.textContent.trim() || null;
-            });
 
+          // Create the grouped query for the id filters (this will have the isPartOf
+          // filter query if exclude is false, and will not have it if exclude is true)
+          var idFilterQuery = this.get("filters")
+            .getGroupQuery(idFilters, "OR")
+            .trim();
+          // Make the query fragment for all of the filters that do not contain ID fields
+          var mainQuery = this.get("filters")
+            .getGroupQuery(mainFilters, operator)
+            .trim();
+          // Make the query string that should be added to all catalog searches
+          var categoryQuery = "";
+          if (this.get("catalogSearch")) {
+            categoryQuery = this.get("filters")
+              .createCatalogSearchQuery()
+              .trim();
+          }
+          // Make the query string for the isPartOf filter when the filter group should
+          // be excluded
+          var isPartOfQuery = "";
+          if (isPartOfFilter) {
+            isPartOfQuery = isPartOfFilter.getQuery().trim();
           }
-        },
-
-        /**
-         * Builds the query string to send to the query engine. Iterates over each filter
-         * in the filter group and adds to the query string.
-         *
-         * @return {string} The query string to send to Solr
-         */
-        getQuery: function(){
-
-          try {
-
-            // Although the logic used in this function is very similar to the getQuery()
-            // function in the Filters collection, we can't just use
-            // this.get("filters").getQuery(operator), because there are some subtle
-            // differences with how queries are built using the information from
-            // filterGroups, especially when the exclude attribute is set to true.
-
-            var queryString = ""
-            if(this.isEmpty()){
-              return queryString
-            }
 
-            // The operator to use between queries from filters/sub-filterGroups
-            var operator = this.get("operator")
-
-            // Helper function that adds URI encoded spaces to either side of a string
-            var padString = function(string){ return "%20" + string + "%20" };
-            // Helper function that appends a new part to a query fragment, using an
-            // operator if the initial fragment is not empty. Returns the string as-is if
-            // the newFragment is empty.
-            var addQueryFragment = function(string, newFragment, operator){
-              if(!newFragment || (newFragment && newFragment.trim().length == 0) ){
-                return string
-              }
-              if(string && string.trim().length){
-                string += padString(operator)
-              }
-              string += newFragment
-              return string
+          if (this.get("exclude")) {
+            // The query is constructed like so for filter groups with exclude set to true:
+            // ( ( -( mainQuery OR idFilterQuery ) AND *:* ) OR isPartOfQuery ) AND categoryQuery
+            // Build the query string piece by piece:
+
+            // 1. mainQuery
+            queryString += mainQuery;
+            queryString = wrapInParentheses(queryString);
+            // 2. ( mainQuery OR idFilterQuery )
+            if (idFilterQuery.trim().length) {
+              idOperator = this.get("mustMatchIds") ? "AND" : "OR";
+              queryString = addQueryFragment(
+                queryString,
+                idFilterQuery,
+                idOperator,
+              );
+              queryString = wrapInParentheses(queryString);
             }
-            // Helper function that wraps a string in parentheses
-            var wrapInParentheses = function(string){
-              if (!string || (string && string.trim().length == 0)) {
-                return string
-              }
-              // TODO: We still want to wrap in parentheses in cases like "(a) OR (b)" and
-              // "a OR (b) or c" but not in cases like (a OR b)
-            
-              // var alreadyWrapped = /^\(.*\)$/.test(string);
-              // if (alreadyWrapped) {
-              //   return string
-              // }
-
-              return "(" + string + ")"
+            // 3. -( mainQuery OR idFilterQuery )
+            if (queryString.trim().length) {
+              queryString = "-" + queryString;
             }
-
-            // Get the list of filters that use id fields since these are used differently.
-            var idFilters = this.get("filters").getIdFilters();
-            // Get the remaining filters that don't contain any ID fields
-            var mainFilters = this.get("filters").getNonIdFilters();
-
-            // If the filterGroup should be excluded from the results, then don't include
-            // the isPartOf filter in the part of the query that gets excluded. The
-            // isPartOf filter is only meant to *include* additional results, never
-            // exclude any.
-            if(this.get("exclude")){
-              var isPartOfFilter = null;
-              idFilters.forEach(function(filterModel, index){
-                if(filterModel.get("fields")[0] == "isPartOf"){
-                  idFilters.splice(index, 1);
-                  isPartOfFilter = filterModel
-                }
-              }, this)
-            }
-
-            // Create the grouped query for the id filters (this will have the isPartOf
-            // filter query if exclude is false, and will not have it if exclude is true)
-            var idFilterQuery = this.get("filters").getGroupQuery(idFilters, "OR").trim();
-            // Make the query fragment for all of the filters that do not contain ID fields
-            var mainQuery = this.get("filters").getGroupQuery(mainFilters, operator).trim();
-            // Make the query string that should be added to all catalog searches
-            var categoryQuery = ""
-            if(this.get("catalogSearch")){
-              categoryQuery = this.get("filters").createCatalogSearchQuery().trim()
+            // 4. ( -( mainQuery OR idFilterQuery ) AND *:* )  - see Filter model
+            // requiresPositiveClause for details on why positive clause is
+            // needed here
+            if (queryString.trim().length) {
+              queryString = addQueryFragment(queryString, "*:*", "AND");
+              queryString = wrapInParentheses(queryString);
             }
-            // Make the query string for the isPartOf filter when the filter group should
-            // be excluded
-            var isPartOfQuery = ""
-            if(isPartOfFilter){
-              isPartOfQuery = isPartOfFilter.getQuery().trim();
+            // 5. ( ( -( mainQuery OR idFilterQuery ) AND *:* ) OR isPartOfQuery)
+            if (isPartOfQuery) {
+              queryString = addQueryFragment(queryString, isPartOfQuery, "OR");
+              queryString = wrapInParentheses(queryString);
             }
 
-            if(this.get("exclude")){
-
-              // The query is constructed like so for filter groups with exclude set to true:
-              // ( ( -( mainQuery OR idFilterQuery ) AND *:* ) OR isPartOfQuery ) AND categoryQuery
-              // Build the query string piece by piece:
-
-              // 1. mainQuery
-              queryString += mainQuery;
-              queryString = wrapInParentheses(queryString)
-              // 2. ( mainQuery OR idFilterQuery )
-              if (idFilterQuery.trim().length){
-                idOperator = this.get("mustMatchIds") ? "AND" : "OR"
-                queryString = addQueryFragment(queryString, idFilterQuery, idOperator)
-                queryString = wrapInParentheses(queryString)
-              }
-              // 3. -( mainQuery OR idFilterQuery )
-              if(queryString.trim().length){
-                queryString = "-" + queryString
-              }
-              // 4. ( -( mainQuery OR idFilterQuery ) AND *:* )  - see Filter model
-              // requiresPositiveClause for details on why positive clause is
-              // needed here
-              if (queryString.trim().length) {
-                queryString = addQueryFragment(queryString, "*:*", "AND")
-                queryString = wrapInParentheses(queryString)
-              }
-              // 5. ( ( -( mainQuery OR idFilterQuery ) AND *:* ) OR isPartOfQuery)
-              if (isPartOfQuery){
-                queryString = addQueryFragment(queryString, isPartOfQuery, "OR")
-                queryString = wrapInParentheses(queryString)
-              }
-              
-              // 6. (-( mainQuery OR idFilterQuery ) AND *:* OR isPartOfQuery) AND
-              //    categoryQuery
-              queryString = addQueryFragment(queryString, categoryQuery, "AND")
-              
-              
-            } else {
-
-              // The query is constructed like so for filter groups with exclude set to
-              // false: ( mainQuery OR idFilterQuery ) AND catalogQuery where
-              // idFilterQuery includes the isPartOfQuery
-
-              // 1. mainQuery
-              queryString += mainQuery;
-              queryString = wrapInParentheses(queryString)
-              // 2. ( mainQuery OR idFilterQuery )
-              if (idFilterQuery.trim().length) {
-                queryString = addQueryFragment(queryString, idFilterQuery, "OR")
-                queryString = wrapInParentheses(queryString)
-              }
-              // 3. ( mainQuery OR idFilterQuery ) AND catalogQuery
-              queryString = addQueryFragment(queryString, categoryQuery, "AND")
-              
+            // 6. (-( mainQuery OR idFilterQuery ) AND *:* OR isPartOfQuery) AND
+            //    categoryQuery
+            queryString = addQueryFragment(queryString, categoryQuery, "AND");
+          } else {
+            // The query is constructed like so for filter groups with exclude set to
+            // false: ( mainQuery OR idFilterQuery ) AND catalogQuery where
+            // idFilterQuery includes the isPartOfQuery
+
+            // 1. mainQuery
+            queryString += mainQuery;
+            queryString = wrapInParentheses(queryString);
+            // 2. ( mainQuery OR idFilterQuery )
+            if (idFilterQuery.trim().length) {
+              queryString = addQueryFragment(queryString, idFilterQuery, "OR");
+              queryString = wrapInParentheses(queryString);
             }
+            // 3. ( mainQuery OR idFilterQuery ) AND catalogQuery
+            queryString = addQueryFragment(queryString, categoryQuery, "AND");
+          }
 
-            return queryString
+          return queryString;
+        } catch (error) {
+          console.log(
+            "Error creating a query for a Filter Group, error details:" + error,
+          );
+        }
+      },
+
+      /**
+       * Overrides the default Backbone.Model.validate.function() to check if this
+       * FilterGroup model has all the required values.
+       *
+       * @param {Object} [attrs] - A literal object of model attributes to validate.
+       * @param {Object} [options] - A literal object of options for this validation
+       * process
+       * @return {Object} If there are errors, an object comprising error messages. If
+       * no errors, returns nothing.
+       */
+      validate: function () {
+        try {
+          var errors = {};
 
-          } catch (error) {
-            console.log("Error creating a query for a Filter Group, error details:" +
-              error
-            );
+          // The operator must be AND or OR
+          if (!["AND", "OR"].includes(this.get("operator"))) {
+            //Reset the value to the default rather than return an error
+            this.set("operator", this.defaults()["operator"]);
           }
-        },
-
-        /**
-         * Overrides the default Backbone.Model.validate.function() to check if this
-         * FilterGroup model has all the required values.
-         *
-         * @param {Object} [attrs] - A literal object of model attributes to validate.
-         * @param {Object} [options] - A literal object of options for this validation
-         * process
-         * @return {Object} If there are errors, an object comprising error messages. If
-         * no errors, returns nothing.
-        */
-        validate: function(){
-
-          try {
-            var errors = {};
-
-            // The operator must be AND or OR
-            if( !["AND", "OR"].includes(this.get("operator")) ){
-              //Reset the value to the default rather than return an error
-              this.set("operator", this.defaults()["operator"]);
-            }
 
-            //Exclude should always be a boolean
-            if( typeof this.get("exclude") !== "boolean" ){
-              // Reset the value to the default rather than return an error
-              this.set("exclude", this.defaults().exclude);
-            }
+          //Exclude should always be a boolean
+          if (typeof this.get("exclude") !== "boolean") {
+            // Reset the value to the default rather than return an error
+            this.set("exclude", this.defaults().exclude);
+          }
 
-            // Validate label, icon, and description for UI Filter Groups 
-            if(this.get("isUIFilterType")){
-              var textAttributes = ["label", "icon", "description"];
-              // These fields should be strings
-              _.each(textAttributes, function(attr){
-                if( typeof this.get(attr) !== "string" ){
+          // Validate label, icon, and description for UI Filter Groups
+          if (this.get("isUIFilterType")) {
+            var textAttributes = ["label", "icon", "description"];
+            // These fields should be strings
+            _.each(
+              textAttributes,
+              function (attr) {
+                if (typeof this.get(attr) !== "string") {
                   // Reset the value to the default rather than return an error
                   this.set(attr, this.defaults()[attr]);
                 }
-              }, this);
-              // If this filter group is not empty, and it's a UI Filter Group, then
-              // the group needs a label to be valid.
-              if(!this.isEmpty() && !this.get("label")){
-                // Set a generic label instead of returning an error
-                this.set("label", "Search")
-              }
-            }
-            
-            // There must be at least one filter or filter group within each group,
-            // and each filter must be valid.
-            if( this.get("filters").length == 0 ){
-              errors.noFilters = "At least one filter is required."
-            }
-            else{
-              this.get("filters").each(function(filter){
-                if( !filter.isValid() ){
-                  errors.filter = "At least one filter is invalid.";
-                }
-              });
+              },
+              this,
+            );
+            // If this filter group is not empty, and it's a UI Filter Group, then
+            // the group needs a label to be valid.
+            if (!this.isEmpty() && !this.get("label")) {
+              // Set a generic label instead of returning an error
+              this.set("label", "Search");
             }
+          }
 
-            if( Object.keys(errors).length ) {
-              return errors;
-            } else {
-              return;
-            }
+          // There must be at least one filter or filter group within each group,
+          // and each filter must be valid.
+          if (this.get("filters").length == 0) {
+            errors.noFilters = "At least one filter is required.";
+          } else {
+            this.get("filters").each(function (filter) {
+              if (!filter.isValid()) {
+                errors.filter = "At least one filter is invalid.";
+              }
+            });
+          }
 
-          } catch (error) {
-            console.log("Error validating a FilterGroup. Error details: " + error);
+          if (Object.keys(errors).length) {
+            return errors;
+          } else {
+            return;
           }
-        
-        },
-
-        /**    
-         * isEmpty - Checks whether this Filter Group has any filter models that are not
-         * empty.
-         *
-         * @return {boolean} returns true if the Filter Group has Filter models that are
-         * not empty
-         */  
-        isEmpty: function(){
-          try {
-            var filters = this.get("filters");
-            if(!filters || !filters.length){
-              return true
-            }
-            var subFilters = filters.getNonEmptyFilters();
-            if(!subFilters || !subFilters.length){
-              return true
-            } else {
-              return false
-            }
-          } catch (error) {
-            console.log("Error checking if a Filter Group is empty. Assuming it is not." +
-            " Error details: " + error);
-            return false
+        } catch (error) {
+          console.log(
+            "Error validating a FilterGroup. Error details: " + error,
+          );
+        }
+      },
+
+      /**
+       * isEmpty - Checks whether this Filter Group has any filter models that are not
+       * empty.
+       *
+       * @return {boolean} returns true if the Filter Group has Filter models that are
+       * not empty
+       */
+      isEmpty: function () {
+        try {
+          var filters = this.get("filters");
+          if (!filters || !filters.length) {
+            return true;
+          }
+          var subFilters = filters.getNonEmptyFilters();
+          if (!subFilters || !subFilters.length) {
+            return true;
+          } else {
+            return false;
+          }
+        } catch (error) {
+          console.log(
+            "Error checking if a Filter Group is empty. Assuming it is not." +
+              " Error details: " +
+              error,
+          );
+          return false;
+        }
+      },
+
+      /**
+       * Updates the XML DOM with the new values from the model
+       * @param {object} [options] A literal object with options for this serialization
+       * @return {XMLElement} An updated filterGroup XML element
+       */
+      updateDOM: function (options) {
+        try {
+          // Don't serialize an empty filter group
+          if (this.isEmpty()) {
+            return null;
           }
-        },
-
-        /**
-         * Updates the XML DOM with the new values from the model
-         * @param {object} [options] A literal object with options for this serialization
-         * @return {XMLElement} An updated filterGroup XML element
-        */
-        updateDOM: function(options){
-
-          try {
 
-            // Don't serialize an empty filter group
-            if(this.isEmpty()){
-              return null
+          // Clone the DOM if it exists
+          var objectDOM = this.get("objectDOM");
+
+          if (objectDOM) {
+            objectDOM = objectDOM.cloneNode(true);
+          } else {
+            // Create an XML filterGroup or definition element from scratch
+            if (!objectDOM) {
+              var name = this.get("nodeName");
+              objectDOM = new DOMParser().parseFromString(
+                "<" + name + "></" + name + ">",
+                "text/xml",
+              );
+              objectDOM = $(objectDOM).find(name)[0];
             }
+          }
 
-            // Clone the DOM if it exists
-            var objectDOM = this.get("objectDOM");
-
-            if(objectDOM){
-              objectDOM = objectDOM.cloneNode(true);
-            } else {
-              // Create an XML filterGroup or definition element from scratch
-              if(!objectDOM){
-                var name = this.get("nodeName");
-                objectDOM = new DOMParser().parseFromString(
-                  "<" + name + "></" + name + ">",
-                  "text/xml"
-                );
-                objectDOM = $(objectDOM).find(name)[0];
+          $(objectDOM).empty();
+
+          // label, description, and icon are elements that are used in Portal
+          // UIFilterGroupType filterGroups only. Collection FilterGroupType filterGroups
+          // do not use these elements.
+          if (this.get("isUIFilterType")) {
+            // Get the new values for the simple text elements
+            var filterGroupData = {
+              label: this.get("label"),
+              description: this.get("description"),
+              icon: this.get("icon"),
+            };
+            // Serialize the simple text elements
+            _.map(filterGroupData, function (value, nodeName) {
+              // Don't serialize falsey values
+              if (value) {
+                // Make new sub-node
+                var nodeSerialized =
+                  objectDOM.ownerDocument.createElement(nodeName);
+                $(nodeSerialized).text(value);
+                // Append new sub-node to objectDOM
+                $(objectDOM).append(nodeSerialized);
               }
-            }
+            });
+          }
 
-            $(objectDOM).empty();
+          // Serialize the filters
+          var filterModels = this.get("filters").models;
 
-            // label, description, and icon are elements that are used in Portal
-            // UIFilterGroupType filterGroups only. Collection FilterGroupType filterGroups
-            // do not use these elements.
-            if(this.get("isUIFilterType")){
+          // TODO: Remove filter types depending on isUIFilterType attribute?
+          // toggleFilter and choiceFilter are only allowed in Portal UIFilterGroupType.
+          // nested filterGroups are only allowed in Collection FilterGroupType.
 
-              // Get the new values for the simple text elements
-              var filterGroupData = {
-                label: this.get("label"),
-                description: this.get("description"),
-                icon: this.get("icon")
+          // Don't serialize falsey values
+          if (filterModels && filterModels.length) {
+            // Update each filter and append it to the DOM
+            _.each(filterModels, function (filterModel) {
+              if (filterModel) {
+                var filterModelSerialized = filterModel.updateDOM();
               }
-              // Serialize the simple text elements
-              _.map(filterGroupData, function (value, nodeName) {
-                // Don't serialize falsey values
-                if (value) {
-                  // Make new sub-node
-                  var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
-                  $(nodeSerialized).text(value);
-                  // Append new sub-node to objectDOM
-                  $(objectDOM).append(nodeSerialized);
-                }
-              });
-            }
-
-            // Serialize the filters
-            var filterModels = this.get("filters").models;
-
-            // TODO: Remove filter types depending on isUIFilterType attribute?
-            // toggleFilter and choiceFilter are only allowed in Portal UIFilterGroupType.
-            // nested filterGroups are only allowed in Collection FilterGroupType.
-
-            // Don't serialize falsey values
-            if (filterModels && filterModels.length) {
-              // Update each filter and append it to the DOM
-              _.each(filterModels, function (filterModel) {
-                if (filterModel) {
-                  var filterModelSerialized = filterModel.updateDOM();
-                }
-                $(objectDOM).append(filterModelSerialized);
-              });
-            }
-
-            // exclude and operator are elements used only in Collection FilterGroupType
-            // filterGroups. Portal UIFilterGroupType filterGroups do not use either of
-            // these elements.
-            if(!this.get("isUIFilterType")){
-              // The nodeName and model attribute are the same in these cases.
-              ["operator", "exclude"].forEach(function(nodeName){
-                // Don't serialize empty, null, undefined, or default values
-                var value = this.get(nodeName);
-                if( (value || value === false) && value !== this.defaults()[nodeName] ){
-                  // Make new sub-node
-                  var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
-                  $(nodeSerialized).text(value);
-                  // Append new sub-node to objectDOM
-                  $(objectDOM).append(nodeSerialized);
-                }
-              }, this);
-            }
-
-            // TODO: serialize the new <option> elements supported for Portal
-            // UIFilterGroupType 1.1.0
-            // if(this.get("isUIFilterType")){
-            //  ... serialize options ...
-            // }
+              $(objectDOM).append(filterModelSerialized);
+            });
+          }
 
-            return objectDOM
-          } catch (error) {
-            console.error("Unable to serialize a Filter Group.", error);
-            return this.get("objectDOM") || "";
+          // exclude and operator are elements used only in Collection FilterGroupType
+          // filterGroups. Portal UIFilterGroupType filterGroups do not use either of
+          // these elements.
+          if (!this.get("isUIFilterType")) {
+            // The nodeName and model attribute are the same in these cases.
+            ["operator", "exclude"].forEach(function (nodeName) {
+              // Don't serialize empty, null, undefined, or default values
+              var value = this.get(nodeName);
+              if (
+                (value || value === false) &&
+                value !== this.defaults()[nodeName]
+              ) {
+                // Make new sub-node
+                var nodeSerialized =
+                  objectDOM.ownerDocument.createElement(nodeName);
+                $(nodeSerialized).text(value);
+                // Append new sub-node to objectDOM
+                $(objectDOM).append(nodeSerialized);
+              }
+            }, this);
           }
 
-        }
+          // TODO: serialize the new <option> elements supported for Portal
+          // UIFilterGroupType 1.1.0
+          // if(this.get("isUIFilterType")){
+          //  ... serialize options ...
+          // }
 
-      });
+          return objectDOM;
+        } catch (error) {
+          console.error("Unable to serialize a Filter Group.", error);
+          return this.get("objectDOM") || "";
+        }
+      },
+    },
+  );
 
-    return FilterGroup;
-  });
+  return FilterGroup;
+});
 
diff --git a/docs/docs/src_js_models_filters_NumericFilter.js.html b/docs/docs/src_js_models_filters_NumericFilter.js.html index 4e5b02289..0715adf9b 100644 --- a/docs/docs/src_js_models_filters_NumericFilter.js.html +++ b/docs/docs/src_js_models_filters_NumericFilter.js.html @@ -44,376 +44,387 @@

Source: src/js/models/filters/NumericFilter.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/filters/Filter'],
-  function ($, _, Backbone, Filter) {
-
-    /**
-    * @class NumericFilter
-    * @classdesc A search filter whose search term is always an exact number or numbber range
-    * @classcategory Models/Filters
-    * @extends Filter
-    * @constructs
-    */
-    var NumericFilter = Filter.extend(
-    /** @lends NumericFilter.prototype */{
-
-        type: "NumericFilter",
-
-        /**
-        * Default attributes for this model
-        * @extends Filter#defaults
-        * @type {Object}
-        * @property {Date}    min - The minimum number to use in the query for this filter
-        * @property {Date}    max - The maximum number to use in the query for this filter
-        * @property {Date}    rangeMin - The lowest possible number that 'min' can be
-        * @property {Date}    rangeMax - The highest possible number that 'max' can be
-        * @property {string}  nodeName - The XML node name to use when serializing this model into XML
-        * @property {boolean} range - If true, this Filter will use a numeric range as the search term instead of an exact number
-        * @property {number}  step - The number to increase the search value by when incrementally increasing or decreasing the numeric range
-        */
-        defaults: function () {
-          return _.extend(Filter.prototype.defaults(), {
-            nodeName: "numericFilter",
-            min: null,
-            max: null,
-            rangeMin: null,
-            rangeMax: null,
-            range: true,
-            step: 1
+            
define(["jquery", "underscore", "backbone", "models/filters/Filter"], function (
+  $,
+  _,
+  Backbone,
+  Filter,
+) {
+  /**
+   * @class NumericFilter
+   * @classdesc A search filter whose search term is always an exact number or numbber range
+   * @classcategory Models/Filters
+   * @extends Filter
+   * @constructs
+   */
+  var NumericFilter = Filter.extend(
+    /** @lends NumericFilter.prototype */ {
+      type: "NumericFilter",
+
+      /**
+       * Default attributes for this model
+       * @extends Filter#defaults
+       * @type {Object}
+       * @property {Date}    min - The minimum number to use in the query for this filter
+       * @property {Date}    max - The maximum number to use in the query for this filter
+       * @property {Date}    rangeMin - The lowest possible number that 'min' can be
+       * @property {Date}    rangeMax - The highest possible number that 'max' can be
+       * @property {string}  nodeName - The XML node name to use when serializing this model into XML
+       * @property {boolean} range - If true, this Filter will use a numeric range as the search term instead of an exact number
+       * @property {number}  step - The number to increase the search value by when incrementally increasing or decreasing the numeric range
+       */
+      defaults: function () {
+        return _.extend(Filter.prototype.defaults(), {
+          nodeName: "numericFilter",
+          min: null,
+          max: null,
+          rangeMin: null,
+          rangeMax: null,
+          range: true,
+          step: 1,
+        });
+      },
+
+      initialize: function (attributes, options) {
+        const model = this;
+        Filter.prototype.initialize.call(this, attributes, options);
+
+        // Limit the range min, range max, and update step if the model switches from
+        // being a coordinate filter to a regular numeric filter or vice versa
+        model.listenTo(model, "change:fields", function () {
+          model.toggleCoordinateLimits();
+        });
+        model.toggleCoordinateLimits();
+      },
+
+      /**
+       * For filters that represent geographic coordinates, return the
+       * appropriate defaults for the NumericFilter model.
+       * @param {'latitude'|'longitude'} coord - The coordinate type to get
+       * defaults for.
+       * @returns {Object} The rangeMin, rangeMax, and step values for the
+       * given coordinate type
+       */
+      coordDefaults: function (coord = "longitude") {
+        return {
+          rangeMin: coord === "longitude" ? -180 : -90,
+          rangeMax: coord === "longitude" ? 180 : 90,
+          step: 0.00001,
+        };
+      },
+
+      /**
+       * Add or remove the rangeMin, rangeMax, and step associated with
+       * coordinate queries. If the filter is a coordinate filter, then add
+       * the appropriate defaults for the rangeMin, rangeMax, and step. If
+       * the filter is NOT a coordinate filter, then set rangeMin, rangeMax,
+       * and step to the regular defaults for a numeric filter.
+       * @param {Boolean} [overwrite=false] - By default, the rangeMin,
+       * rangeMax, and step will only be reset if they are currently set to
+       * one of the default values (e.g. if the model has default values for
+       * a numeric filter, they will be set to the default values for a
+       * coordinate filter). To change this behaviour to always reset the
+       * attributes to the new defaults values, set overwrite to true.
+       */
+      toggleCoordinateLimits: function (overwrite = false) {
+        try {
+          const model = this;
+          const lonDefaults = model.coordDefaults("longitude");
+          const latDefaults = model.coordDefaults("latitude");
+          const numDefaults = model.defaults();
+          const attrs = Object.keys(lonDefaults); // 'rangeMin', 'rangeMax', and 'step'
+
+          const isDefault = function (attr) {
+            const val = model.get(attr);
+            return (
+              val == numDefaults[attr] ||
+              val == latDefaults[attr] ||
+              val == lonDefaults[attr]
+            );
+          };
+
+          // When the model has changed to a numeric filter, set the range min, range max,
+          // and step to the default values for a numeric filter, if they are currently set
+          // to the default values for a coordinate filter (or when overwrite is true).
+          let defaultsToSet = numDefaults;
+
+          // When the model has changed to a coordinate filter, set the range min, range max,
+          // and step to the default values for a coordinate filter, if they are currently set
+          // to the default values for a numeric filter (or when overwrite is true).
+          if (model.isCoordinateQuery()) {
+            // Use longitude range (-180, 180) for longitude only queries, or queries with
+            // both longitude and latitude
+            defaultsToSet = lonDefaults;
+            if (model.isLatitudeQuery()) {
+              defaultsToSet = latDefaults;
+            }
+          }
+          attrs.forEach(function (attr) {
+            if (isDefault(attr) || overwrite) {
+              model.set(attr, defaultsToSet[attr]);
+            }
           });
-        },
 
-        initialize: function (attributes, options) {
+          model.limitToRange();
+          model.roundToStep();
+        } catch (error) {
+          console.log(
+            "There was an error toggling Coordinate limits in a NumericFilter" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
 
+      /**
+       * Ensures that the min, max, and value are within the rangeMin and rangeMax.
+       */
+      limitToRange: function () {
+        try {
           const model = this;
-          Filter.prototype.initialize.call(this, attributes, options);
+          const min = model.get("min");
+          const max = model.get("max");
 
-          // Limit the range min, range max, and update step if the model switches from
-          // being a coordinate filter to a regular numeric filter or vice versa
-          model.listenTo(model, 'change:fields', function () {
-            model.toggleCoordinateLimits()
-          })
-          model.toggleCoordinateLimits();
+          const rangeMin = model.get("rangeMin");
+          const rangeMax = model.get("rangeMax");
 
-        },
-
-        /**
-         * For filters that represent geographic coordinates, return the
-         * appropriate defaults for the NumericFilter model.
-         * @param {'latitude'|'longitude'} coord - The coordinate type to get
-         * defaults for.
-         * @returns {Object} The rangeMin, rangeMax, and step values for the
-         * given coordinate type
-         */
-        coordDefaults: function (coord = 'longitude') {
-          return {
-            rangeMin: coord === 'longitude' ? -180 : -90,
-            rangeMax: coord === 'longitude' ? 180 : 90,
-            step: 0.00001
-          }
-        },
-
-        /**
-         * Add or remove the rangeMin, rangeMax, and step associated with
-         * coordinate queries. If the filter is a coordinate filter, then add
-         * the appropriate defaults for the rangeMin, rangeMax, and step. If
-         * the filter is NOT a coordinate filter, then set rangeMin, rangeMax,
-         * and step to the regular defaults for a numeric filter.
-         * @param {Boolean} [overwrite=false] - By default, the rangeMin,
-         * rangeMax, and step will only be reset if they are currently set to
-         * one of the default values (e.g. if the model has default values for
-         * a numeric filter, they will be set to the default values for a
-         * coordinate filter). To change this behaviour to always reset the
-         * attributes to the new defaults values, set overwrite to true.
-         */
-        toggleCoordinateLimits: function (overwrite = false) {
-          try {
-            const model = this;
-            const lonDefaults = model.coordDefaults('longitude');
-            const latDefaults = model.coordDefaults('latitude');
-            const numDefaults = model.defaults();
-            const attrs = Object.keys(lonDefaults); // 'rangeMin', 'rangeMax', and 'step'
-
-            const isDefault = function (attr) {
-              const val = model.get(attr)
-              return (val == numDefaults[attr]) || (val == latDefaults[attr]) || (val == lonDefaults[attr])
+          const values = model.get("values");
+          const value = values != null && values.length ? values[0] : null;
+
+          // Set MIN to min or max if it is outside the range
+          if (min != null) {
+            if (rangeMin != null && min < rangeMin) {
+              model.set("min", rangeMin);
+            }
+            if (rangeMax != null && min > rangeMax) {
+              model.set("min", rangeMax);
             }
+          }
 
-            // When the model has changed to a numeric filter, set the range min, range max,
-            // and step to the default values for a numeric filter, if they are currently set
-            // to the default values for a coordinate filter (or when overwrite is true).
-            let defaultsToSet = numDefaults
-
-            // When the model has changed to a coordinate filter, set the range min, range max,
-            // and step to the default values for a coordinate filter, if they are currently set
-            // to the default values for a numeric filter (or when overwrite is true).
-            if (model.isCoordinateQuery()) {
-              // Use longitude range (-180, 180) for longitude only queries, or queries with
-              // both longitude and latitude
-              defaultsToSet = lonDefaults
-              if (model.isLatitudeQuery()) {
-                defaultsToSet = latDefaults
-              }
+          // Set the MAX to min or max if it is outside the range
+          if (max != null) {
+            if (rangeMax != null && max > rangeMax) {
+              model.set("max", rangeMax);
             }
-            attrs.forEach(function (attr) {
-              if (isDefault(attr) || overwrite) {
-                model.set(attr, defaultsToSet[attr])
-              }
-            })
+            if (rangeMin != null && max < rangeMin) {
+              model.set("max", rangeMin);
+            }
+          }
 
-            model.limitToRange()
-            model.roundToStep()
+          // Set the VALUE to min or max if it is outside the range
+          if (value != null) {
+            if (rangeMax != null && value > rangeMax) {
+              values[0] = rangeMax;
+              model.set("values", values);
+            }
+            if (rangeMin != null && value < rangeMin) {
+              values[0] = rangeMin;
+              model.set("values", values);
+            }
           }
-          catch (error) {
-            console.log(
-              'There was an error toggling Coordinate limits in a NumericFilter' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error limiting a NumericFilter to the range" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Rounds the min, max, and/or value to the same number of decimal
+       * places as the step.
+       */
+      roundToStep: function () {
+        try {
+          const model = this;
+          const min = model.get("min");
+          const max = model.get("max");
+          const step = model.get("step");
+
+          const values = model.get("values");
+          const value = values != null && values.length ? values[0] : null;
+
+          // Returns the number of decimal places in a number
+          function countDecimals(n) {
+            let text = n.toString();
+            // verify if number 0.000005 is represented as "5e-6"
+            if (text.indexOf("e-") > -1) {
+              let [base, trail] = text.split("e-");
+              let deg = parseInt(trail, 10);
+              return deg;
+            }
+            // count decimals for number in representation like "0.123456"
+            if (Math.floor(n) !== n) {
+              return n.toString().split(".")[1].length || 0;
+            }
+            return 0;
           }
-        },
-
-        /**
-         * Ensures that the min, max, and value are within the rangeMin and rangeMax.
-         */
-        limitToRange: function () {
-          try {
-            const model = this;
-            const min = model.get('min');
-            const max = model.get('max');
-
-            const rangeMin = model.get('rangeMin');
-            const rangeMax = model.get('rangeMax');
 
-            const values = model.get('values');
-            const value = values != null && values.length ? values[0] : null;
+          // Rounds a number to the specified number of decimal places
+          function roundTo(n, digits) {
+            if (digits === undefined) {
+              digits = 0;
+            }
+            const multiplicator = Math.pow(10, digits);
+            n = parseFloat((n * multiplicator).toFixed(11));
+            const test = Math.round(n) / multiplicator;
+            return +test.toFixed(digits);
+          }
 
-            // Set MIN to min or max if it is outside the range
+          // Round min & max to number of decimal places in step
+          if (step != null) {
+            let digits = countDecimals(step);
             if (min != null) {
-              if (rangeMin != null && min < rangeMin) {
-                model.set('min', rangeMin);
-              }
-              if (rangeMax != null && min > rangeMax) {
-                model.set('min', rangeMax);
-              }
+              model.set("min", roundTo(min, digits));
             }
-
-            // Set the MAX to min or max if it is outside the range
             if (max != null) {
-              if (rangeMax != null && max > rangeMax) {
-                model.set('max', rangeMax);
-              }
-              if (rangeMin != null && max < rangeMin) {
-                model.set('max', rangeMin);
-              }
+              model.set("max", roundTo(max, digits));
             }
-
-            // Set the VALUE to min or max if it is outside the range
             if (value != null) {
-              if (rangeMax != null && value > rangeMax) {
-                values[0] = rangeMax;
-                model.set('values', values);
-              }
-              if (rangeMin != null && value < rangeMin) {
-                values[0] = rangeMin;
-                model.set('values', values);
-              }
+              values[0] = roundTo(value, digits);
+              model.set("values", values);
             }
           }
-          catch (error) {
-            console.log(
-              'There was an error limiting a NumericFilter to the range' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Rounds the min, max, and/or value to the same number of decimal
-         * places as the step.
-         */
-        roundToStep: function () {
-          try {
-            const model = this;
-            const min = model.get('min');
-            const max = model.get('max');
-            const step = model.get('step');
-
-            const values = model.get('values');
-            const value = values != null && values.length ? values[0] : null;
-
-            // Returns the number of decimal places in a number
-            function countDecimals(n) {
-              let text = n.toString()
-              // verify if number 0.000005 is represented as "5e-6"
-              if (text.indexOf('e-') > -1) {
-                let [base, trail] = text.split('e-');
-                let deg = parseInt(trail, 10);
-                return deg;
-              }
-              // count decimals for number in representation like "0.123456"
-              if (Math.floor(n) !== n) {
-                return n.toString().split(".")[1].length || 0;
-              }
-              return 0;
-            }
-
-            // Rounds a number to the specified number of decimal places
-            function roundTo(n, digits) {
-              if (digits === undefined) {
-                digits = 0;
-              }
-              const multiplicator = Math.pow(10, digits);
-              n = parseFloat((n * multiplicator).toFixed(11));
-              const test = (Math.round(n) / multiplicator);
-              return +(test.toFixed(digits));
-            }
-
-            // Round min & max to number of decimal places in step
-            if (step != null) {
-              let digits = countDecimals(step)
-              if (min != null) {
-                model.set('min', roundTo(min, digits))
-              }
-              if (max != null) {
-                model.set('max', roundTo(max, digits))
-              }
-              if (value != null) {
-                values[0] = roundTo(value, digits)
-                model.set('values', values)
-              }
-            }
+        } catch (error) {
+          console.log(
+            "There was an error rounding values in a NumericFilter to the step" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Parses the numericFilter XML node into JSON
+       *
+       * @param {Element} xml - The XML Element that contains all the NumericFilter elements
+       * @return {JSON} - The JSON object literal to be set on the model
+       */
+      parse: function (xml) {
+        try {
+          var modelJSON = Filter.prototype.parse.call(this, xml);
+
+          //Get the rangeMin and rangeMax nodes
+          var rangeMinNode = $(xml).find("rangeMin"),
+            rangeMaxNode = $(xml).find("rangeMax");
+
+          //Parse the range min
+          if (rangeMinNode.length) {
+            modelJSON.rangeMin = parseFloat(rangeMinNode[0].textContent);
           }
-          catch (error) {
-            console.log(
-              'There was an error rounding values in a NumericFilter to the step' +
-              '. Error details: ' + error
-            );
+          //Parse the range max
+          if (rangeMaxNode.length) {
+            modelJSON.rangeMax = parseFloat(rangeMaxNode[0].textContent);
           }
-        },
-
-        /**
-        * Parses the numericFilter XML node into JSON
-        *
-        * @param {Element} xml - The XML Element that contains all the NumericFilter elements
-        * @return {JSON} - The JSON object literal to be set on the model
-        */
-        parse: function (xml) {
-
-          try {
-            var modelJSON = Filter.prototype.parse.call(this, xml);
-
-            //Get the rangeMin and rangeMax nodes
-            var rangeMinNode = $(xml).find("rangeMin"),
-              rangeMaxNode = $(xml).find("rangeMax");
-
-            //Parse the range min
-            if (rangeMinNode.length) {
-              modelJSON.rangeMin = parseFloat(rangeMinNode[0].textContent);
-            }
-            //Parse the range max
-            if (rangeMaxNode.length) {
-              modelJSON.rangeMax = parseFloat(rangeMaxNode[0].textContent);
-            }
 
-            //If this Filter is in a filter group, don't parse the values
-            if (!this.get("isUIFilterType")) {
-              //Get the min, max, and value nodes
-              var minNode = $(xml).find("min"),
-                maxNode = $(xml).find("max"),
-                valueNode = $(xml).find("value");
+          //If this Filter is in a filter group, don't parse the values
+          if (!this.get("isUIFilterType")) {
+            //Get the min, max, and value nodes
+            var minNode = $(xml).find("min"),
+              maxNode = $(xml).find("max"),
+              valueNode = $(xml).find("value");
 
-              //Parse the min value
-              if (minNode.length) {
-                modelJSON.min = parseFloat(minNode[0].textContent);
-              }
-              //Parse the max value
-              if (maxNode.length) {
-                modelJSON.max = parseFloat(maxNode[0].textContent);
-              }
-              //Parse the value
-              if (valueNode.length) {
-                modelJSON.values = [parseFloat(valueNode[0].textContent)];
-              }
+            //Parse the min value
+            if (minNode.length) {
+              modelJSON.min = parseFloat(minNode[0].textContent);
             }
-            //If a range min and max was given, or if a min and max value was given,
-            // then this NumericFilter should be presented as a numeric range (rather than
-            // an exact numeric value).
-            if (rangeMinNode.length || rangeMaxNode.length || (minNode.length && maxNode.length)) {
-              //Set the range attribute on the JSON
-              modelJSON.range = true;
+            //Parse the max value
+            if (maxNode.length) {
+              modelJSON.max = parseFloat(maxNode[0].textContent);
             }
-            else {
-              //Set the range attribute on the JSON
-              modelJSON.range = false;
-            }
-
-            //If a range step was given, save it
-            if (modelJSON.range) {
-              var stepNode = $(xml).find("step");
-
-              if (stepNode.length) {
-                //Parse the text content of the node into a float
-                modelJSON.step = parseFloat(stepNode[0].textContent);
-              }
+            //Parse the value
+            if (valueNode.length) {
+              modelJSON.values = [parseFloat(valueNode[0].textContent)];
             }
           }
-          catch (e) {
-            //If an error occurred while parsing the XML, return a blank JS object
-            //(i.e. this model will just have the default values).
-            return {};
-          }
-
-          return modelJSON;
-        },
-
-
-        /**
-         * Builds a query string that represents this filter.
-         *
-         * @return {string} The query string to send to Solr
-         */
-        getQuery: function () {
-
-          //Start the query string
-          var queryString = "";
-
+          //If a range min and max was given, or if a min and max value was given,
+          // then this NumericFilter should be presented as a numeric range (rather than
+          // an exact numeric value).
           if (
-            // For numeric filters that are ranges, only construct the query if the min or max
-            // is different than the default
-            this.get("min") != this.get("rangeMin") ||
-            this.get("max") != this.get("rangeMax") ||
-            // Otherwise, a numeric filter could search for an exact value
-            (this.get("values") && this.get("values").length)
-
+            rangeMinNode.length ||
+            rangeMaxNode.length ||
+            (minNode.length && maxNode.length)
           ) {
+            //Set the range attribute on the JSON
+            modelJSON.range = true;
+          } else {
+            //Set the range attribute on the JSON
+            modelJSON.range = false;
+          }
 
-            //Iterate over each filter field and add to the query string
-            _.each(this.get("fields"), function (field, i, allFields) {
+          //If a range step was given, save it
+          if (modelJSON.range) {
+            var stepNode = $(xml).find("step");
 
+            if (stepNode.length) {
+              //Parse the text content of the node into a float
+              modelJSON.step = parseFloat(stepNode[0].textContent);
+            }
+          }
+        } catch (e) {
+          //If an error occurred while parsing the XML, return a blank JS object
+          //(i.e. this model will just have the default values).
+          return {};
+        }
+
+        return modelJSON;
+      },
+
+      /**
+       * Builds a query string that represents this filter.
+       *
+       * @return {string} The query string to send to Solr
+       */
+      getQuery: function () {
+        //Start the query string
+        var queryString = "";
+
+        if (
+          // For numeric filters that are ranges, only construct the query if the min or max
+          // is different than the default
+          this.get("min") != this.get("rangeMin") ||
+          this.get("max") != this.get("rangeMax") ||
+          // Otherwise, a numeric filter could search for an exact value
+          (this.get("values") && this.get("values").length)
+        ) {
+          //Iterate over each filter field and add to the query string
+          _.each(
+            this.get("fields"),
+            function (field, i, allFields) {
               //Get the minimum, maximum, and value.
               var max = this.get("max"),
                 min = this.get("min"),
                 value = this.get("values") ? this.get("values")[0] : null,
-                escapeMinus = function (val) { return val.toString().replace("-", "\\%2D") },
-                exists = function (val) { return val !== null && val !== undefined }
-
+                escapeMinus = function (val) {
+                  return val.toString().replace("-", "\\%2D");
+                },
+                exists = function (val) {
+                  return val !== null && val !== undefined;
+                };
 
               //Construct a query string for ranges, min, or max
-              if (
-                this.get("range") ||
-                (max || max === 0) ||
-                (min || min === 0)
-              ) {
-
+              if (this.get("range") || max || max === 0 || min || min === 0) {
                 //If no min or max was set, but there is a value, construct an exact value match query
-                if (!min && min !== 0 && !max && max !== 0 && (value || value === 0)) {
+                if (
+                  !min &&
+                  min !== 0 &&
+                  !max &&
+                  max !== 0 &&
+                  (value || value === 0)
+                ) {
                   // Escape the minus sign if needed
                   queryString += field + ":" + escapeMinus(value);
                 }
                 //If there is no min or max or value, set an empty query string
-                else if (!min && min !== 0 && !max && max !== 0 &&
-                  (!value && value !== 0)) {
+                else if (
+                  !min &&
+                  min !== 0 &&
+                  !max &&
+                  max !== 0 &&
+                  !value &&
+                  value !== 0
+                ) {
                   queryString = "";
                 }
                 //If there is at least a min or max
@@ -427,12 +438,18 @@ 

Source: src/js/models/filters/NumericFilter.js

min = "*"; } //If the max is higher than the min, set the max to a wildcard (unbounded) - else if (exists(max) && exists(min) && (max < min)) { + else if (exists(max) && exists(min) && max < min) { max = "*"; } //Add the range for this field to the query string - queryString += field + ":[" + escapeMinus(min) + "%20TO%20" + escapeMinus(max) + "]"; + queryString += + field + + ":[" + + escapeMinus(min) + + "%20TO%20" + + escapeMinus(max) + + "]"; } } //If there is a value set, construct an exact numeric match query @@ -445,219 +462,239 @@

Source: src/js/models/filters/NumericFilter.js

if (allFields[i + 1] && queryString.length) { queryString += "%20" + this.get("fieldsOperator") + "%20"; } + }, + this, + ); - }, this); - - //If there is more than one field, wrap the query in parentheses - if (this.get("fields").length > 1 && queryString.length) { - queryString = "(" + queryString + ")"; - } - + //If there is more than one field, wrap the query in parentheses + if (this.get("fields").length > 1 && queryString.length) { + queryString = "(" + queryString + ")"; } + } - return queryString; - - }, - - /** - * Updates the XML DOM with the new values from the model - * @inheritdoc - * @return {XMLElement} An updated numericFilter XML element from a portal document - */ - updateDOM: function (options) { - - try { - if (typeof options == "undefined") { - var options = {}; - } - - var objectDOM = Filter.prototype.updateDOM.call(this); + return queryString; + }, + + /** + * Updates the XML DOM with the new values from the model + * @inheritdoc + * @return {XMLElement} An updated numericFilter XML element from a portal document + */ + updateDOM: function (options) { + try { + if (typeof options == "undefined") { + var options = {}; + } - //Numeric Filters don't use matchSubstring nodes - $(objectDOM).children("matchSubstring").remove(); + var objectDOM = Filter.prototype.updateDOM.call(this); - //Get a clone of the original DOM - var originalDOM; - if (this.get("objectDOM")) { - originalDOM = this.get("objectDOM").cloneNode(true); - } + //Numeric Filters don't use matchSubstring nodes + $(objectDOM).children("matchSubstring").remove(); - // Get new numeric data - var numericData = { - min: this.get("min"), - max: this.get("max") - }; - - if (this.get("isUIFilterType")) { - numericData = _.extend(numericData, { - rangeMin: this.get("rangeMin"), - rangeMax: this.get("rangeMax"), - step: this.get("step") - }); - } + //Get a clone of the original DOM + var originalDOM; + if (this.get("objectDOM")) { + originalDOM = this.get("objectDOM").cloneNode(true); + } - // Make subnodes and append to DOM - _.map(numericData, function (value, nodeName) { + // Get new numeric data + var numericData = { + min: this.get("min"), + max: this.get("max"), + }; + + if (this.get("isUIFilterType")) { + numericData = _.extend(numericData, { + rangeMin: this.get("rangeMin"), + rangeMax: this.get("rangeMax"), + step: this.get("step"), + }); + } + // Make subnodes and append to DOM + _.map( + numericData, + function (value, nodeName) { if (value || value === 0) { - //If this value is the same as the default value, but it wasn't previously serialized, - if ((value == this.defaults()[nodeName]) && + if ( + value == this.defaults()[nodeName] && (!$(originalDOM).children(nodeName).length || - ($(originalDOM).children(nodeName).text() != value + "-01-01T00:00:00Z"))) { + $(originalDOM).children(nodeName).text() != + value + "-01-01T00:00:00Z") + ) { return; } - var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName); + var nodeSerialized = + objectDOM.ownerDocument.createElement(nodeName); $(nodeSerialized).text(value); $(objectDOM).append(nodeSerialized); } - - }, this); - - //Remove filterOptions for collection definition filters - if (!this.get("isUIFilterType")) { - $(objectDOM).children("filterOptions").remove(); - } - else { - //Make sure the filterOptions are listed last - //Get the filterOptions element - var filterOptions = $(objectDOM).children("filterOptions"); - //If the filterOptions exist - if (filterOptions.length) { - //Detach from their current position and append to the end - filterOptions.detach(); - $(objectDOM).append(filterOptions); - } + }, + this, + ); + + //Remove filterOptions for collection definition filters + if (!this.get("isUIFilterType")) { + $(objectDOM).children("filterOptions").remove(); + } else { + //Make sure the filterOptions are listed last + //Get the filterOptions element + var filterOptions = $(objectDOM).children("filterOptions"); + //If the filterOptions exist + if (filterOptions.length) { + //Detach from their current position and append to the end + filterOptions.detach(); + $(objectDOM).append(filterOptions); } - - // If there is a min or max or both, there must not be a value - if (numericData.min || numericData.min === 0 || numericData.max || numericData.max === 0) { - $(objectDOM).children("value").remove(); - } - - return objectDOM; } - catch (e) { - return ""; - } - - }, - /** - * Creates a human-readable string that represents the value set on this model - * @return {string} - */ - getReadableValue: function () { - - var readableValue = ""; - - var min = this.get("min"), - max = this.get("max"), - value = this.get("values")[0]; + // If there is a min or max or both, there must not be a value + if ( + numericData.min || + numericData.min === 0 || + numericData.max || + numericData.max === 0 + ) { + $(objectDOM).children("value").remove(); + } - if (!value && value !== 0) { - //If there is a min and max - if ((min || min === 0) && (max || max === 0)) { - readableValue = min + " to " + max; - } - //If there is only a max - else if (max || max === 0) { - readableValue = "No more than " + max; - } - else { - readableValue = "At least " + min; - } + return objectDOM; + } catch (e) { + return ""; + } + }, + + /** + * Creates a human-readable string that represents the value set on this model + * @return {string} + */ + getReadableValue: function () { + var readableValue = ""; + + var min = this.get("min"), + max = this.get("max"), + value = this.get("values")[0]; + + if (!value && value !== 0) { + //If there is a min and max + if ((min || min === 0) && (max || max === 0)) { + readableValue = min + " to " + max; } - else { - readableValue = value; + //If there is only a max + else if (max || max === 0) { + readableValue = "No more than " + max; + } else { + readableValue = "At least " + min; } + } else { + readableValue = value; + } - return readableValue; - - }, - - /** - * @inheritdoc - */ - hasChangedValues: function () { - - return (this.get("values").length > 0 || - this.get("min") != this.defaults().min || - this.get("max") != this.defaults().max); - - }, - - /** - * Checks if the values set on this model are valid and expected - * @return {object} - Returns a literal object with the invalid attributes and their corresponding error message - */ - validate: function () { - - //Validate most of the NumericFilter attributes using the parent validate function - var errors = Filter.prototype.validate.call(this); + return readableValue; + }, + + /** + * @inheritdoc + */ + hasChangedValues: function () { + return ( + this.get("values").length > 0 || + this.get("min") != this.defaults().min || + this.get("max") != this.defaults().max + ); + }, + + /** + * Checks if the values set on this model are valid and expected + * @return {object} - Returns a literal object with the invalid attributes and their corresponding error message + */ + validate: function () { + //Validate most of the NumericFilter attributes using the parent validate function + var errors = Filter.prototype.validate.call(this); + + //If everything is valid so far, then we have to create a new object to store errors + if (typeof errors != "object") { + errors = {}; + } - //If everything is valid so far, then we have to create a new object to store errors - if (typeof errors != "object") { - errors = {}; - } + //Delete error messages for the attributes that are going to be validated specially for the NumericFilter + delete errors.values; + delete errors.min; + delete errors.max; + delete errors.rangeMin; + delete errors.rangeMax; - //Delete error messages for the attributes that are going to be validated specially for the NumericFilter - delete errors.values; - delete errors.min; - delete errors.max; - delete errors.rangeMin; - delete errors.rangeMax; - - //If there is an exact number set as the search term - if (Array.isArray(this.get("values")) && this.get("values").length) { - //Check that all the values are numbers - if (_.find(this.get("values"), function (n) { return typeof n != "number" })) { - errors.values = "All of the search terms for this filter need to be numbers."; - } - } - //If there is a search term set on the model that is not an array, or number, - // or undefined, or null, then it is some other invalid value like a string or date. - else if (!Array.isArray(this.get("values")) && typeof values != "number" && typeof values != "undefined" && values !== null) { - errors.values = "The search term for this filter needs to a number."; - } - //Check that the min and max values are in order, if the minimum is not the default value of 0 - else if (typeof this.get("min") == "number" && typeof this.get("max") == "number") { - if (this.get("min") > this.get("max") && this.get("min") != 0) { - errors.min = "The minimum is after the maximum. The minimum must be a number less than the maximum, which is " + this.get("max"); - } - } - //If there is only a minimum number specified, check that it is a number - else if (this.get("min") && typeof this.get("min") != "number") { - errors.min = "The minimum needs to be a number." - if (this.get("max") && typeof this.get("max") != "number") { - errors.max = "The maximum needs to be a number." - } - } - //Check if the maximum is a value other than a number - else if (this.get("max") && typeof this.get("max") != "number") { - errors.max = "The maximum needs to be a number." - } - //If there is no min, max, or value, then return an errors - else if (!this.get("max") && this.get("max") !== 0 && !this.get("min") && this.get("min") !== 0 && - ((!this.get("values") && this.get("values") !== 0) || (Array.isArray(this.get("values")) && !this.get("values").length))) { - errors.values = "This search filter needs an exact number or a number range to use in the search query." + //If there is an exact number set as the search term + if (Array.isArray(this.get("values")) && this.get("values").length) { + //Check that all the values are numbers + if ( + _.find(this.get("values"), function (n) { + return typeof n != "number"; + }) + ) { + errors.values = + "All of the search terms for this filter need to be numbers."; } - - //Return the error messages - if (Object.keys(errors).length) { - return errors; + } + //If there is a search term set on the model that is not an array, or number, + // or undefined, or null, then it is some other invalid value like a string or date. + else if ( + !Array.isArray(this.get("values")) && + typeof values != "number" && + typeof values != "undefined" && + values !== null + ) { + errors.values = "The search term for this filter needs to a number."; + } + //Check that the min and max values are in order, if the minimum is not the default value of 0 + else if ( + typeof this.get("min") == "number" && + typeof this.get("max") == "number" + ) { + if (this.get("min") > this.get("max") && this.get("min") != 0) { + errors.min = + "The minimum is after the maximum. The minimum must be a number less than the maximum, which is " + + this.get("max"); } - else { - return; + } + //If there is only a minimum number specified, check that it is a number + else if (this.get("min") && typeof this.get("min") != "number") { + errors.min = "The minimum needs to be a number."; + if (this.get("max") && typeof this.get("max") != "number") { + errors.max = "The maximum needs to be a number."; } - + } + //Check if the maximum is a value other than a number + else if (this.get("max") && typeof this.get("max") != "number") { + errors.max = "The maximum needs to be a number."; + } + //If there is no min, max, or value, then return an errors + else if ( + !this.get("max") && + this.get("max") !== 0 && + !this.get("min") && + this.get("min") !== 0 && + ((!this.get("values") && this.get("values") !== 0) || + (Array.isArray(this.get("values")) && !this.get("values").length)) + ) { + errors.values = + "This search filter needs an exact number or a number range to use in the search query."; } - }); + //Return the error messages + if (Object.keys(errors).length) { + return errors; + } else { + return; + } + }, + }, + ); - return NumericFilter; - }); + return NumericFilter; +});
diff --git a/docs/docs/src_js_models_filters_SpatialFilter.js.html b/docs/docs/src_js_models_filters_SpatialFilter.js.html index 43eb32c97..09e7a8d23 100644 --- a/docs/docs/src_js_models_filters_SpatialFilter.js.html +++ b/docs/docs/src_js_models_filters_SpatialFilter.js.html @@ -48,7 +48,7 @@

Source: src/js/models/filters/SpatialFilter.js

"underscore", "jquery", "models/filters/Filter", - "collections/maps/Geohashes" + "collections/maps/Geohashes", ], function (_, $, Filter, Geohashes) { /** * @classdesc A SpatialFilter represents a spatial constraint on the query to @@ -178,7 +178,7 @@

Source: src/js/models/filters/SpatialFilter.js

* a GeoBoundingBox model * @since 2.25.0 */ - getBounds: function (as="object") { + getBounds: function (as = "object") { const coords = { north: this.get("north"), south: this.get("south"), @@ -291,7 +291,7 @@

Source: src/js/models/filters/SpatialFilter.js

if (precisions.length === 1) { return this.createBaseFilter( precisions, - geohashes.getAllHashStrings() + geohashes.getAllHashStrings(), ).getQuery(); } @@ -304,8 +304,8 @@

Source: src/js/models/filters/SpatialFilter.js

filters.add( this.createBaseFilter( [precision], - geohashes.getAllHashStrings(precision) - ) + geohashes.getAllHashStrings(precision), + ), ); } }); @@ -359,7 +359,7 @@

Source: src/js/models/filters/SpatialFilter.js

//Insert the matchSubstring node $(matchSubstringNode).insertBefore( - $updatedDOM.children("value").first() + $updatedDOM.children("value").first(), ); //Return the updated DOM @@ -380,7 +380,7 @@

Source: src/js/models/filters/SpatialFilter.js

this.removeListeners(); let df = this.defaults(); - + this.set({ values: df.values, east: df.east, @@ -389,11 +389,11 @@

Source: src/js/models/filters/SpatialFilter.js

south: df.south, height: df.height, }); - + // Reset the listeners this.setListeners(); }, - } + }, ); return SpatialFilter; }); diff --git a/docs/docs/src_js_models_filters_ToggleFilter.js.html b/docs/docs/src_js_models_filters_ToggleFilter.js.html index e843a64c5..af0e6d2c7 100644 --- a/docs/docs/src_js_models_filters_ToggleFilter.js.html +++ b/docs/docs/src_js_models_filters_ToggleFilter.js.html @@ -44,146 +44,152 @@

Source: src/js/models/filters/ToggleFilter.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/filters/Filter'],
-    function($, _, Backbone, Filter) {
-
+            
define(["jquery", "underscore", "backbone", "models/filters/Filter"], function (
+  $,
+  _,
+  Backbone,
+  Filter,
+) {
   /**
-  * @class ToggleFilter
-  * @classdesc A search filter whose search term is only one of two opposing choices
-  * @classcategory Models/Filters
-  * @constructs ToggleFilter
-  * @extends Filter
-  */
-	var ToggleFilter = Filter.extend(
-    /** @lends ToggleFilter.prototype */{
-
-    type: "ToggleFilter",
-
-    /**
-    * The Backbone Model attributes set on this ToggleFilter
-    * @type {object}
-    * @extends Filter#defaultts
-    * @property {string}         trueLabel - A human-readable label for the first search term
-    * @property {string}         falseLabel - A human-readable label for the second search term
-    * @property {string|boolean} trueValue - The exact search value to use for search term one
-    * @property {string|boolean} falseValue - The exact search value to use for search term two
-    * @property {string}         nodeName - The XML node name to use when serializing this model into XML
-    */
-    defaults: function(){
-      return _.extend(Filter.prototype.defaults(), {
-        trueLabel: "On",
-        trueValue: null,
-        falseLabel: "Off",
-        falseValue: null,
-        nodeName: "toggleFilter"
-      });
-    },
-
-    /*
-    * Parses the ToggleFilter XML node into JSON
-    *
-    * @param {Element} xml - The XML Element that contains all the ToggleFilter elements
-    * @return {JSON} - The JSON object literal to be set on the model
-    */
-    parse: function(xml){
-
-      var modelJSON = Filter.prototype.parse.call(this, xml);
-
-      //Parse the trueLabel and falseLabels
-      modelJSON.trueLabel = this.parseTextNode(xml, "trueLabel");
-      modelJSON.trueValue = this.parseTextNode(xml, "trueValue");
-      modelJSON.falseLabel = this.parseTextNode(xml, "falseLabel");
-      modelJSON.falseValue = this.parseTextNode(xml, "falseValue");
-
-      //Delete any attributes from the JSON that don't exist in the XML
-      if( !modelJSON.trueLabel ){
-        delete modelJSON.trueLabel;
-      }
-      if( !modelJSON.falseLabel ){
-        delete modelJSON.falseLabel;
-      }
-      if( !modelJSON.trueValue && modelJSON.trueValue !== false ){
-        delete modelJSON.trueValue;
-      }
-      if( !modelJSON.falseValue && modelJSON.falseValue !== false ){
-        delete modelJSON.falseValue;
-      }
-
-      return modelJSON;
-    },
-
-    /**
-     * Updates the XML DOM with the new values from the model
-     *  @inheritdoc
-     *  @return {XMLElement} An updated toggleFilter XML element from a portal document
-    */
-    updateDOM: function(options){
-
-      try{
-        var objectDOM = Filter.prototype.updateDOM.call(this, options);
-
-        if( (typeof options == "undefined") || (typeof options == "object" && this.get("isUIFilterType")) ){
-
-          var toggleData = {
-            trueValue: this.get("trueValue"),
-            trueLabel: this.get("trueLabel"),
-            falseValue: this.get("falseValue"),
-            falseLabel: this.get("falseLabel")
-          }
+   * @class ToggleFilter
+   * @classdesc A search filter whose search term is only one of two opposing choices
+   * @classcategory Models/Filters
+   * @constructs ToggleFilter
+   * @extends Filter
+   */
+  var ToggleFilter = Filter.extend(
+    /** @lends ToggleFilter.prototype */ {
+      type: "ToggleFilter",
 
-          // Make and append new subnodes
-          _.map(toggleData, function(value, nodeName){
-
-            // Remove the node if it exists in the DOM already
-            $(objectDOM).find(nodeName).remove();
-
-            // Don't serialize falsey or default values
-            if((value || value === false) && value != this.defaults()[nodeName]){
+      /**
+       * The Backbone Model attributes set on this ToggleFilter
+       * @type {object}
+       * @extends Filter#defaultts
+       * @property {string}         trueLabel - A human-readable label for the first search term
+       * @property {string}         falseLabel - A human-readable label for the second search term
+       * @property {string|boolean} trueValue - The exact search value to use for search term one
+       * @property {string|boolean} falseValue - The exact search value to use for search term two
+       * @property {string}         nodeName - The XML node name to use when serializing this model into XML
+       */
+      defaults: function () {
+        return _.extend(Filter.prototype.defaults(), {
+          trueLabel: "On",
+          trueValue: null,
+          falseLabel: "Off",
+          falseValue: null,
+          nodeName: "toggleFilter",
+        });
+      },
 
-              var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
-              $(nodeSerialized).text(value);
-              $(objectDOM).append(nodeSerialized);
-            }
+      /*
+       * Parses the ToggleFilter XML node into JSON
+       *
+       * @param {Element} xml - The XML Element that contains all the ToggleFilter elements
+       * @return {JSON} - The JSON object literal to be set on the model
+       */
+      parse: function (xml) {
+        var modelJSON = Filter.prototype.parse.call(this, xml);
+
+        //Parse the trueLabel and falseLabels
+        modelJSON.trueLabel = this.parseTextNode(xml, "trueLabel");
+        modelJSON.trueValue = this.parseTextNode(xml, "trueValue");
+        modelJSON.falseLabel = this.parseTextNode(xml, "falseLabel");
+        modelJSON.falseValue = this.parseTextNode(xml, "falseValue");
+
+        //Delete any attributes from the JSON that don't exist in the XML
+        if (!modelJSON.trueLabel) {
+          delete modelJSON.trueLabel;
+        }
+        if (!modelJSON.falseLabel) {
+          delete modelJSON.falseLabel;
+        }
+        if (!modelJSON.trueValue && modelJSON.trueValue !== false) {
+          delete modelJSON.trueValue;
+        }
+        if (!modelJSON.falseValue && modelJSON.falseValue !== false) {
+          delete modelJSON.falseValue;
+        }
 
-          }, this);
+        return modelJSON;
+      },
 
-          //Move the filterOptions node to the end of the filter node
-        /*  var filterOptionsNode = $(objectDOM).find("filterOptions");
+      /**
+       * Updates the XML DOM with the new values from the model
+       *  @inheritdoc
+       *  @return {XMLElement} An updated toggleFilter XML element from a portal document
+       */
+      updateDOM: function (options) {
+        try {
+          var objectDOM = Filter.prototype.updateDOM.call(this, options);
+
+          if (
+            typeof options == "undefined" ||
+            (typeof options == "object" && this.get("isUIFilterType"))
+          ) {
+            var toggleData = {
+              trueValue: this.get("trueValue"),
+              trueLabel: this.get("trueLabel"),
+              falseValue: this.get("falseValue"),
+              falseLabel: this.get("falseLabel"),
+            };
+
+            // Make and append new subnodes
+            _.map(
+              toggleData,
+              function (value, nodeName) {
+                // Remove the node if it exists in the DOM already
+                $(objectDOM).find(nodeName).remove();
+
+                // Don't serialize falsey or default values
+                if (
+                  (value || value === false) &&
+                  value != this.defaults()[nodeName]
+                ) {
+                  var nodeSerialized =
+                    objectDOM.ownerDocument.createElement(nodeName);
+                  $(nodeSerialized).text(value);
+                  $(objectDOM).append(nodeSerialized);
+                }
+              },
+              this,
+            );
+
+            //Move the filterOptions node to the end of the filter node
+            /*  var filterOptionsNode = $(objectDOM).find("filterOptions");
           filterOptionsNode.detach();
           $(objectDOM).append(filterOptionsNode);*/
+          }
+          //For collection definitions, serialize the filter differently
+          else {
+            //Remove the filterOptions
+            $(objectDOM).find("filterOptions").remove();
 
-        }
-        //For collection definitions, serialize the filter differently
-        else{
-          //Remove the filterOptions
-          $(objectDOM).find("filterOptions").remove();
+            //Change the root element into a <filter> element
+            var newFilterEl = objectDOM.ownerDocument.createElement("filter");
+            $(newFilterEl).html($(objectDOM).children());
 
-          //Change the root element into a <filter> element
-          var newFilterEl = objectDOM.ownerDocument.createElement("filter");
-          $(newFilterEl).html( $(objectDOM).children() );
+            //Return this node
+            return newFilterEl;
+          }
 
-          //Return this node
-          return newFilterEl;
+          return objectDOM;
+        } catch (e) {
+          //If there's an error, return the original DOM or an empty string
+          console.log(
+            "error updating the toggle filter object DOM, returning un-updated object DOM instead. Error message: " +
+              e,
+          );
+          return this.get("objectDOM") || "";
         }
-
-        return objectDOM;
-      }
-      //If there's an error, return the original DOM or an empty string
-      catch(e){
-        console.log("error updating the toggle filter object DOM, returning un-updated object DOM instead. Error message: " + e);
-        return this.get("objectDOM") || "";
-      }
       },
-    
+
       /**
-      * Checks if the values set on this model are valid and expected
-      * @return {object} - Returns a literal object with the invalid attributes and their
-      * corresponding error message
-      */
+       * Checks if the values set on this model are valid and expected
+       * @return {object} - Returns a literal object with the invalid attributes and their
+       * corresponding error message
+       */
       validate: function () {
         try {
-
           // Validate most of the ToggleFilter attributes using the parent validate
           // function
           var errors = Filter.prototype.validate.call(this);
@@ -196,32 +202,34 @@ 

Source: src/js/models/filters/ToggleFilter.js

// Delete error messages for the attributes that are going to be validated // specially for the ToggleFilter - ["trueLabel", "trueValue", "falseLabel", "falseValue"].forEach(function (attr) { - delete errors[attr] - }); + ["trueLabel", "trueValue", "falseLabel", "falseValue"].forEach( + function (attr) { + delete errors[attr]; + }, + ); // At least one trueValue required - var trueValue = this.get("trueValue") + var trueValue = this.get("trueValue"); if (!trueValue) { - errors.trueValue = "The filter requires a term to search for when the toggle is on" + errors.trueValue = + "The filter requires a term to search for when the toggle is on"; } // Return the errors, if there are any - if (Object.keys(errors).length) - return errors; + if (Object.keys(errors).length) return errors; else { return; } - } - catch (error) { + } catch (error) { console.log( - 'There was an error validating a ToggleFilter' + - '. Error details: ' + error + "There was an error validating a ToggleFilter" + + ". Error details: " + + error, ); } }, - - }); + }, + ); return ToggleFilter; }); diff --git a/docs/docs/src_js_models_formats_ObjectFormat.js.html b/docs/docs/src_js_models_formats_ObjectFormat.js.html index ec0eeb319..ca8cbf21e 100644 --- a/docs/docs/src_js_models_formats_ObjectFormat.js.html +++ b/docs/docs/src_js_models_formats_ObjectFormat.js.html @@ -44,55 +44,53 @@

Source: src/js/models/formats/ObjectFormat.js

-
/* global define */
-"use strict";
-
-define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
-
-    /**
-     * @class ObjectFormat
-     * @classdesc An ObjectFormat represents a V2 DataONE object format
-     * See https://purl.dataone.org/architecture/apis/Types2.html#v2_0.Types.ObjectFormat
-     * @classcategory Models/Formats
-     * @extends Backbone.Model
-     */
-    var ObjectFormat = Backbone.Model.extend(
-      /** @lends ObjectFormat.prototype */{
-
-        /* The default object format fields */
-        defaults: function() {
-            return {
-                formatId: null,
-                formatName: null,
-                formatType: null,
-                mediaType: null,
-                extension: null
-            };
-        },
-
-        /* Constructs a new instance */
-        initialize: function(attrs, options) {
-        },
-
-        /*
-         * The constructed URL of the model
-         * (/cn/v2/formats/{formatId})
-         */
-        url: function() {
-            if( ! this.get("formatId") ) return "";
-
-            return MetacatUI.appModel.get("formatsServiceUrl") +
-                (encodeURIComponent(this.get("formatId")));
-        },
-
-        /* No op - Formats are read only */
-        save: function() {
-
-            return false;
-        }
-    });
-
-    return ObjectFormat;
+            
"use strict";
+
+define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @class ObjectFormat
+   * @classdesc An ObjectFormat represents a V2 DataONE object format
+   * See https://purl.dataone.org/architecture/apis/Types2.html#v2_0.Types.ObjectFormat
+   * @classcategory Models/Formats
+   * @extends Backbone.Model
+   */
+  var ObjectFormat = Backbone.Model.extend(
+    /** @lends ObjectFormat.prototype */ {
+      /* The default object format fields */
+      defaults: function () {
+        return {
+          formatId: null,
+          formatName: null,
+          formatType: null,
+          mediaType: null,
+          extension: null,
+        };
+      },
+
+      /* Constructs a new instance */
+      initialize: function (attrs, options) {},
+
+      /*
+       * The constructed URL of the model
+       * (/cn/v2/formats/{formatId})
+       */
+      url: function () {
+        if (!this.get("formatId")) return "";
+
+        return (
+          MetacatUI.appModel.get("formatsServiceUrl") +
+          encodeURIComponent(this.get("formatId"))
+        );
+      },
+
+      /* No op - Formats are read only */
+      save: function () {
+        return false;
+      },
+    },
+  );
+
+  return ObjectFormat;
 });
 
diff --git a/docs/docs/src_js_models_geocoder_GeocodedLocation.js.html b/docs/docs/src_js_models_geocoder_GeocodedLocation.js.html index 23a95a5cc..d3ca8785b 100644 --- a/docs/docs/src_js_models_geocoder_GeocodedLocation.js.html +++ b/docs/docs/src_js_models_geocoder_GeocodedLocation.js.html @@ -44,53 +44,58 @@

Source: src/js/models/geocoder/GeocodedLocation.js

-
'use strict';
-
-define(
-  ['backbone', 'models/maps/GeoBoundingBox'],
-  (Backbone, GeoBoundingBox) => {
-    /**
-    * @class GeocodedLocation
-    * @classdes GeocodedLocation is the representation of a place that has been
-    * geocoded to provide latitude and longitude bounding coordinates for
-    * navigating to on a map.
-    * @classcategory Models/Geocoder
-    * @since 2.28.0
-    * @extends Backbone.Model
-    */
-    const GeocodedLocation = Backbone.Model.extend(
-      /** @lends GeocodedLocation.prototype */{
-        /**
-         * Overrides the default Backbone.Model.defaults() function to specify
-         * default attributes.
-         * @name GeocodedLocation#defaults
-         * @type {Object}
-         * @property {GeoBoundingBox} box Bounding box representing this location 
-         * on a map.
-         * @property {string} displayName A name that can be displayed to the user
-         * representing this location.
-         */
-        defaults() {
-          return {
-            box: new GeoBoundingBox,
-            displayName: '',
-          };
-        },
-
-        /**
-         * @typedef {Object} GeocodedLocationOptions
-         * @property {Object} box An object representing a boundary around a
-         * location on a map. 
-         * @property {string} displayName A display name for the location. 
-         */
-        initialize({ box: { north, south, east, west } = {}, displayName = '' } = {}) {
-          this.set('box', new GeoBoundingBox({ north, south, east, west }));
-          this.set('displayName', displayName);
-        },
-      });
-
-    return GeocodedLocation;
-  });
+            
"use strict";
+
+define(["backbone", "models/maps/GeoBoundingBox"], (
+  Backbone,
+  GeoBoundingBox,
+) => {
+  /**
+   * @class GeocodedLocation
+   * @classdes GeocodedLocation is the representation of a place that has been
+   * geocoded to provide latitude and longitude bounding coordinates for
+   * navigating to on a map.
+   * @classcategory Models/Geocoder
+   * @since 2.28.0
+   * @extends Backbone.Model
+   */
+  const GeocodedLocation = Backbone.Model.extend(
+    /** @lends GeocodedLocation.prototype */ {
+      /**
+       * Overrides the default Backbone.Model.defaults() function to specify
+       * default attributes.
+       * @name GeocodedLocation#defaults
+       * @type {Object}
+       * @property {GeoBoundingBox} box Bounding box representing this location
+       * on a map.
+       * @property {string} displayName A name that can be displayed to the user
+       * representing this location.
+       */
+      defaults() {
+        return {
+          box: new GeoBoundingBox(),
+          displayName: "",
+        };
+      },
+
+      /**
+       * @typedef {Object} GeocodedLocationOptions
+       * @property {Object} box An object representing a boundary around a
+       * location on a map.
+       * @property {string} displayName A display name for the location.
+       */
+      initialize({
+        box: { north, south, east, west } = {},
+        displayName = "",
+      } = {}) {
+        this.set("box", new GeoBoundingBox({ north, south, east, west }));
+        this.set("displayName", displayName);
+      },
+    },
+  );
+
+  return GeocodedLocation;
+});
 
diff --git a/docs/docs/src_js_models_geocoder_GeocoderSearch.js.html b/docs/docs/src_js_models_geocoder_GeocoderSearch.js.html index 57ba63c76..1731ed442 100644 --- a/docs/docs/src_js_models_geocoder_GeocoderSearch.js.html +++ b/docs/docs/src_js_models_geocoder_GeocoderSearch.js.html @@ -44,59 +44,57 @@

Source: src/js/models/geocoder/GeocoderSearch.js

-
'use strict';
-
-define(
-  [
-    'models/geocoder/GoogleMapsGeocoder',
-    'models/geocoder/GoogleMapsAutocompleter',
-  ],
-  (GoogleMapsGeocoder, GoogleMapsAutocompleter) => {
+            
"use strict";
+
+define([
+  "models/geocoder/GoogleMapsGeocoder",
+  "models/geocoder/GoogleMapsAutocompleter",
+], (GoogleMapsGeocoder, GoogleMapsAutocompleter) => {
+  /**
+   * GeocoderSearch interfaces with various geocoding and location
+   * searching services.
+   * @classcategory Models/Geocoder
+   * @since 2.28.0
+   */
+  class GeocoderSearch {
     /**
-     * GeocoderSearch interfaces with various geocoding and location
-     * searching services.
-     * @classcategory Models/Geocoder
-     * @since 2.28.0
+     * GoogleMapsAutocompleter model for interacting with Google Maps Places
+     * Autocomplete APIs.
      */
-    class GeocoderSearch {
-      /**
-       * GoogleMapsAutocompleter model for interacting with Google Maps Places
-       * Autocomplete APIs.
-       */
-      googleMapsAutocompleter = new GoogleMapsAutocompleter();
-
-      /**
-       * GoogleMapsGeocoder for interacting with Google Maps Geocoder APIs.
-       */
-      googleMapsGeocoder = new GoogleMapsGeocoder();
-
-      /**
-       * Convert a Google Maps Place ID into a list geocoded objects that can be
-       * displayed in the map widget.
-       * @param {string} newQuery - The user's input search query.
-       * @returns {Prediction[]} An array of places that could be the result the
-       * user is looking for. Most often this comes in five or less results.
-       */
-      async autocomplete(newQuery) {
-        return this.googleMapsAutocompleter.autocomplete(newQuery);
-      }
-
-      /**
-       * Convert a Google Maps Place ID into a list geocoded objects that can be
-       * displayed in the map widget.
-       * @param {Prediction} prediction An autocomplete prediction that includes
-       * a unique identifier for geocoding.
-       * @returns {GeocodedLocation[]} An array of locations with an associated
-       * bounding box. According to Google Maps API this should most often be a
-       * single value, but could potentially be many.
-       */
-      async geocode(prediction) {
-        return this.googleMapsGeocoder.geocode(prediction);
-      }
+    googleMapsAutocompleter = new GoogleMapsAutocompleter();
+
+    /**
+     * GoogleMapsGeocoder for interacting with Google Maps Geocoder APIs.
+     */
+    googleMapsGeocoder = new GoogleMapsGeocoder();
+
+    /**
+     * Convert a Google Maps Place ID into a list geocoded objects that can be
+     * displayed in the map widget.
+     * @param {string} newQuery - The user's input search query.
+     * @returns {Prediction[]} An array of places that could be the result the
+     * user is looking for. Most often this comes in five or less results.
+     */
+    async autocomplete(newQuery) {
+      return this.googleMapsAutocompleter.autocomplete(newQuery);
+    }
+
+    /**
+     * Convert a Google Maps Place ID into a list geocoded objects that can be
+     * displayed in the map widget.
+     * @param {Prediction} prediction An autocomplete prediction that includes
+     * a unique identifier for geocoding.
+     * @returns {GeocodedLocation[]} An array of locations with an associated
+     * bounding box. According to Google Maps API this should most often be a
+     * single value, but could potentially be many.
+     */
+    async geocode(prediction) {
+      return this.googleMapsGeocoder.geocode(prediction);
     }
+  }
 
-    return GeocoderSearch;
-  });
+  return GeocoderSearch;
+});
 
diff --git a/docs/docs/src_js_models_geocoder_GoogleMapsAutocompleter.js.html b/docs/docs/src_js_models_geocoder_GoogleMapsAutocompleter.js.html index fe0fc119c..db1a8c0d4 100644 --- a/docs/docs/src_js_models_geocoder_GoogleMapsAutocompleter.js.html +++ b/docs/docs/src_js_models_geocoder_GoogleMapsAutocompleter.js.html @@ -44,54 +44,60 @@

Source: src/js/models/geocoder/GoogleMapsAutocompleter.js
-
'use strict';
+            
"use strict";
+
+define(["backbone", "gmaps", "models/geocoder/Prediction"], (
+  Backbone,
+  gmaps,
+  Prediction,
+) => {
+  /**
+   * Integrate with the Google Maps Places Autocomplete API using the
+   * Google Maps AutocompleteService JS library.
+   * @classcategory Models/Geocoder
+   * @since 2.28.0
+   */
+  class GoogleMapsAutocompleter {
+    /**
+     * Google Maps service for interacting with the Places Autocomplete API.
+     */
+    autocompleter = new gmaps.places.AutocompleteService();
+
+    /**
+     * Use the Google Maps Places API to get place predictions based off of a
+     * user input string as the user types.
+     * @param {string} input - User input to search for Google Maps places.
+     * @returns {Prediction[]} An array of places that could be the result the
+     * user is looking for. Most often this comes in five or less results.
+     */
+    async autocomplete(input) {
+      if (!input) return [];
+      const response = await this.autocompleter.getPlacePredictions({
+        input,
+      });
+      return this.getPredictionsFromResults(response.predictions);
+    }
 
-define(
-  ['backbone', 'gmaps', 'models/geocoder/Prediction'],
-  (Backbone, gmaps, Prediction) => {
     /**
-     * Integrate with the Google Maps Places Autocomplete API using the
-     * Google Maps AutocompleteService JS library.
-     * @classcategory Models/Geocoder
-     * @since 2.28.0
+     * Helper function that converts a Google Maps Autocomplete API result
+     * into a useable Prediction model.
+     * @param {Object[]} List of Google Maps Autocomplete API results.
+     * @returns {Prediction[]} List of corresponding predictions.
      */
-    class GoogleMapsAutocompleter {
-      /**
-       * Google Maps service for interacting with the Places Autocomplete API.
-       */
-      autocompleter = new gmaps.places.AutocompleteService();
-
-      /**
-       * Use the Google Maps Places API to get place predictions based off of a
-       * user input string as the user types.
-       * @param {string} input - User input to search for Google Maps places.
-       * @returns {Prediction[]} An array of places that could be the result the
-       * user is looking for. Most often this comes in five or less results.
-       */
-      async autocomplete(input) {
-        if (!input) return [];
-        const response = await this.autocompleter.getPlacePredictions({
-          input,
-        });
-        return this.getPredictionsFromResults(response.predictions);
-      }
-
-      /**
-       * Helper function that converts a Google Maps Autocomplete API result
-       * into a useable Prediction model.
-       * @param {Object[]} List of Google Maps Autocomplete API results.
-       * @returns {Prediction[]} List of corresponding predictions.
-       */
-      getPredictionsFromResults(results) {
-        return results.map(result => new Prediction({
-          description: result.description,
-          googleMapsPlaceId: result.place_id,
-        }));
-      }
+    getPredictionsFromResults(results) {
+      return results.map(
+        (result) =>
+          new Prediction({
+            description: result.description,
+            googleMapsPlaceId: result.place_id,
+          }),
+      );
     }
+  }
 
-    return GoogleMapsAutocompleter;
-  });
+ return GoogleMapsAutocompleter; +}); +
diff --git a/docs/docs/src_js_models_geocoder_GoogleMapsGeocoder.js.html b/docs/docs/src_js_models_geocoder_GoogleMapsGeocoder.js.html index 7973aa8a5..f575d22b0 100644 --- a/docs/docs/src_js_models_geocoder_GoogleMapsGeocoder.js.html +++ b/docs/docs/src_js_models_geocoder_GoogleMapsGeocoder.js.html @@ -44,56 +44,58 @@

Source: src/js/models/geocoder/GoogleMapsGeocoder.js

-
'use strict';
+            
"use strict";
+
+define(["backbone", "gmaps", "models/geocoder/GeocodedLocation"], (
+  Backbone,
+  gmaps,
+  GeocodedLocation,
+) => {
+  /**
+   * Integrate with the Google Maps Geocoder API using the Google
+   * Maps Geocoder JS library.
+   * @classcategory Models/Geocoder
+   * @since 2.28.0
+   */
+  class GoogleMapsGeocoder {
+    /** Google Maps service for interacting  with the Geocoder API.  */
+    geocoder = new gmaps.Geocoder();
 
-define(
-  ['backbone', 'gmaps', 'models/geocoder/GeocodedLocation'],
-  (Backbone, gmaps, GeocodedLocation) => {
     /**
-     * Integrate with the Google Maps Geocoder API using the Google
-     * Maps Geocoder JS library.
-     * @classcategory Models/Geocoder
-     * @since 2.28.0
+     * Use the Google Maps Geocoder API to convert a Google Maps Place ID into
+     * a geocoded object that includes latitude and longitude information
+     * along with a bound box for viewing the location.
+     * @param {Prediction} prediction An autocomplete prediction that includes
+     * a unique identifier for geocoding.
+     * @returns {GeocodedLocation[]} An array of locations with an associated
+     * bounding box. According to Google Maps API this should most often be a
+     * single value, but could potentially be many.
      */
-    class GoogleMapsGeocoder {
-      /** Google Maps service for interacting  with the Geocoder API.  */
-      geocoder = new gmaps.Geocoder();
-
-      /**
-       * Use the Google Maps Geocoder API to convert a Google Maps Place ID into
-       * a geocoded object that includes latitude and longitude information 
-       * along with a bound box for viewing the location.
-       * @param {Prediction} prediction An autocomplete prediction that includes
-       * a unique identifier for geocoding.
-       * @returns {GeocodedLocation[]} An array of locations with an associated
-       * bounding box. According to Google Maps API this should most often be a
-       * single value, but could potentially be many.
-       */
-      async geocode(prediction) {
-        const response = await this.geocoder.geocode({
-          placeId: prediction.get('googleMapsPlaceId')
-        });
-        return this.getGeocodedLocationsFromResults(response.results);
-      }
-
-      /**
-       * Helper function that converts a Google Maps Places API result into a
-       * useable GeocodedLocation model.
-       * @param {Object[]} List of Google Maps Places API results.
-       * @returns {GeocodedLocation[]} List of corresponding geocoded locations.
-       */
-      getGeocodedLocationsFromResults(results) {
-        return results.map(result => {
-          return new GeocodedLocation({
-            box: result.geometry.viewport.toJSON(),
-            displayName: result.address_components[0].long_name,
-          });
+    async geocode(prediction) {
+      const response = await this.geocoder.geocode({
+        placeId: prediction.get("googleMapsPlaceId"),
+      });
+      return this.getGeocodedLocationsFromResults(response.results);
+    }
+
+    /**
+     * Helper function that converts a Google Maps Places API result into a
+     * useable GeocodedLocation model.
+     * @param {Object[]} List of Google Maps Places API results.
+     * @returns {GeocodedLocation[]} List of corresponding geocoded locations.
+     */
+    getGeocodedLocationsFromResults(results) {
+      return results.map((result) => {
+        return new GeocodedLocation({
+          box: result.geometry.viewport.toJSON(),
+          displayName: result.address_components[0].long_name,
         });
-      }
+      });
     }
+  }
 
-    return GoogleMapsGeocoder;
-  });
+  return GoogleMapsGeocoder;
+});
 
diff --git a/docs/docs/src_js_models_geocoder_Prediction.js.html b/docs/docs/src_js_models_geocoder_Prediction.js.html index 3ad9f9f1f..e1cf7cd0c 100644 --- a/docs/docs/src_js_models_geocoder_Prediction.js.html +++ b/docs/docs/src_js_models_geocoder_Prediction.js.html @@ -44,19 +44,19 @@

Source: src/js/models/geocoder/Prediction.js

-
'use strict';
+            
"use strict";
 
-define(['backbone'], (Backbone) => {
+define(["backbone"], (Backbone) => {
   /**
-  * @class Prediction
-  * @classdes Prediction represents a value returned from a location
-  * autocompletion search.
-  * @classcategory Models/Geocoder
-  * @since 2.28.0
-  * @extends Backbone.Model
-  */
+   * @class Prediction
+   * @classdes Prediction represents a value returned from a location
+   * autocompletion search.
+   * @classcategory Models/Geocoder
+   * @since 2.28.0
+   * @extends Backbone.Model
+   */
   const Prediction = Backbone.Model.extend(
-    /** @lends Prediction.prototype */{
+    /** @lends Prediction.prototype */ {
       /**
        * Overrides the default Backbone.Model.defaults() function to specify
        * default attributes for the Map
@@ -64,25 +64,26 @@ 

Source: src/js/models/geocoder/Prediction.js

* @type {Object} * @property {string} description A user-friendly description of a Google * Maps Place. - * @property {string} googleMapsPlaceId Unique identifier that can be + * @property {string} googleMapsPlaceId Unique identifier that can be * geocoded by the Google Maps Geocoder API. */ defaults() { - return { description: '', googleMapsPlaceId: '' }; + return { description: "", googleMapsPlaceId: "" }; }, /** * @typedef {Object} PredictionOptions * @property {string} description A string describing the location - * represented by the Prediction. + * represented by the Prediction. * @property {string} googleMapsPlaceId The place ID that is used to - * uniquely identify a place in Google Maps API. + * uniquely identify a place in Google Maps API. */ - initialize({ description, googleMapsPlaceId, } = {}) { - this.set('description', description); - this.set('googleMapsPlaceId', googleMapsPlaceId); + initialize({ description, googleMapsPlaceId } = {}) { + this.set("description", description); + this.set("googleMapsPlaceId", googleMapsPlaceId); }, - }); + }, + ); return Prediction; }); diff --git a/docs/docs/src_js_models_maps_AssetCategory.js.html b/docs/docs/src_js_models_maps_AssetCategory.js.html index 903ba77f1..1b83c5449 100644 --- a/docs/docs/src_js_models_maps_AssetCategory.js.html +++ b/docs/docs/src_js_models_maps_AssetCategory.js.html @@ -88,8 +88,8 @@

Source: src/js/models/maps/AssetCategory.js

*/ defaults() { return { - label: '', - icon: '', + label: "", + icon: "", expanded: false, }; }, @@ -116,7 +116,9 @@

Source: src/js/models/maps/AssetCategory.js

*/ initialize(categoryConfig) { if (!categoryConfig?.layers) { - throw new Error("Category " + categoryConfig.label + " has empty layers."); + throw new Error( + "Category " + categoryConfig.label + " has empty layers.", + ); } this.set("mapAssets", new MapAssets(categoryConfig.layers)); @@ -128,8 +130,9 @@

Source: src/js/models/maps/AssetCategory.js

if (IconUtilities.isSVG(categoryConfig.icon)) { this.updateIcon(categoryConfig.icon); } else { - IconUtilities.fetchIcon(categoryConfig.icon) - .then(icon => this.updateIcon(icon)); + IconUtilities.fetchIcon(categoryConfig.icon).then((icon) => + this.updateIcon(icon), + ); } } catch (error) { // Do nothing. Use the default icon instead. @@ -158,7 +161,7 @@

Source: src/js/models/maps/AssetCategory.js

setMapModel(mapModel) { this.get("mapAssets").setMapModel(mapModel); }, - } + }, ); return AssetCategory; diff --git a/docs/docs/src_js_models_maps_AssetColor.js.html b/docs/docs/src_js_models_maps_AssetColor.js.html index 0b29b3a85..4ffaf26ab 100644 --- a/docs/docs/src_js_models_maps_AssetColor.js.html +++ b/docs/docs/src_js_models_maps_AssetColor.js.html @@ -44,172 +44,164 @@

Source: src/js/models/maps/AssetColor.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone'
-  ],
-  function (
-    $,
-    _,
-    Backbone
-  ) {
-    /**
-     * @classdesc An AssetColor Model represents one color in a color scale that maps to
-     * attributes of a Map Asset. For vector assets (e.g. Cesium3DTileset models), the
-     * color is used to conditionally color vector data in a map (or plot).
-     * @classcategory Models/Maps
-     * @class AssetColor
-     * @name AssetColor
-     * @extends Backbone.Model
-     * @since 2.18.0
-     * @constructor
-    */
-    var AssetColor = Backbone.Model.extend(
-      /** @lends AssetColor.prototype */ {
-
-        /**
-         * The name of this type of model
-         * @type {string}
-        */
-        type: 'AssetColor',
-
-        /**
-         * A color to use in a map color palette, along with the value that the color
-         * represents.
-         * @typedef {Object} ColorConfig
-         * @name MapConfig#ColorConfig
-         * @property {string|number} value The value of the attribute in a MapAsset that
-         * corresponds to this color. If set to null, then this color will be the default
-         * color.
-         * @property {string} [label] A user-facing name for this attribute value,
-         * to show in map legends, etc. If not set, then the value will be displayed
-         * instead.
-         * @property {(string|AssetColor#Color)} color Either an object with 'red',
-         * 'green', 'blue' properties defining the intensity of each of the three colours
-         * with a value between 0 and 1, OR a string with a hex color code beginning with
-         * #, e.g. '#44A96A'. The {@link AssetColor} model will convert the string to an
-         * {@link AssetColor#Color} object.
-         * 
-         * @example
-         * {
-         *   value: 0,
-         *   label: 'water',
-         *   color: {
-         *     red: 0,
-         *     green: 0.1,
-         *     blue: 1 
-         *   }
-         * }
-         * 
-         * @example
-         * {
-         *   value: 'landmark',
-         *   color: '#7B44A9'
-         * }
-         */
-
-        /**
-         * An object that defines the properties of a color
-         * @typedef {Object} Color
-         * @name AssetColor#Color
-         * @property {number} [red=1] A number between 0 and 1 indicating the intensity of red
-         * in this color.
-         * @property {number} [blue=1] A number between 0 and 1 indicating the intensity of
-         * red in this color.
-         * @property {number} [green=1] A number between 0 and 1 indicating the intensity of
-         * red in this color.
-         * @property {number} [alpha=1] A number between 0 and 1 indicating the opacity of
-         * this color.
-         */
-
-        /**
-         * Default attributes for AssetColor models
-         * @name AssetColor#defaults
-         * @type {Object}
-         * @property {string|number} value The value of the attribute that corresponds to
-         * this color. If set to null, then this color will be the default color.
-         * @property {string} [label] A user-facing name for this attribute value,
-         * to show in map legends, etc. If not set, then the value will be displayed
-         * instead.
-         * @property {AssetColor#Color} color The red, green, and blue intensities that define the
-         * color
-        */
-        defaults: function () {
-          return {
-            value: null,
-            label: null,
-            color: {
-              red: 1,
-              blue: 1,
-              green: 1,
-              alpha: 1
-            }
+            
"use strict";
+
+define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @classdesc An AssetColor Model represents one color in a color scale that maps to
+   * attributes of a Map Asset. For vector assets (e.g. Cesium3DTileset models), the
+   * color is used to conditionally color vector data in a map (or plot).
+   * @classcategory Models/Maps
+   * @class AssetColor
+   * @name AssetColor
+   * @extends Backbone.Model
+   * @since 2.18.0
+   * @constructor
+   */
+  var AssetColor = Backbone.Model.extend(
+    /** @lends AssetColor.prototype */ {
+      /**
+       * The name of this type of model
+       * @type {string}
+       */
+      type: "AssetColor",
+
+      /**
+       * A color to use in a map color palette, along with the value that the color
+       * represents.
+       * @typedef {Object} ColorConfig
+       * @name MapConfig#ColorConfig
+       * @property {string|number} value The value of the attribute in a MapAsset that
+       * corresponds to this color. If set to null, then this color will be the default
+       * color.
+       * @property {string} [label] A user-facing name for this attribute value,
+       * to show in map legends, etc. If not set, then the value will be displayed
+       * instead.
+       * @property {(string|AssetColor#Color)} color Either an object with 'red',
+       * 'green', 'blue' properties defining the intensity of each of the three colours
+       * with a value between 0 and 1, OR a string with a hex color code beginning with
+       * #, e.g. '#44A96A'. The {@link AssetColor} model will convert the string to an
+       * {@link AssetColor#Color} object.
+       *
+       * @example
+       * {
+       *   value: 0,
+       *   label: 'water',
+       *   color: {
+       *     red: 0,
+       *     green: 0.1,
+       *     blue: 1
+       *   }
+       * }
+       *
+       * @example
+       * {
+       *   value: 'landmark',
+       *   color: '#7B44A9'
+       * }
+       */
+
+      /**
+       * An object that defines the properties of a color
+       * @typedef {Object} Color
+       * @name AssetColor#Color
+       * @property {number} [red=1] A number between 0 and 1 indicating the intensity of red
+       * in this color.
+       * @property {number} [blue=1] A number between 0 and 1 indicating the intensity of
+       * red in this color.
+       * @property {number} [green=1] A number between 0 and 1 indicating the intensity of
+       * red in this color.
+       * @property {number} [alpha=1] A number between 0 and 1 indicating the opacity of
+       * this color.
+       */
+
+      /**
+       * Default attributes for AssetColor models
+       * @name AssetColor#defaults
+       * @type {Object}
+       * @property {string|number} value The value of the attribute that corresponds to
+       * this color. If set to null, then this color will be the default color.
+       * @property {string} [label] A user-facing name for this attribute value,
+       * to show in map legends, etc. If not set, then the value will be displayed
+       * instead.
+       * @property {AssetColor#Color} color The red, green, and blue intensities that define the
+       * color
+       */
+      defaults: function () {
+        return {
+          value: null,
+          label: null,
+          color: {
+            red: 1,
+            blue: 1,
+            green: 1,
+            alpha: 1,
+          },
+        };
+      },
+
+      /**
+       * Executed when a new AssetColor model is created.
+       * @param {MapConfig#ColorConfig} [colorConfig] The initial values of the
+       * attributes, which will be set on the model.
+       */
+      initialize: function (colorConfig) {
+        try {
+          // If the color is a hex code instead of an object with RGB values, then
+          // convert it.
+          if (
+            colorConfig &&
+            colorConfig.color &&
+            typeof colorConfig.color === "string"
+          ) {
+            // Assume the string is an hex color code and convert it to RGBA,
+            // otherwise use the default color
+            this.set(
+              "color",
+              this.hexToRGBA(colorConfig.color) || this.defaults().color,
+            );
           }
-        },
-
-        /**
-         * Executed when a new AssetColor model is created.
-         * @param {MapConfig#ColorConfig} [colorConfig] The initial values of the
-         * attributes, which will be set on the model.
-         */
-        initialize: function (colorConfig) {
-          try {
-            // If the color is a hex code instead of an object with RGB values, then
-            // convert it.
-            if (colorConfig && colorConfig.color && typeof colorConfig.color === 'string') {
-              // Assume the string is an hex color code and convert it to RGBA,
-              // otherwise use the default color
-              this.set('color',
-                this.hexToRGBA(colorConfig.color) ||
-                this.defaults().color
-              )
-            }
-            // Set missing RGB values to 0, and alpha to 1
-            let color = this.get('color');
-            color.red = color.red || 0;
-            color.green = color.green || 0;
-            color.blue = color.blue || 0;
-            if (!color.alpha && color.alpha !== 0) {
-              color.alpha = 1;
+          // Set missing RGB values to 0, and alpha to 1
+          let color = this.get("color");
+          color.red = color.red || 0;
+          color.green = color.green || 0;
+          color.blue = color.blue || 0;
+          if (!color.alpha && color.alpha !== 0) {
+            color.alpha = 1;
+          }
+          this.set("color", color);
+        } catch (error) {
+          console.log(
+            "There was an error initializing a AssetColor model" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Converts an 6 to 8 digit hex color value to RGBA values between 0 and 1
+       * @param {string} hex - A hex color code, e.g. '#44A96A' or '#44A96A88'
+       * @return {Color} - The RGBA values of the color
+       * @since 2.25.0
+       */
+      hexToRGBA: function (hex) {
+        var result =
+          /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex);
+        return result
+          ? {
+              red: parseInt(result[1], 16) / 255,
+              green: parseInt(result[2], 16) / 255,
+              blue: parseInt(result[3], 16) / 255,
+              alpha: parseInt(result[4], 16) / 255,
             }
-            this.set('color', color)
+          : null;
+      },
+    },
+  );
 
-          }
-          catch (error) {
-            console.log(
-              'There was an error initializing a AssetColor model' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-
-        /**
-         * Converts an 6 to 8 digit hex color value to RGBA values between 0 and 1
-         * @param {string} hex - A hex color code, e.g. '#44A96A' or '#44A96A88'
-         * @return {Color} - The RGBA values of the color
-         * @since 2.25.0
-         */
-        hexToRGBA: function (hex) {
-          var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex);
-          return result ? {
-            red: parseInt(result[1], 16) / 255,
-            green: parseInt(result[2], 16) / 255,
-            blue: parseInt(result[3], 16) / 255,
-            alpha: parseInt(result[4], 16) / 255
-          } : null;
-        },
-
-      });
-
-    return AssetColor;
-
-  }
-);
+  return AssetColor;
+});
 
diff --git a/docs/docs/src_js_models_maps_AssetColorPalette.js.html b/docs/docs/src_js_models_maps_AssetColorPalette.js.html index 7301e6d5c..dc173d207 100644 --- a/docs/docs/src_js_models_maps_AssetColorPalette.js.html +++ b/docs/docs/src_js_models_maps_AssetColorPalette.js.html @@ -186,7 +186,7 @@

Source: src/js/models/maps/AssetColorPalette.js

console.log( "There was an error initializing a AssetColorPalette model" + ". Error details: " + - error + error, ); } }, @@ -330,8 +330,7 @@

Source: src/js/models/maps/AssetColorPalette.js

getDefaultColor: function () { return this.get("colors").getDefaultColor().get("color"); }, - - } + }, ); return AssetColorPalette; diff --git a/docs/docs/src_js_models_maps_Feature.js.html b/docs/docs/src_js_models_maps_Feature.js.html index a2c744b05..77ec485dc 100644 --- a/docs/docs/src_js_models_maps_Feature.js.html +++ b/docs/docs/src_js_models_maps_Feature.js.html @@ -108,7 +108,7 @@

Source: src/js/models/maps/Feature.js

} catch (error) { console.log( "Failed to check if a Feature model is the default.", - error + error, ); } }, @@ -129,7 +129,7 @@

Source: src/js/models/maps/Feature.js

console.log( "There was an error reset a Feature model to default" + ". Error details: " + - error + error, ); } }, @@ -185,7 +185,7 @@

Source: src/js/models/maps/Feature.js

if (input.featureObject && options.assets) { const attrs = this.attrsFromFeatureObject( input.featureObject, - options.assets + options.assets, ); input = Object.assign({}, input, attrs); } @@ -195,7 +195,7 @@

Source: src/js/models/maps/Feature.js

console.log("Failed to parse a Feature model", error); } }, - } + }, ); return Feature; diff --git a/docs/docs/src_js_models_maps_GeoBoundingBox.js.html b/docs/docs/src_js_models_maps_GeoBoundingBox.js.html index ac9813263..552b525b2 100644 --- a/docs/docs/src_js_models_maps_GeoBoundingBox.js.html +++ b/docs/docs/src_js_models_maps_GeoBoundingBox.js.html @@ -221,7 +221,7 @@

Source: src/js/models/maps/GeoBoundingBox.js

); } }, - } + }, ); return GeoBoundingBox; diff --git a/docs/docs/src_js_models_maps_GeoPoint.js.html b/docs/docs/src_js_models_maps_GeoPoint.js.html index df533c322..f65c54c66 100644 --- a/docs/docs/src_js_models_maps_GeoPoint.js.html +++ b/docs/docs/src_js_models_maps_GeoPoint.js.html @@ -48,7 +48,7 @@

Source: src/js/models/maps/GeoPoint.js

define(["backbone", "models/maps/GeoUtilities"], function ( Backbone, - GeoUtilities + GeoUtilities, ) { // Regular expression matching a string that contains two numbers optionally separated by a comma. const FLOATS_REGEX = /[+-]?[0-9]*[.]?[0-9]+/g; @@ -95,24 +95,28 @@

Source: src/js/models/maps/GeoPoint.js

}, /** - * Parse a string according to a regular expression. + * Parse a string according to a regular expression. * @param {string} value A user-entered value for parsing into a latiude * and longitude pair. * @throws An error indicating that more than two numbers have been * entered. - * @returns {Object} Latitude and longitude information for creating a + * @returns {Object} Latitude and longitude information for creating a * GeoPoint. */ parse(value) { - if (typeof value !== 'string') { + if (typeof value !== "string") { return {}; } const matches = value?.match(FLOATS_REGEX); - if (matches?.length !== 2 || isNaN(matches[0]) || isNaN(matches[1]) - || !GeoPoint.couldBeLatLong(value)) { + if ( + matches?.length !== 2 || + isNaN(matches[0]) || + isNaN(matches[1]) || + !GeoPoint.couldBeLatLong(value) + ) { throw new Error( - 'Try entering a search query with two numerical values representing a latitude and longitude (e.g. 64.84, -147.72).' + "Try entering a search query with two numerical values representing a latitude and longitude (e.g. 64.84, -147.72).", ); } @@ -200,7 +204,7 @@

Source: src/js/models/maps/GeoPoint.js

if (attrs.longitude < -180 || attrs.longitude > 180) { return { - longitude: "Invalid longitude. Must be between -180 and 180." + longitude: "Invalid longitude. Must be between -180 and 180.", }; } @@ -221,13 +225,13 @@

Source: src/js/models/maps/GeoPoint.js

* be in a lat,long pair. */ couldBeLatLong(value) { - if (typeof value !== 'string') { + if (typeof value !== "string") { return false; } return value?.match(NON_LAT_LONG_CHARS_REGEX) == null; }, - } + }, ); return GeoPoint; diff --git a/docs/docs/src_js_models_maps_GeoScale.js.html b/docs/docs/src_js_models_maps_GeoScale.js.html index 25f65b11a..e8c0995ee 100644 --- a/docs/docs/src_js_models_maps_GeoScale.js.html +++ b/docs/docs/src_js_models_maps_GeoScale.js.html @@ -101,7 +101,7 @@

Source: src/js/models/maps/GeoScale.js

return "Invalid meters scale. Must be greater than 0."; } }, - } + }, ); return GeoScale; diff --git a/docs/docs/src_js_models_maps_GeoUtilities.js.html b/docs/docs/src_js_models_maps_GeoUtilities.js.html index d4830e12b..18596a1ec 100644 --- a/docs/docs/src_js_models_maps_GeoUtilities.js.html +++ b/docs/docs/src_js_models_maps_GeoUtilities.js.html @@ -94,7 +94,7 @@

Source: src/js/models/maps/GeoUtilities.js

return [x, y, z]; }, - } + }, ); return GeoUtilities; diff --git a/docs/docs/src_js_models_maps_Geohash.js.html b/docs/docs/src_js_models_maps_Geohash.js.html index 87a0127c6..a6346bea0 100644 --- a/docs/docs/src_js_models_maps_Geohash.js.html +++ b/docs/docs/src_js_models_maps_Geohash.js.html @@ -125,7 +125,7 @@

Source: src/js/models/maps/Geohash.js

// Must use prototype.get to avoid infinite loop const properties = Backbone.Model.prototype.get.call( this, - "properties" + "properties", ); return properties?.hasOwnProperty(key); }, @@ -206,7 +206,7 @@

Source: src/js/models/maps/Geohash.js

new Geohash({ hashString: hashString + i.toString(32), properties: keepProperties ? this.get("properties") : {}, - }) + }), ); } return geohashes; @@ -346,7 +346,7 @@

Source: src/js/models/maps/Geohash.js

const id = properties["hashString"]; const ecefCoordinates = rectangle.map((coord) => - this.geodeticToECEF(coord) + this.geodeticToECEF(coord), ); const ecefPosition = this.geodeticToECEF([ point.longitude, @@ -395,7 +395,7 @@

Source: src/js/models/maps/Geohash.js

geodeticToECEF: function (coord) { return GeoUtilities.prototype.geodeticToECEF(coord); }, - } + }, ); return Geohash; diff --git a/docs/docs/src_js_models_maps_Map.js.html b/docs/docs/src_js_models_maps_Map.js.html index 4401c493a..37ef72eec 100644 --- a/docs/docs/src_js_models_maps_Map.js.html +++ b/docs/docs/src_js_models_maps_Map.js.html @@ -55,7 +55,8 @@

Source: src/js/models/maps/Map.js

"collections/maps/AssetCategories", "models/maps/GeoPoint", "collections/maps/viewfinder/ZoomPresets", -], function ($, +], function ( + $, _, Backbone, MapAssets, @@ -111,8 +112,8 @@

Source: src/js/models/maps/Map.js

* home button in the toolbar. * @property {Boolean} [showViewfinder=false] - Whether or not to show the * viewfinder UI and viewfinder button in the toolbar. The ViewfinderView - * requires a Google Maps API key present in the AppModel. In order to - * work properly the Geocoding API and Places API must be enabled. + * requires a Google Maps API key present in the AppModel. In order to + * work properly the Geocoding API and Places API must be enabled. * @property {Boolean} [toolbarOpen=false] - Whether or not the toolbar is * open when the map is initialized. Set to false by default, so that the * toolbar is hidden by default. @@ -139,7 +140,7 @@

Source: src/js/models/maps/Map.js

* @property {ZoomPresets} [zoomPresets=null] - A Backbone.Collection of a * predefined list of locations with an enabled list of layer IDs to be * shown the zoom presets UI. Requires `showViewfinder` to be true as this - * UI appears within the ViewfinderView. + * UI appears within the ViewfinderView. * * @example * { @@ -262,7 +263,7 @@

Source: src/js/models/maps/Map.js

* @property {ZoomPresets} [zoomPresets=null] - A Backbone.Collection of a * predefined list of locations with an enabled list of layer IDs to be * shown the zoom presets UI. Requires `showViewfinder` to be true as this - * UI appears within the ViewfinderView. + * UI appears within the ViewfinderView. */ defaults: function () { return { @@ -311,7 +312,9 @@

Source: src/js/models/maps/Map.js

} if (isNonEmptyArray(config.layerCategories)) { - const assetCategories = new AssetCategories(config.layerCategories); + const assetCategories = new AssetCategories( + config.layerCategories, + ); assetCategories.setMapModel(this); this.set("layerCategories", assetCategories); this.unset("layers"); @@ -324,14 +327,20 @@

Source: src/js/models/maps/Map.js

this.set("terrains", new MapAssets(config.terrains)); } - this.set('zoomPresetsCollection', new ZoomPresets({ - zoomPresetObjects: config.zoomPresets, - allLayers: this.getAllLayers(), - }, { parse: true })); + this.set( + "zoomPresetsCollection", + new ZoomPresets( + { + zoomPresetObjects: config.zoomPresets, + allLayers: this.getAllLayers(), + }, + { parse: true }, + ), + ); } this.setUpInteractions(); } catch (error) { - console.log('Failed to initialize a Map model.', error); + console.log("Failed to initialize a Map model.", error); } }, @@ -393,7 +402,7 @@

Source: src/js/models/maps/Map.js

*/ resetLayerVisibility: function () { for (const layer of this.getAllLayers()) { - layer.set("visible", layer.get('originalVisibility')); + layer.set("visible", layer.get("originalVisibility")); } }, @@ -435,7 +444,7 @@

Source: src/js/models/maps/Map.js

if (this.has("layerCategories")) { return this.get("layerCategories") .getMapAssets() - .map(assets => assets.models) + .map((assets) => assets.models) .flat(); } else if (this.has("layers")) { return this.get("layers").models; @@ -471,7 +480,7 @@

Source: src/js/models/maps/Map.js

// is a bug in the MapAssets collection or Backbone? if (layers) layers.remove(asset.cid); }, - } + }, ); return MapModel; diff --git a/docs/docs/src_js_models_maps_MapInteraction.js.html b/docs/docs/src_js_models_maps_MapInteraction.js.html index a222e1d91..b3dd43c6f 100644 --- a/docs/docs/src_js_models_maps_MapInteraction.js.html +++ b/docs/docs/src_js_models_maps_MapInteraction.js.html @@ -54,7 +54,15 @@

Source: src/js/models/maps/MapInteraction.js

"models/maps/GeoPoint", "models/maps/GeoScale", "collections/maps/MapAssets", -], function (Backbone, Features, Feature, GeoBoundingBox, GeoPoint, GeoScale, MapAssets) { +], function ( + Backbone, + Features, + Feature, + GeoBoundingBox, + GeoPoint, + GeoScale, + MapAssets, +) { /** * @class MapInteraction * @classdesc The Map Interaction stores information about user interaction @@ -85,7 +93,7 @@

Source: src/js/models/maps/MapInteraction.js

* user last clicked. * @property {GeoScale} scale - The current scale of the map in * pixels:meters, i.e. The number of pixels on the screen that equal the - * number of meters on the map/globe. Updated by the map widget. + * number of meters on the map/globe. Updated by the map widget. * @property {GeoBoundingBox} viewExtent - The extent of the currently * visible area in the map widget. Updated by the map widget. * @property {Features} hoveredFeatures - The feature that the mouse is @@ -167,7 +175,7 @@

Source: src/js/models/maps/MapInteraction.js

listener.stopListening(); listener.destroy(); } - } + }, ); }, @@ -192,11 +200,11 @@

Source: src/js/models/maps/MapInteraction.js

listener.listenToOnce(model, "cameraChanged", function () { listener.stopListening(model, "moveEnd"); model.trigger("moveStartAndChanged"); - }) + }); listener.listenToOnce(model, "moveEnd", function () { listener.stopListening(model, "cameraChanged"); - }) - }) + }); + }); }, /** @@ -247,12 +255,12 @@

Source: src/js/models/maps/MapInteraction.js

* properties. * @returns {GeoPoint} The corresponding position as a GeoPoint model. */ - setPosition: function(attributeName, position) { + setPosition: function (attributeName, position) { let point = this.get(attributeName); if (!point) { point = new GeoPoint(); this.set(attributeName, point); - } + } point.set(position); return point; }, @@ -263,17 +271,17 @@

Source: src/js/models/maps/MapInteraction.js

* properties. * @returns {GeoPoint} The clicked position as a GeoPoint model. */ - setClickedPosition: function(position) { + setClickedPosition: function (position) { return this.setPosition("clickedPosition", position); }, /** - * Sets the position of the mouse on the map. - * @param {Object} position - An object with 'longitude' and 'latitude' - * properties. - * @returns {GeoPoint} The mouse position as a GeoPoint model. - */ - setMousePosition: function(position) { + * Sets the position of the mouse on the map. + * @param {Object} position - An object with 'longitude' and 'latitude' + * properties. + * @returns {GeoPoint} The mouse position as a GeoPoint model. + */ + setMousePosition: function (position) { return this.setPosition("mousePosition", position); }, @@ -383,10 +391,14 @@

Source: src/js/models/maps/MapInteraction.js

return; } - const assets = _.reduce(this.get("mapModel")?.getLayerGroups(), (mapAssets, layers) => { - mapAssets.add(layers.models); - return mapAssets; - }, new MapAssets()); + const assets = _.reduce( + this.get("mapModel")?.getLayerGroups(), + (mapAssets, layers) => { + mapAssets.add(layers.models); + return mapAssets; + }, + new MapAssets(), + ); const newAttrs = features.map((f) => ({ featureObject: f })); @@ -397,7 +409,7 @@

Source: src/js/models/maps/MapInteraction.js

console.log("Failed to select a Feature in a Map model.", e); } }, - } + }, ); return MapInteraction; diff --git a/docs/docs/src_js_models_maps_VectorFilter.js.html b/docs/docs/src_js_models_maps_VectorFilter.js.html index c936ea98d..c4cc9feaa 100644 --- a/docs/docs/src_js_models_maps_VectorFilter.js.html +++ b/docs/docs/src_js_models_maps_VectorFilter.js.html @@ -44,241 +44,228 @@

Source: src/js/models/maps/VectorFilter.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone'
-  ],
-  function (
-    $,
-    _,
-    Backbone
-  ) {
-    /**
-     * @classdesc A VectorFilter Model represents one condition used to show or hide
-     * specific features of a vector layer on a map. The filter defines rules used to show
-     * features conditionally based on properties of the feature. For example, it could
-     * specify hiding all vectors for an asset that have an area greater than 10 km2. 
-     * @classcategory Models/Maps
-     * @class VectorFilter
-     * @name VectorFilter
-     * @extends Backbone.Model
-     * @since 2.18.0
-     * @constructor
-    */
-    var VectorFilter = Backbone.Model.extend(
-      /** @lends VectorFilter.prototype */ {
-
-        /**
-         * The name of this type of model
-         * @type {string}
-        */
-        type: 'VectorFilter',
-
-        /**
-         * A VectorFilterConfig specifies conditions under which specific features of a
-         * vector layer on a map should be visible. The filter defines rules used to show
-         * features conditionally based on properties of the feature. For example, it
-         * could specify hiding all vectors for an asset that have an area greater than
-         * 10km2. This configuration is passed to the {@link VectorFilter} model.
-         * @typedef {Object} VectorFilterConfig
-         * @name MapConfig#VectorFilterConfig
-         * @property {('categorical'|'numeric')} filterType If categorical, then a feature
-         * will be visible when its property value exactly matches one of those listed in
-         * the values attribute. If numeric, then a feature will be visible when its
-         * property value is between the min and max.
-         * @property {string} property The property (attribute) of the {@link MapAsset}
-         * feature to filter on.
-         * @property {(string[]|number[])} values Only used for categorical filters. If
-         * the property matches one of the values listed, the feature will be displayed.
-         * If the filter type is categorical and no values are set, then features will not
-         * be filtered on this property.
-         * @property {number} max Only used for numeric filters. The property's value must
-         * be less than the value set here for the feature to be visible. If the filter
-         * type is numeric, and max is set, then the max is infinite.
-         * @property {number} min Only used for numeric filters. The property's value must
-         * be greater than the value set here for the feature to be visible. If the filter
-         * type is numeric, and min is set, then the min is minus infinity.
-         *
-         * @example
-         * // Only show vectors with an 'area' property set to less than 10
-         * {
-         *   filterType: 'numeric'
-         *   property: 'area'
-         *   max: 10
-         * }
-         *
-         * @example
-         * // Show only features that have the 'boreal' or 'tropical' property set on their 'forestType' attribute
-         * {
-         *   filterType: 'categorical'
-         *   property: 'forestType'
-         *   values: ['boreal', 'tropical']
-         * }
-         */
-
-        /**
-         * Default attributes for VectorFilter models
-         * @name VectorFilter#defaults
-         * @type {Object}
-         * @property {('categorical'|'numeric')} [filterType='categorical'] If
-         * categorical, then a feature will be visible when its property value exactly
-         * matches one of those listed in the values attribute. If numerical, then a
-         * feature will be visible when its property value is between the min and max.
-         * @property {string} property The property (attribute) of the feature to filter
-         * on.
-         * @property {(string[]|number[])} values Only used for categorical filters. If
-         * the property matches one of the values listed, the feature will be displayed.
-         * If the filter type is categorical and no values are set, then features will not
-         * be filtered on this property.
-         * @property {number} max Only used for numeric filters. The property's value must
-         * be less than the value set here for the feature to be visible. If the filter
-         * type is numeric, and max is set, then the max is infinite.
-         * @property {number} min Only used for numeric filters. The property's value must
-         * be greater than the value set here for the feature to be visible. If the filter
-         * type is numeric, and min is set, then the min is minus infinity.
-         *
-        */
-        defaults: function () {
-          return {
-            filterType: 'categorical',
-            property: null,
-            values: [],
-            max: null,
-            min: null
+            
"use strict";
+
+define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @classdesc A VectorFilter Model represents one condition used to show or hide
+   * specific features of a vector layer on a map. The filter defines rules used to show
+   * features conditionally based on properties of the feature. For example, it could
+   * specify hiding all vectors for an asset that have an area greater than 10 km2.
+   * @classcategory Models/Maps
+   * @class VectorFilter
+   * @name VectorFilter
+   * @extends Backbone.Model
+   * @since 2.18.0
+   * @constructor
+   */
+  var VectorFilter = Backbone.Model.extend(
+    /** @lends VectorFilter.prototype */ {
+      /**
+       * The name of this type of model
+       * @type {string}
+       */
+      type: "VectorFilter",
+
+      /**
+       * A VectorFilterConfig specifies conditions under which specific features of a
+       * vector layer on a map should be visible. The filter defines rules used to show
+       * features conditionally based on properties of the feature. For example, it
+       * could specify hiding all vectors for an asset that have an area greater than
+       * 10km2. This configuration is passed to the {@link VectorFilter} model.
+       * @typedef {Object} VectorFilterConfig
+       * @name MapConfig#VectorFilterConfig
+       * @property {('categorical'|'numeric')} filterType If categorical, then a feature
+       * will be visible when its property value exactly matches one of those listed in
+       * the values attribute. If numeric, then a feature will be visible when its
+       * property value is between the min and max.
+       * @property {string} property The property (attribute) of the {@link MapAsset}
+       * feature to filter on.
+       * @property {(string[]|number[])} values Only used for categorical filters. If
+       * the property matches one of the values listed, the feature will be displayed.
+       * If the filter type is categorical and no values are set, then features will not
+       * be filtered on this property.
+       * @property {number} max Only used for numeric filters. The property's value must
+       * be less than the value set here for the feature to be visible. If the filter
+       * type is numeric, and max is set, then the max is infinite.
+       * @property {number} min Only used for numeric filters. The property's value must
+       * be greater than the value set here for the feature to be visible. If the filter
+       * type is numeric, and min is set, then the min is minus infinity.
+       *
+       * @example
+       * // Only show vectors with an 'area' property set to less than 10
+       * {
+       *   filterType: 'numeric'
+       *   property: 'area'
+       *   max: 10
+       * }
+       *
+       * @example
+       * // Show only features that have the 'boreal' or 'tropical' property set on their 'forestType' attribute
+       * {
+       *   filterType: 'categorical'
+       *   property: 'forestType'
+       *   values: ['boreal', 'tropical']
+       * }
+       */
+
+      /**
+       * Default attributes for VectorFilter models
+       * @name VectorFilter#defaults
+       * @type {Object}
+       * @property {('categorical'|'numeric')} [filterType='categorical'] If
+       * categorical, then a feature will be visible when its property value exactly
+       * matches one of those listed in the values attribute. If numerical, then a
+       * feature will be visible when its property value is between the min and max.
+       * @property {string} property The property (attribute) of the feature to filter
+       * on.
+       * @property {(string[]|number[])} values Only used for categorical filters. If
+       * the property matches one of the values listed, the feature will be displayed.
+       * If the filter type is categorical and no values are set, then features will not
+       * be filtered on this property.
+       * @property {number} max Only used for numeric filters. The property's value must
+       * be less than the value set here for the feature to be visible. If the filter
+       * type is numeric, and max is set, then the max is infinite.
+       * @property {number} min Only used for numeric filters. The property's value must
+       * be greater than the value set here for the feature to be visible. If the filter
+       * type is numeric, and min is set, then the min is minus infinity.
+       *
+       */
+      defaults: function () {
+        return {
+          filterType: "categorical",
+          property: null,
+          values: [],
+          max: null,
+          min: null,
+        };
+      },
+
+      /**
+       * This function checks if a feature is visible based on the filter's rules.
+       * @param {Object} properties The properties of the feature to be filtered. (See
+       * the 'properties' attribute of {@link Feature#defaults}.)
+       * @returns {boolean} Returns true if the feature properties pass this filter
+       */
+      featureIsVisible: function (properties) {
+        try {
+          if (!properties) {
+            properties = {};
           }
-        },
-
-        /**
-         * This function checks if a feature is visible based on the filter's rules.
-         * @param {Object} properties The properties of the feature to be filtered. (See
-         * the 'properties' attribute of {@link Feature#defaults}.)
-         * @returns {boolean} Returns true if the feature properties pass this filter
-         */
-        featureIsVisible: function (properties) {
-          try {
-            if (!properties) {
-              properties = {};
+          var visible = true;
+          if (this.get("filterType") === "categorical") {
+            var values = this.get("values");
+            if (values.length > 0) {
+              visible = _.contains(values, properties[this.get("property")]);
             }
-            var visible = true;
-            if (this.get('filterType') === 'categorical') {
-              var values = this.get('values');
-              if (values.length > 0) {
-                visible = _.contains(values, properties[this.get('property')]);
-              }
-            } else if (this.get('filterType') === 'numeric') {
-              var max = this.get('max');
-              var min = this.get('min');
-              if (max !== null) {
-                visible = properties[this.get('property')] < max;
-              }
-              if (min !== null) {
-                visible = properties[this.get('property')] > min && visible;
-              }
+          } else if (this.get("filterType") === "numeric") {
+            var max = this.get("max");
+            var min = this.get("min");
+            if (max !== null) {
+              visible = properties[this.get("property")] < max;
+            }
+            if (min !== null) {
+              visible = properties[this.get("property")] > min && visible;
             }
-            return visible;
-          }
-          catch (error) {
-            console.log(
-              'There was an error checking feature visibility in a VectorFilter' +
-              '. Error details: ' + error
-            );
           }
-        },
-
-        // /**
-        //  * Executed when a new VectorFilter model is created.
-        //  * @param {Object} [attributes] The initial values of the attributes, which will
-        //  * be set on the model.
-        //  * @param {Object} [options] Options for the initialize function.
-        //  */
-        // initialize: function (attributes, options) {
-        //   try {
-
-        //   }
-        //   catch (error) {
-        //     console.log(
-        //       'There was an error initializing a VectorFilter model' +
-        //       '. Error details: ' + error
-        //     );
-        //   }
-        // },
-
-        // /**
-        //  * Parses the given input into a JSON object to be set on the model.
-        //  *
-        //  * @param {TODO} input - The raw response object
-        //  * @return {TODO} - The JSON object of all the VectorFilter attributes
-        //  */
-        // parse: function (input) {
-
-        //   try {
-
-        //     var modelJSON = {};
-
-        //     return modelJSON
-
-        //   }
-        //   catch (error) {
-        //     console.log(
-        //       'There was an error parsing a VectorFilter model' +
-        //       '. Error details: ' + error
-        //     );
-        //   }
-
-        // },
-
-        // /**
-        //  * Overrides the default Backbone.Model.validate.function() to check if this if
-        //  * the values set on this model are valid.
-        //  * 
-        //  * @param {Object} [attrs] - A literal object of model attributes to validate.
-        //  * @param {Object} [options] - A literal object of options for this validation
-        //  * process
-        //  * 
-        //  * @return {Object} - Returns a literal object with the invalid attributes and
-        //  * their corresponding error message, if there are any. If there are no errors,
-        //  * returns nothing.
-        //  */
-        // validate: function (attrs, options) {
-        //   try {
-
-        //   }
-        //   catch (error) {
-        //     console.log(
-        //       'There was an error validating a VectorFilter model' +
-        //       '. Error details: ' + error
-        //     );
-        //   }
-        // },
-
-        // /**
-        //  * Creates a string using the values set on this model's attributes.
-        //  * @return {string} The VectorFilter string
-        //  */
-        // serialize: function () {
-        //   try {
-        //     var serializedVectorFilter = '';
-
-        //     return serializedVectorFilter;
-        //   }
-        //   catch (error) {
-        //     console.log(
-        //       'There was an error serializing a VectorFilter model' +
-        //       '. Error details: ' + error
-        //     );
-        //   }
-        // },
-
-      });
-
-    return VectorFilter;
-
-  }
-);
+          return visible;
+        } catch (error) {
+          console.log(
+            "There was an error checking feature visibility in a VectorFilter" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      // /**
+      //  * Executed when a new VectorFilter model is created.
+      //  * @param {Object} [attributes] The initial values of the attributes, which will
+      //  * be set on the model.
+      //  * @param {Object} [options] Options for the initialize function.
+      //  */
+      // initialize: function (attributes, options) {
+      //   try {
+
+      //   }
+      //   catch (error) {
+      //     console.log(
+      //       'There was an error initializing a VectorFilter model' +
+      //       '. Error details: ' + error
+      //     );
+      //   }
+      // },
+
+      // /**
+      //  * Parses the given input into a JSON object to be set on the model.
+      //  *
+      //  * @param {TODO} input - The raw response object
+      //  * @return {TODO} - The JSON object of all the VectorFilter attributes
+      //  */
+      // parse: function (input) {
+
+      //   try {
+
+      //     var modelJSON = {};
+
+      //     return modelJSON
+
+      //   }
+      //   catch (error) {
+      //     console.log(
+      //       'There was an error parsing a VectorFilter model' +
+      //       '. Error details: ' + error
+      //     );
+      //   }
+
+      // },
+
+      // /**
+      //  * Overrides the default Backbone.Model.validate.function() to check if this if
+      //  * the values set on this model are valid.
+      //  *
+      //  * @param {Object} [attrs] - A literal object of model attributes to validate.
+      //  * @param {Object} [options] - A literal object of options for this validation
+      //  * process
+      //  *
+      //  * @return {Object} - Returns a literal object with the invalid attributes and
+      //  * their corresponding error message, if there are any. If there are no errors,
+      //  * returns nothing.
+      //  */
+      // validate: function (attrs, options) {
+      //   try {
+
+      //   }
+      //   catch (error) {
+      //     console.log(
+      //       'There was an error validating a VectorFilter model' +
+      //       '. Error details: ' + error
+      //     );
+      //   }
+      // },
+
+      // /**
+      //  * Creates a string using the values set on this model's attributes.
+      //  * @return {string} The VectorFilter string
+      //  */
+      // serialize: function () {
+      //   try {
+      //     var serializedVectorFilter = '';
+
+      //     return serializedVectorFilter;
+      //   }
+      //   catch (error) {
+      //     console.log(
+      //       'There was an error serializing a VectorFilter model' +
+      //       '. Error details: ' + error
+      //     );
+      //   }
+      // },
+    },
+  );
+
+  return VectorFilter;
+});
 
diff --git a/docs/docs/src_js_models_maps_assets_Cesium3DTileset.js.html b/docs/docs/src_js_models_maps_assets_Cesium3DTileset.js.html index 024d81926..eede5c56f 100644 --- a/docs/docs/src_js_models_maps_assets_Cesium3DTileset.js.html +++ b/docs/docs/src_js_models_maps_assets_Cesium3DTileset.js.html @@ -44,435 +44,429 @@

Source: src/js/models/maps/assets/Cesium3DTileset.js

-
'use strict';
-
-define(
-  [
-    'cesium',
-    'models/maps/assets/MapAsset',
-    'models/maps/AssetColorPalette',
-    'collections/maps/VectorFilters'
-  ],
-  function (
-    Cesium,
-    MapAsset,
-    AssetColorPalette,
-    VectorFilters
-  ) {
-    /**
-     * @classdesc A Cesium3DTileset Model is a special type of vector layer that can be used in
-     * Cesium maps, and that follows the 3d-tiles specification. See
-     * {@link https://github.com/CesiumGS/3d-tiles}
-     * @classcategory Models/Maps/Assets
-     * @class Cesium3DTileset
-     * @name Cesium3DTileset
-     * @extends MapAsset
-     * @since 2.18.0
-     * @constructor
-    */
-    var Cesium3DTileset = MapAsset.extend(
-      /** @lends Cesium3DTileset.prototype */ {
-
-        /**
-         * The name of this type of model
-         * @type {string}
-        */
-        type: 'Cesium3DTileset',
-
-        /**
-         * Options that are supported for creating 3D tilesets. The object will be passed
-         * to `Cesium.Cesium3DTileset(options)` as options, so the properties listed in
-         * the Cesium3DTileset documentation are also supported, see
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileset.html}
-         * @typedef {Object} Cesium3DTileset#cesiumOptions
-         * @property {string|number} ionAssetId - If this tileset is hosted by Cesium Ion,
-         * then Ion asset ID. 
-         * @property {string} cesiumToken - If this tileset is hosted by Cesium Ion, then
-         * the token needed to access this resource. If one is not set, then the default
-         * set in the repository's {@link AppConfig#cesiumToken} will be used.
-         */
-
-        /**
-         * Default attributes for Cesium3DTileset models
-         * @name Cesium3DTileset#defaults
-         * @extends MapAsset#defaults
-         * @type {Object}
-         * @property {'Cesium3DTileset'} type The format of the data. Must be
-         * 'Cesium3DTileset'.
-         * @property {VectorFilters} [filters=new VectorFilters()] A set of conditions
-         * used to show or hide specific features of this tileset.
-         * @property {AssetColorPalette} [colorPalette=new AssetColorPalette()] The color
-         * or colors mapped to attributes of this asset. Used to style the features and to
-         * make a legend.
-         * @property {Cesium.Cesium3DTileset} cesiumModel A model created and used by
-         * Cesium that organizes the data to display in the Cesium Widget. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileset.html}
-         * @property {Cesium3DTileset#cesiumOptions} cesiumOptions options are passed
-         * to the function that creates the Cesium model. The properties of options are
-         * specific to each type of asset.
-        */
-        defaults: function () {
-          return Object.assign(
-            {},
-            this.constructor.__super__.defaults(),
-            {
-              type: 'Cesium3DTileset',
-              filters: new VectorFilters(),
-              cesiumModel: null,
-              cesiumOptions: {},
-              colorPalette: new AssetColorPalette(),
-              icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m12.6 12.8 4.9 5c.2.2.2.6 0 .8l-5 5c-.2.1-.5.1-.8 0l-4.9-5a.6.6 0 0 1 0-.8l5-5c.2-.2.5-.2.8 0ZM6.3 6.6l5 5v.7l-5 5c-.2.2-.6.2-.8 0l-5-5a.6.6 0 0 1 0-.8l5-5c.2-.1.6-.1.8 0Zm11 7.8 1.7 1.8c.3.2.3.6 0 .8l-.2.3c-.2.2-.6.2-.8 0l-1.8-1.8c-.2-.3-.2-.6 0-.9l.2-.2c.3-.2.6-.2.9 0ZM22 9.7l1.7 1.8c.3.2.3.6 0 .8l-3.3 3.4c-.2.2-.6.2-.9 0l-1.7-1.8a.6.6 0 0 1 0-.8L21 9.7c.3-.2.6-.2.9 0Zm-6-.2 1.7 1.7c.3.3.3.6 0 .9l-2 2c-.2.2-.6.2-.9 0l-1.7-1.8c-.2-.2-.3-.6 0-.8l2-2c.3-.3.6-.3.9 0ZM12.6.3l4.9 5c.2.2.2.5 0 .8l-5 4.9c-.2.2-.5.2-.8 0L6.8 6a.6.6 0 0 1 0-.8l5-4.9c.2-.2.5-.2.8 0Zm6.2 6.3 1.8 1.7c.2.3.2.7 0 1L19 10.7c-.3.3-.7.3-1 0L16.6 9c-.3-.2-.3-.6 0-1l1.4-1.3c.3-.3.7-.3 1 0Z"/></svg>',
-              featureType: Cesium.Cesium3DTileFeature
-            }
-          );
-        },
-
-        /**
-         * Executed when a new Cesium3DTileset model is created.
-         * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
-         * attributes, which will be set on the model.
-         */
-        initialize: function (assetConfig) {
-          try {
-
-            MapAsset.prototype.initialize.call(this, assetConfig);
-
-            if (assetConfig.filters) {
-              this.set('filters', new VectorFilters(assetConfig.filters))
-            }
-
-            this.createCesiumModel();
-          }
-          catch (error) {
-            console.log(
-              'There was an error initializing a 3DTileset model' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Creates a Cesium.Cesium3DTileset model and sets it to this model's
-         * 'cesiumModel' attribute. This cesiumModel contains all the information required
-         * for Cesium to render tiles. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileset.html?classFilter=3Dtiles}
-         * @param {Boolean} recreate - Set recreate to true to force the function create
-         * the Cesium Model again. Otherwise, if a cesium model already exists, that is
-         * returned instead.
-         */
-        createCesiumModel: function (recreate = false) {
-
-          try {
-
-            // If the cesium model already exists, don't create it again unless specified
-            const currentModel = this.get('cesiumModel')
-            if (!recreate && currentModel) return currentModel
-
-            const model = this;
-            const cesiumOptions = this.getCesiumOptions();
-            let cesiumModel = null
-
-            if (!cesiumOptions) {
-              model.set('status', 'error');
-              model.set('statusDetails', 'No options were set for this tileset.');
-              return;
-            }
-
-            model.resetStatus();
-
-            // If this tileset is a Cesium Ion resource set the url from the asset Id
-            cesiumOptions.url = this.getCesiumURL(cesiumOptions) || cesiumOptions.url;
-
-            cesiumModel = new Cesium.Cesium3DTileset(cesiumOptions)
-            model.set('cesiumModel', cesiumModel)
-            cesiumModel.readyPromise
-              .then(function () {
-                // Let the map views know that the tileset is ready to render
-                model.set('status', 'ready');
-                // Listen to changes in the opacity, color, etc
-                model.setListeners();
-                // Set the initial visibility, opacity, filters, and colors
-                model.updateAppearance();
-              })
-              .otherwise(function (error) {
-                // See https://cesium.com/learn/cesiumjs/ref-doc/RequestErrorEvent.html
-                let details = error;
-                // Write a helpful error message
-                switch (error.statusCode) {
-                  case 404:
-                    details = 'The resource was not found (error code 404).'
-                    break;
-                  case 500:
-                    details = 'There was a server error (error code 500).'
-                    break;
-                }
-                model.set('status', 'error');
-                model.set('statusDetails', details)
-              })
-          }
-          catch (error) {
-            console.log(
-              'Failed to create a Cesium Model within a 3D Tileset model' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-
-        /**
-         * Checks whether there is an asset ID for a Cesium Ion resource and if
-         * so, return the URL to the resource.
-         * @returns {string} The URL to the Cesium Ion resource
-         * @since 2.26.0
-         */
-        getCesiumURL: function () {
-          try {
-            const cesiumOptions = this.getCesiumOptions();
-            if (!cesiumOptions || !cesiumOptions.ionAssetId) return null
-            // The Cesium Ion ID of the resource to access
-            const assetId = Number(cesiumOptions.ionAssetId)
-            // Options to pass to Cesium's fromAssetId function. Access token
-            // needs to be set before requesting cesium ion resources
-            const ionResourceOptions = {
-              accessToken: cesiumOptions.cesiumToken ||
-                MetacatUI.appModel.get('cesiumToken')
-            }
-            // Create the new URL and set it on the model options
-            return Cesium.IonResource.fromAssetId(assetId, ionResourceOptions);
-          }
-          catch (error) {
-            console.log(
-              'There was an error settings a Cesium URL in a Cesium3DTileset' +
-              '. Error details: ' + error
-            );
+            
"use strict";
+
+define([
+  "cesium",
+  "models/maps/assets/MapAsset",
+  "models/maps/AssetColorPalette",
+  "collections/maps/VectorFilters",
+], function (Cesium, MapAsset, AssetColorPalette, VectorFilters) {
+  /**
+   * @classdesc A Cesium3DTileset Model is a special type of vector layer that can be used in
+   * Cesium maps, and that follows the 3d-tiles specification. See
+   * {@link https://github.com/CesiumGS/3d-tiles}
+   * @classcategory Models/Maps/Assets
+   * @class Cesium3DTileset
+   * @name Cesium3DTileset
+   * @extends MapAsset
+   * @since 2.18.0
+   * @constructor
+   */
+  var Cesium3DTileset = MapAsset.extend(
+    /** @lends Cesium3DTileset.prototype */ {
+      /**
+       * The name of this type of model
+       * @type {string}
+       */
+      type: "Cesium3DTileset",
+
+      /**
+       * Options that are supported for creating 3D tilesets. The object will be passed
+       * to `Cesium.Cesium3DTileset(options)` as options, so the properties listed in
+       * the Cesium3DTileset documentation are also supported, see
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileset.html}
+       * @typedef {Object} Cesium3DTileset#cesiumOptions
+       * @property {string|number} ionAssetId - If this tileset is hosted by Cesium Ion,
+       * then Ion asset ID.
+       * @property {string} cesiumToken - If this tileset is hosted by Cesium Ion, then
+       * the token needed to access this resource. If one is not set, then the default
+       * set in the repository's {@link AppConfig#cesiumToken} will be used.
+       */
+
+      /**
+       * Default attributes for Cesium3DTileset models
+       * @name Cesium3DTileset#defaults
+       * @extends MapAsset#defaults
+       * @type {Object}
+       * @property {'Cesium3DTileset'} type The format of the data. Must be
+       * 'Cesium3DTileset'.
+       * @property {VectorFilters} [filters=new VectorFilters()] A set of conditions
+       * used to show or hide specific features of this tileset.
+       * @property {AssetColorPalette} [colorPalette=new AssetColorPalette()] The color
+       * or colors mapped to attributes of this asset. Used to style the features and to
+       * make a legend.
+       * @property {Cesium.Cesium3DTileset} cesiumModel A model created and used by
+       * Cesium that organizes the data to display in the Cesium Widget. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileset.html}
+       * @property {Cesium3DTileset#cesiumOptions} cesiumOptions options are passed
+       * to the function that creates the Cesium model. The properties of options are
+       * specific to each type of asset.
+       */
+      defaults: function () {
+        return Object.assign({}, this.constructor.__super__.defaults(), {
+          type: "Cesium3DTileset",
+          filters: new VectorFilters(),
+          cesiumModel: null,
+          cesiumOptions: {},
+          colorPalette: new AssetColorPalette(),
+          icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m12.6 12.8 4.9 5c.2.2.2.6 0 .8l-5 5c-.2.1-.5.1-.8 0l-4.9-5a.6.6 0 0 1 0-.8l5-5c.2-.2.5-.2.8 0ZM6.3 6.6l5 5v.7l-5 5c-.2.2-.6.2-.8 0l-5-5a.6.6 0 0 1 0-.8l5-5c.2-.1.6-.1.8 0Zm11 7.8 1.7 1.8c.3.2.3.6 0 .8l-.2.3c-.2.2-.6.2-.8 0l-1.8-1.8c-.2-.3-.2-.6 0-.9l.2-.2c.3-.2.6-.2.9 0ZM22 9.7l1.7 1.8c.3.2.3.6 0 .8l-3.3 3.4c-.2.2-.6.2-.9 0l-1.7-1.8a.6.6 0 0 1 0-.8L21 9.7c.3-.2.6-.2.9 0Zm-6-.2 1.7 1.7c.3.3.3.6 0 .9l-2 2c-.2.2-.6.2-.9 0l-1.7-1.8c-.2-.2-.3-.6 0-.8l2-2c.3-.3.6-.3.9 0ZM12.6.3l4.9 5c.2.2.2.5 0 .8l-5 4.9c-.2.2-.5.2-.8 0L6.8 6a.6.6 0 0 1 0-.8l5-4.9c.2-.2.5-.2.8 0Zm6.2 6.3 1.8 1.7c.2.3.2.7 0 1L19 10.7c-.3.3-.7.3-1 0L16.6 9c-.3-.2-.3-.6 0-1l1.4-1.3c.3-.3.7-.3 1 0Z"/></svg>',
+          featureType: Cesium.Cesium3DTileFeature,
+        });
+      },
+
+      /**
+       * Executed when a new Cesium3DTileset model is created.
+       * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
+       * attributes, which will be set on the model.
+       */
+      initialize: function (assetConfig) {
+        try {
+          MapAsset.prototype.initialize.call(this, assetConfig);
+
+          if (assetConfig.filters) {
+            this.set("filters", new VectorFilters(assetConfig.filters));
           }
-        },
 
-        /**
-         * Set listeners that update the cesium model when the backbone model is updated.
-         */
-        setListeners: function () {
-          try {
-
-            // call the super method
-            this.constructor.__super__.setListeners.call(this);
-
-            // When opacity, color, or visibility changes (will also update the filters)
-            this.stopListening(this, 'change:opacity change:color change:visible')
-            this.listenTo(
-              this, 'change:opacity change:color change:visible', this.updateAppearance
-            )
+          this.createCesiumModel();
+        } catch (error) {
+          console.log(
+            "There was an error initializing a 3DTileset model" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Creates a Cesium.Cesium3DTileset model and sets it to this model's
+       * 'cesiumModel' attribute. This cesiumModel contains all the information required
+       * for Cesium to render tiles. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileset.html?classFilter=3Dtiles}
+       * @param {Boolean} recreate - Set recreate to true to force the function create
+       * the Cesium Model again. Otherwise, if a cesium model already exists, that is
+       * returned instead.
+       */
+      createCesiumModel: function (recreate = false) {
+        try {
+          // If the cesium model already exists, don't create it again unless specified
+          const currentModel = this.get("cesiumModel");
+          if (!recreate && currentModel) return currentModel;
 
-            // When filters change
-            this.stopListening(this.get('filters'), 'update')
-            this.listenTo(this.get('filters'), 'update', this.updateFeatureVisibility)
+          const model = this;
+          const cesiumOptions = this.getCesiumOptions();
+          let cesiumModel = null;
 
+          if (!cesiumOptions) {
+            model.set("status", "error");
+            model.set("statusDetails", "No options were set for this tileset.");
+            return;
           }
-          catch (error) {
-            console.log(
-              'There was an error setting listeners in a Cesium3DTileset' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Sets a new Cesium3DTileStyle on the Cesium 3D tileset model's style property,
-         * based on the attributes set on this model. 
-         */
-        updateAppearance: function () {
-          try {
-            const model = this;
-            // The style set on the Cesium 3D tileset needs to be updated to show the
-            // changes on a Cesium map.
-            const cesiumModel = model.get('cesiumModel')
-
-            if(!cesiumModel) return
-
-            // If the layer isn't visible at all, don't bother setting up colors or
-            // filters. Just set every feature to hidden.
-            if (!model.isVisible()) {
-              cesiumModel.style = new Cesium.Cesium3DTileStyle({
-                show: false
-              });
-              // Indicate that the layer is hidden if the opacity is zero by updating the
-              // visibility property
-              if (model.get('opacity') === 0) {
-                model.set('visible', false);
-              }
 
-              // Let the map and/or other parent views know that a change has been made
-              // that requires the map to be re-rendered
-              model.trigger('appearanceChanged')
-            } else {
-              // Set a new 3D style with a  function that Cesium will use to shade each
-              // feature.
-              cesiumModel.style = new Cesium.Cesium3DTileStyle({
-                color: {
-                  evaluateColor: model.getColorFunction(),
-                }
-              });
-              // Since the style has to be reset, re-add the filters expression. This also
-              // triggers the appearanceChanged event
-              model.updateFeatureVisibility()
-            }
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error updating a 3D Tile style property in a Cesium3DTileset' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Updates the Cesium style set on this tileset. Adds a new expression to the
-         * show property that will filter the features based on the filters set on this
-         * model.
-         */
-        updateFeatureVisibility: function () {
-          try {
-
-            const model = this;
-            const cesiumModel = this.get('cesiumModel')
-            const filters = this.get('filters')
-
-            // If there are no filters, just set the show property to true
-            if (!filters || !filters.length) {
-              cesiumModel.style.show = true
-            } else {
-              const expression = new Cesium.StyleExpression()
-              expression.evaluate = function (feature) {
-                const properties = model.getPropertiesFromFeature(feature)
-                return model.featureIsVisible(properties)
+          model.resetStatus();
+
+          // If this tileset is a Cesium Ion resource set the url from the asset Id
+          cesiumOptions.url =
+            this.getCesiumURL(cesiumOptions) || cesiumOptions.url;
+
+          cesiumModel = new Cesium.Cesium3DTileset(cesiumOptions);
+          model.set("cesiumModel", cesiumModel);
+          cesiumModel.readyPromise
+            .then(function () {
+              // Let the map views know that the tileset is ready to render
+              model.set("status", "ready");
+              // Listen to changes in the opacity, color, etc
+              model.setListeners();
+              // Set the initial visibility, opacity, filters, and colors
+              model.updateAppearance();
+            })
+            .otherwise(function (error) {
+              // See https://cesium.com/learn/cesiumjs/ref-doc/RequestErrorEvent.html
+              let details = error;
+              // Write a helpful error message
+              switch (error.statusCode) {
+                case 404:
+                  details = "The resource was not found (error code 404).";
+                  break;
+                case 500:
+                  details = "There was a server error (error code 500).";
+                  break;
               }
-              cesiumModel.style.show = expression
+              model.set("status", "error");
+              model.set("statusDetails", details);
+            });
+        } catch (error) {
+          console.log(
+            "Failed to create a Cesium Model within a 3D Tileset model" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Checks whether there is an asset ID for a Cesium Ion resource and if
+       * so, return the URL to the resource.
+       * @returns {string} The URL to the Cesium Ion resource
+       * @since 2.26.0
+       */
+      getCesiumURL: function () {
+        try {
+          const cesiumOptions = this.getCesiumOptions();
+          if (!cesiumOptions || !cesiumOptions.ionAssetId) return null;
+          // The Cesium Ion ID of the resource to access
+          const assetId = Number(cesiumOptions.ionAssetId);
+          // Options to pass to Cesium's fromAssetId function. Access token
+          // needs to be set before requesting cesium ion resources
+          const ionResourceOptions = {
+            accessToken:
+              cesiumOptions.cesiumToken ||
+              MetacatUI.appModel.get("cesiumToken"),
+          };
+          // Create the new URL and set it on the model options
+          return Cesium.IonResource.fromAssetId(assetId, ionResourceOptions);
+        } catch (error) {
+          console.log(
+            "There was an error settings a Cesium URL in a Cesium3DTileset" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Set listeners that update the cesium model when the backbone model is updated.
+       */
+      setListeners: function () {
+        try {
+          // call the super method
+          this.constructor.__super__.setListeners.call(this);
+
+          // When opacity, color, or visibility changes (will also update the filters)
+          this.stopListening(
+            this,
+            "change:opacity change:color change:visible",
+          );
+          this.listenTo(
+            this,
+            "change:opacity change:color change:visible",
+            this.updateAppearance,
+          );
+
+          // When filters change
+          this.stopListening(this.get("filters"), "update");
+          this.listenTo(
+            this.get("filters"),
+            "update",
+            this.updateFeatureVisibility,
+          );
+        } catch (error) {
+          console.log(
+            "There was an error setting listeners in a Cesium3DTileset" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Sets a new Cesium3DTileStyle on the Cesium 3D tileset model's style property,
+       * based on the attributes set on this model.
+       */
+      updateAppearance: function () {
+        try {
+          const model = this;
+          // The style set on the Cesium 3D tileset needs to be updated to show the
+          // changes on a Cesium map.
+          const cesiumModel = model.get("cesiumModel");
+
+          if (!cesiumModel) return;
+
+          // If the layer isn't visible at all, don't bother setting up colors or
+          // filters. Just set every feature to hidden.
+          if (!model.isVisible()) {
+            cesiumModel.style = new Cesium.Cesium3DTileStyle({
+              show: false,
+            });
+            // Indicate that the layer is hidden if the opacity is zero by updating the
+            // visibility property
+            if (model.get("opacity") === 0) {
+              model.set("visible", false);
             }
-            model.trigger('appearanceChanged')
 
+            // Let the map and/or other parent views know that a change has been made
+            // that requires the map to be re-rendered
+            model.trigger("appearanceChanged");
+          } else {
+            // Set a new 3D style with a  function that Cesium will use to shade each
+            // feature.
+            cesiumModel.style = new Cesium.Cesium3DTileStyle({
+              color: {
+                evaluateColor: model.getColorFunction(),
+              },
+            });
+            // Since the style has to be reset, re-add the filters expression. This also
+            // triggers the appearanceChanged event
+            model.updateFeatureVisibility();
           }
-          catch (error) {
-            console.log(
-              'There was an error  in a Cesium3DTileset' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Given a feature from a Cesium 3D tileset, returns any properties that are set
-         * on the feature, similar to an attributes table.
-         * @param {Cesium.Cesium3DTileFeature} feature A Cesium 3D Tile feature
-         * @returns {Object} An object containing key-value mapping of property names to
-         * properties.
-         */
-        getPropertiesFromFeature: function(feature) {
-          if (!this.usesFeatureType(feature)) return null
-          let properties = {};
-          feature.getPropertyNames().forEach(function (propertyName) {
-            properties[propertyName] = feature.getProperty(propertyName)
-          })
-          properties = this.addCustomProperties(properties)
-          return properties
-        },
-
-        /**
-         * Return the label for a feature from a Cesium 3D tileset
-         * @param {Cesium.Cesium3DTileFeature} feature A Cesium 3D Tile feature
-         * @returns {string} The label
-         * @since 2.25.0
-         */
-        getLabelFromFeature: function (feature) {
-          if (!this.usesFeatureType(feature)) return null
-          return feature.getProperty('name') || feature.getProperty('label') || null
-        },
-
-        /**
-         * Return the Cesium3DTileset model for a feature from a Cesium 3D tileset
-         * @param {Cesium.Cesium3DTileFeature} feature A Cesium 3D Tile feature
-         * @returns {Cesium3DTileset} The model
-         * @since 2.25.0
-         */
-        getCesiumModelFromFeature: function (feature) {
-          if (!this.usesFeatureType(feature)) return null
-          return feature.primitive
-        },
-
-        /**
-         * Return the ID used by Cesium for a feature from a Cesium 3D tileset
-         * @param {Cesium.Cesium3DTileFeature} feature A Cesium 3D Tile feature
-         * @returns {string} The ID
-         * @since 2.25.0
-         */
-        getIDFromFeature: function (feature) {
-          if (!this.usesFeatureType(feature)) return null
-          return feature.pickId ? feature.pickId.key : null
-        },
-
-        /**
-         * Creates a function that takes a Cesium3DTileFeature (see
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileFeature.html}) and
-         * returns a Cesium color based on the colorPalette property set on this model.
-         * The returned function is designed to be used as the evaluateColor function that
-         * is set in the color property of a Cesium3DTileStyle StyleExpression. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileStyle.html#color}
-         * @returns {function} A Cesium 3dTile evaluate color function
-         */
-        getColorFunction: function () {
-          try {
-            const model = this;
-            // Opacity of the entire layer is set by using it as the alpha for each color
-            const opacity = model.get('opacity')
-
-            const evaluateColor = function (feature) {
+        } catch (error) {
+          console.log(
+            "There was an error updating a 3D Tile style property in a Cesium3DTileset" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Updates the Cesium style set on this tileset. Adds a new expression to the
+       * show property that will filter the features based on the filters set on this
+       * model.
+       */
+      updateFeatureVisibility: function () {
+        try {
+          const model = this;
+          const cesiumModel = this.get("cesiumModel");
+          const filters = this.get("filters");
+
+          // If there are no filters, just set the show property to true
+          if (!filters || !filters.length) {
+            cesiumModel.style.show = true;
+          } else {
+            const expression = new Cesium.StyleExpression();
+            expression.evaluate = function (feature) {
               const properties = model.getPropertiesFromFeature(feature);
-              let featureOpacity = opacity;
-              // If the feature is currently selected, set the opacity to max (otherwise the
-              // 'silhouette' borders in the map do not show in the Cesium widget)
-              if (model.featureIsSelected(feature)) {
-                featureOpacity = 1
-              }
-              const rgb = model.getColor(properties)
-              if (rgb) {
-                return new Cesium.Color(rgb.red, rgb.green, rgb.blue, featureOpacity);
-              } else {
-                return new Cesium.Color();
-              }
-            }
-            return evaluateColor
-          }
-          catch (error) {
-            console.log(
-              'There was an error creating a color function in a Cesium3DTileset model' +
-              '. Error details: ' + error
-            );
+              return model.featureIsVisible(properties);
+            };
+            cesiumModel.style.show = expression;
           }
-        },
-
-        /**
-        * Gets a Cesium Bounding Sphere that can be used to navigate to view the full
-        * extent of the tileset. See
-        * {@link https://cesium.com/learn/cesiumjs/ref-doc/BoundingSphere.html}.
-        * @returns {Promise} Returns a promise that resolves to a Cesium Bounding Sphere
-        * when ready
-        */
-        getBoundingSphere: function () {
+          model.trigger("appearanceChanged");
+        } catch (error) {
+          console.log(
+            "There was an error  in a Cesium3DTileset" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Given a feature from a Cesium 3D tileset, returns any properties that are set
+       * on the feature, similar to an attributes table.
+       * @param {Cesium.Cesium3DTileFeature} feature A Cesium 3D Tile feature
+       * @returns {Object} An object containing key-value mapping of property names to
+       * properties.
+       */
+      getPropertiesFromFeature: function (feature) {
+        if (!this.usesFeatureType(feature)) return null;
+        let properties = {};
+        feature.getPropertyNames().forEach(function (propertyName) {
+          properties[propertyName] = feature.getProperty(propertyName);
+        });
+        properties = this.addCustomProperties(properties);
+        return properties;
+      },
+
+      /**
+       * Return the label for a feature from a Cesium 3D tileset
+       * @param {Cesium.Cesium3DTileFeature} feature A Cesium 3D Tile feature
+       * @returns {string} The label
+       * @since 2.25.0
+       */
+      getLabelFromFeature: function (feature) {
+        if (!this.usesFeatureType(feature)) return null;
+        return (
+          feature.getProperty("name") || feature.getProperty("label") || null
+        );
+      },
+
+      /**
+       * Return the Cesium3DTileset model for a feature from a Cesium 3D tileset
+       * @param {Cesium.Cesium3DTileFeature} feature A Cesium 3D Tile feature
+       * @returns {Cesium3DTileset} The model
+       * @since 2.25.0
+       */
+      getCesiumModelFromFeature: function (feature) {
+        if (!this.usesFeatureType(feature)) return null;
+        return feature.primitive;
+      },
+
+      /**
+       * Return the ID used by Cesium for a feature from a Cesium 3D tileset
+       * @param {Cesium.Cesium3DTileFeature} feature A Cesium 3D Tile feature
+       * @returns {string} The ID
+       * @since 2.25.0
+       */
+      getIDFromFeature: function (feature) {
+        if (!this.usesFeatureType(feature)) return null;
+        return feature.pickId ? feature.pickId.key : null;
+      },
+
+      /**
+       * Creates a function that takes a Cesium3DTileFeature (see
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileFeature.html}) and
+       * returns a Cesium color based on the colorPalette property set on this model.
+       * The returned function is designed to be used as the evaluateColor function that
+       * is set in the color property of a Cesium3DTileStyle StyleExpression. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileStyle.html#color}
+       * @returns {function} A Cesium 3dTile evaluate color function
+       */
+      getColorFunction: function () {
+        try {
           const model = this;
-          return this.whenReady()
-            .then(function (model) {
-              const tileset = model.get('cesiumModel')
-              const bSphere = Cesium.BoundingSphere.clone(tileset.boundingSphere);
-              return bSphere
-            })
+          // Opacity of the entire layer is set by using it as the alpha for each color
+          const opacity = model.get("opacity");
+
+          const evaluateColor = function (feature) {
+            const properties = model.getPropertiesFromFeature(feature);
+            let featureOpacity = opacity;
+            // If the feature is currently selected, set the opacity to max (otherwise the
+            // 'silhouette' borders in the map do not show in the Cesium widget)
+            if (model.featureIsSelected(feature)) {
+              featureOpacity = 1;
+            }
+            const rgb = model.getColor(properties);
+            if (rgb) {
+              return new Cesium.Color(
+                rgb.red,
+                rgb.green,
+                rgb.blue,
+                featureOpacity,
+              );
+            } else {
+              return new Cesium.Color();
+            }
+          };
+          return evaluateColor;
+        } catch (error) {
+          console.log(
+            "There was an error creating a color function in a Cesium3DTileset model" +
+              ". Error details: " +
+              error,
+          );
         }
-
-      });
-
-    return Cesium3DTileset;
-
-  }
-);
+      },
+
+      /**
+       * Gets a Cesium Bounding Sphere that can be used to navigate to view the full
+       * extent of the tileset. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/BoundingSphere.html}.
+       * @returns {Promise} Returns a promise that resolves to a Cesium Bounding Sphere
+       * when ready
+       */
+      getBoundingSphere: function () {
+        const model = this;
+        return this.whenReady().then(function (model) {
+          const tileset = model.get("cesiumModel");
+          const bSphere = Cesium.BoundingSphere.clone(tileset.boundingSphere);
+          return bSphere;
+        });
+      },
+    },
+  );
+
+  return Cesium3DTileset;
+});
 
diff --git a/docs/docs/src_js_models_maps_assets_CesiumGeohash.js.html b/docs/docs/src_js_models_maps_assets_CesiumGeohash.js.html index d8dc849a1..c9e055f3c 100644 --- a/docs/docs/src_js_models_maps_assets_CesiumGeohash.js.html +++ b/docs/docs/src_js_models_maps_assets_CesiumGeohash.js.html @@ -65,7 +65,7 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

AssetColorPalette, AssetColor, Geohash, - Geohashes + Geohashes, ) { /** * @classdesc A Geohash Model represents a geohash layer in a map. @@ -173,7 +173,7 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

* For the property of interest (e.g. count) Get the min and max values * from the geohashes collection and update the color palette. * @since 2.25.0 - * + * */ updateColorRangeValues: function () { const colorPalette = this.get("colorPalette"); @@ -198,7 +198,7 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

*/ getPrecision: function () { const limit = this.get("maxGeoHashes"); - const geohashes = this.get("geohashes") + const geohashes = this.get("geohashes"); const area = this.getViewExtent().getArea(); return geohashes.getMaxPrecision(area, limit); }, @@ -236,7 +236,7 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

function () { this.updateColorRangeValues(); this.createCesiumModel(true); - } + }, ); } catch (error) { console.log("Failed to set listeners in CesiumGeohash", error); @@ -251,7 +251,7 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

* to those that are within the current map extent. * @returns {Geohashes} The geohashes to display. */ - getGeohashes: function(limitToExtent = true) { + getGeohashes: function (limitToExtent = true) { let geohashes = this.get("geohashes"); if (limitToExtent) { geohashes = this.getGeohashesForExtent(); @@ -275,7 +275,7 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

* @returns {GeoBoundingBox} The current map extent */ getViewExtent: function () { - return this.get("mapModel")?.get("interactions")?.get("viewExtent") + return this.get("mapModel")?.get("interactions")?.get("viewExtent"); }, /** @@ -323,7 +323,8 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

// Set the GeoJSON representing geohashes on the model const cesiumOptions = this.getCesiumOptions(); const type = model.get("type"); - const data = type === "GeoJsonDataSource" ? this.getGeoJSON() : this.getCZML(); + const data = + type === "GeoJsonDataSource" ? this.getGeoJSON() : this.getCZML(); cesiumOptions["data"] = data; cesiumOptions["height"] = 0; model.set("cesiumOptions", cesiumOptions); @@ -342,10 +343,15 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

*/ selectGeohashes: function (geohashes) { try { - const toSelect = [...new Set(geohashes.map((geohash) => { - const parent = this.get("geohashes").getContainingGeohash(geohash); - return parent?.get("hashString"); - }, this))]; + const toSelect = [ + ...new Set( + geohashes.map((geohash) => { + const parent = + this.get("geohashes").getContainingGeohash(geohash); + return parent?.get("hashString"); + }, this), + ), + ]; const entities = this.get("cesiumModel").entities.values; const selected = entities.filter((entity) => { const hashString = this.getPropertiesFromFeature(entity).hashString; @@ -359,7 +365,7 @@

Source: src/js/models/maps/assets/CesiumGeohash.js

console.log("Error selecting geohashes", e); } }, - } + }, ); });
diff --git a/docs/docs/src_js_models_maps_assets_CesiumImagery.js.html b/docs/docs/src_js_models_maps_assets_CesiumImagery.js.html index 95472eac4..a32c5427b 100644 --- a/docs/docs/src_js_models_maps_assets_CesiumImagery.js.html +++ b/docs/docs/src_js_models_maps_assets_CesiumImagery.js.html @@ -44,464 +44,475 @@

Source: src/js/models/maps/assets/CesiumImagery.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'cesium',
-    'models/maps/assets/MapAsset',
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    Cesium,
-    MapAsset
-  ) {
-    /**
-     * @classdesc A CesiumImagery Model contains the information required for Cesium to
-     * request and draw high-resolution image tiles using several standards (Cesium
-     * "imagery providers"), including Cesium Ion and Bing Maps. Imagery layers have
-     * brightness, contrast, gamma, hue, and saturation properties that can be dynamically
-     * changed. 
-     * @classcategory Models/Maps/Assets
-     * @class CesiumImagery
-     * @name CesiumImagery
-     * @extends MapAsset
-     * @since 2.18.0
-     * @constructor
-    */
-    var CesiumImagery = MapAsset.extend(
-      /** @lends CesiumImagery.prototype */ {
-
-        /**
-         * The name of this type of model
-         * @type {string}
-        */
-        type: 'CesiumImagery',
-
-        /**
-         * Options that are supported for creating imagery tiles. Any properties provided
-         * here are passed to the Cesium constructor function, so other properties that
-         * are documented in Cesium are also supported. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/BingMapsImageryProvider.html#.ConstructorOptions}
-         * and
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/IonImageryProvider.html#.ConstructorOptions}.
-         * @typedef {Object} CesiumImagery#cesiumOptions
-         * @property {string|number} ionAssetId - If this imagery is hosted by Cesium
-         * Ion, then Ion asset ID. 
-         * @property {string|number} key - A key or token required to access the tiles.
-         * For example, if this is a BingMapsImageryProvider, then the Bing maps key. If
-         * one is required and not set, the model will look in the {@link AppModel} for a
-         * key, for example, {@link AppModel#bingMapsKey}
-         * @property {'GeographicTilingScheme'|'WebMercatorTilingScheme'} tilingScheme -
-         * The tiling scheme to use when constructing an imagery provider. If not set,
-         * Cesium uses WebMercatorTilingScheme by default.
-         * @property {Number[]} rectangle - The rectangle covered by the layer. The list
-         * of west, south, east, north bounding degree coordinates, respectively. This
-         * will be passed to Cesium.Rectangle.fromDegrees to define the bounding box of
-         * the imagery layer. If left undefined, the layer will cover the entire globe.
-         */
-
-        /**
-         * Default attributes for CesiumImagery models
-         * @name CesiumImagery#defaults
-         * @extends MapAsset#defaults
-         * @type {Object}
-         * @property {'BingMapsImageryProvider'|'IonImageryProvider'|'TileMapServiceImageryProvider'|'WebMapTileServiceImageryProvider'} type
-         * A string indicating a Cesium Imagery Provider type. See
-         * {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#more-imagery-providers}
-         * @property {Cesium.ImageryLayer} cesiumModel A model created and used by Cesium
-         * that organizes the data to display in the Cesium Widget. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/ImageryLayer.html?classFilter=ImageryLayer}
-         * and
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/?classFilter=ImageryProvider}
-         * @property {CesiumImagery#cesiumOptions} cesiumOptions options that are passed
-         * to the function that creates the Cesium model. The properties of options are
-         * specific to each type of asset.
-        */
-        defaults: function () {
-          return _.extend(
-            this.constructor.__super__.defaults(),
-            {
-              type: '',
-              cesiumModel: null,
-              cesiumOptions: {},
-              // brightness: 1, contrast: 1, gamma: 1, hue: 0, saturation: 1,
-            }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "cesium",
+  "models/maps/assets/MapAsset",
+], function ($, _, Backbone, Cesium, MapAsset) {
+  /**
+   * @classdesc A CesiumImagery Model contains the information required for Cesium to
+   * request and draw high-resolution image tiles using several standards (Cesium
+   * "imagery providers"), including Cesium Ion and Bing Maps. Imagery layers have
+   * brightness, contrast, gamma, hue, and saturation properties that can be dynamically
+   * changed.
+   * @classcategory Models/Maps/Assets
+   * @class CesiumImagery
+   * @name CesiumImagery
+   * @extends MapAsset
+   * @since 2.18.0
+   * @constructor
+   */
+  var CesiumImagery = MapAsset.extend(
+    /** @lends CesiumImagery.prototype */ {
+      /**
+       * The name of this type of model
+       * @type {string}
+       */
+      type: "CesiumImagery",
+
+      /**
+       * Options that are supported for creating imagery tiles. Any properties provided
+       * here are passed to the Cesium constructor function, so other properties that
+       * are documented in Cesium are also supported. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/BingMapsImageryProvider.html#.ConstructorOptions}
+       * and
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/IonImageryProvider.html#.ConstructorOptions}.
+       * @typedef {Object} CesiumImagery#cesiumOptions
+       * @property {string|number} ionAssetId - If this imagery is hosted by Cesium
+       * Ion, then Ion asset ID.
+       * @property {string|number} key - A key or token required to access the tiles.
+       * For example, if this is a BingMapsImageryProvider, then the Bing maps key. If
+       * one is required and not set, the model will look in the {@link AppModel} for a
+       * key, for example, {@link AppModel#bingMapsKey}
+       * @property {'GeographicTilingScheme'|'WebMercatorTilingScheme'} tilingScheme -
+       * The tiling scheme to use when constructing an imagery provider. If not set,
+       * Cesium uses WebMercatorTilingScheme by default.
+       * @property {Number[]} rectangle - The rectangle covered by the layer. The list
+       * of west, south, east, north bounding degree coordinates, respectively. This
+       * will be passed to Cesium.Rectangle.fromDegrees to define the bounding box of
+       * the imagery layer. If left undefined, the layer will cover the entire globe.
+       */
+
+      /**
+       * Default attributes for CesiumImagery models
+       * @name CesiumImagery#defaults
+       * @extends MapAsset#defaults
+       * @type {Object}
+       * @property {'BingMapsImageryProvider'|'IonImageryProvider'|'TileMapServiceImageryProvider'|'WebMapTileServiceImageryProvider'} type
+       * A string indicating a Cesium Imagery Provider type. See
+       * {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#more-imagery-providers}
+       * @property {Cesium.ImageryLayer} cesiumModel A model created and used by Cesium
+       * that organizes the data to display in the Cesium Widget. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/ImageryLayer.html?classFilter=ImageryLayer}
+       * and
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/?classFilter=ImageryProvider}
+       * @property {CesiumImagery#cesiumOptions} cesiumOptions options that are passed
+       * to the function that creates the Cesium model. The properties of options are
+       * specific to each type of asset.
+       */
+      defaults: function () {
+        return _.extend(this.constructor.__super__.defaults(), {
+          type: "",
+          cesiumModel: null,
+          cesiumOptions: {},
+          // brightness: 1, contrast: 1, gamma: 1, hue: 0, saturation: 1,
+        });
+      },
+
+      /**
+       * Executed when a new CesiumImagery model is created.
+       * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
+       * attributes, which will be set on the model.
+       */
+      initialize: function (assetConfig) {
+        try {
+          MapAsset.prototype.initialize.call(this, assetConfig);
+
+          if (assetConfig.type == "NaturalEarthII") {
+            this.initNaturalEarthII(assetConfig);
+          } else if (assetConfig.type == "USGSImageryTopo") {
+            this.initUSGSImageryTopo(assetConfig);
+          }
+
+          this.createCesiumModel();
+
+          this.getThumbnail();
+        } catch (e) {
+          console.log("Error initializing a CesiumImagery model: ", e);
+        }
+      },
+
+      /**
+       * Initializes a CesiumImagery model for the Natural Earth II asset.
+       * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
+       * attributes, which will be set on the model.
+       */
+      initNaturalEarthII: function (assetConfig) {
+        try {
+          if (
+            !assetConfig.cesiumOptions ||
+            typeof assetConfig.cesiumOptions !== "object"
+          ) {
+            assetConfig.cesiumOptions = {};
+          }
+
+          assetConfig.cesiumOptions.url = Cesium.buildModuleUrl(
+            "Assets/Textures/NaturalEarthII",
+          );
+          this.set("type", "TileMapServiceImageryProvider");
+          this.set("cesiumOptions", assetConfig.cesiumOptions);
+        } catch (error) {
+          console.log(
+            "There was an error initializing NaturalEarthII in a CesiumImagery" +
+              ". Error details: " +
+              error,
           );
-        },
-
-        /**
-         * Executed when a new CesiumImagery model is created.
-         * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
-         * attributes, which will be set on the model.
-         */
-        initialize: function (assetConfig) {
-          try {
-            MapAsset.prototype.initialize.call(this, assetConfig);
-
-            if (assetConfig.type == 'NaturalEarthII') {
-              this.initNaturalEarthII(assetConfig);
-            }
-            else if (assetConfig.type == 'USGSImageryTopo') {
-              this.initUSGSImageryTopo(assetConfig);
-            }
-
-            this.createCesiumModel();
-
-            this.getThumbnail();
+        }
+      },
+
+      /**
+       * Initializes a CesiumImagery model for the USGS Imagery Topo asset.
+       * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
+       * attributes, which will be set on the model.
+       */
+      initUSGSImageryTopo: function (assetConfig) {
+        try {
+          if (
+            !assetConfig.cesiumOptions ||
+            typeof assetConfig.cesiumOptions !== "object"
+          ) {
+            assetConfig.cesiumOptions = {};
           }
-          catch (e) {
-            console.log('Error initializing a CesiumImagery model: ', e);
+          this.set("type", "WebMapServiceImageryProvider");
+          assetConfig.cesiumOptions.url =
+            "https://basemap.nationalmap.gov:443/arcgis/services/USGSImageryTopo/MapServer/WmsServer";
+          assetConfig.cesiumOptions.layers = "0";
+          assetConfig.cesiumOptions.parameters = {
+            transparent: true,
+            format: "image/png",
+          };
+          this.set("cesiumOptions", assetConfig.cesiumOptions);
+          if (!assetConfig.moreInfoLink) {
+            this.set(
+              "moreInfoLink",
+              "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer",
+            );
           }
-        },
-
-        /**
-         * Initializes a CesiumImagery model for the Natural Earth II asset.
-         * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
-         * attributes, which will be set on the model.
-         */
-        initNaturalEarthII: function (assetConfig) {
-          try {
-            if (
-              !assetConfig.cesiumOptions || typeof assetConfig.cesiumOptions !== 'object'
-            ) {
-              assetConfig.cesiumOptions = {};
-            }
-
-            assetConfig.cesiumOptions.url = Cesium.buildModuleUrl(
-              'Assets/Textures/NaturalEarthII'
+          if (!assetConfig.attribution) {
+            this.set(
+              "attribution",
+              "USGS The National Map: Orthoimagery and US Topo. Data refreshed January, 2022.",
             );
-            this.set('type', 'TileMapServiceImageryProvider')
-            this.set('cesiumOptions', assetConfig.cesiumOptions);
           }
-          catch (error) {
-            console.log(
-              'There was an error initializing NaturalEarthII in a CesiumImagery' +
-              '. Error details: ' + error
+          if (!assetConfig.description) {
+            this.set(
+              "description",
+              "USGS Imagery Topo is a tile cache base map of orthoimagery in The National Map and US Topo vectors visible to the 1:9,028 scale.",
             );
           }
-        },
-
-        /**
-         * Initializes a CesiumImagery model for the USGS Imagery Topo asset.
-         * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
-         * attributes, which will be set on the model.
-         */
-        initUSGSImageryTopo: function (assetConfig) {
-          try {
-            if (
-              !assetConfig.cesiumOptions || typeof assetConfig.cesiumOptions !== 'object'
-            ) {
-              assetConfig.cesiumOptions = {};
-            }
-            this.set('type', 'WebMapServiceImageryProvider')
-            assetConfig.cesiumOptions.url = 'https://basemap.nationalmap.gov:443/arcgis/services/USGSImageryTopo/MapServer/WmsServer'
-            assetConfig.cesiumOptions.layers = '0'
-            assetConfig.cesiumOptions.parameters = {
-              transparent: true,
-              format: 'image/png',
-            }
-            this.set('cesiumOptions', assetConfig.cesiumOptions);
-            if (!assetConfig.moreInfoLink) {
-              this.set('moreInfoLink', 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer')
-            }
-            if (!assetConfig.attribution) {
-              this.set('attribution', 'USGS The National Map: Orthoimagery and US Topo. Data refreshed January, 2022.')
-            }
-            if (!assetConfig.description) {
-              this.set('description', 'USGS Imagery Topo is a tile cache base map of orthoimagery in The National Map and US Topo vectors visible to the 1:9,028 scale.')
-            }
-            if (!assetConfig.label) {
-              this.set('label', 'USGS Imagery Topo')
-            }
+          if (!assetConfig.label) {
+            this.set("label", "USGS Imagery Topo");
           }
-          catch (error) {
+        } catch (error) {
+          console.log(
+            "There was an error initializing USGSImageryTopo in a CesiumImagery" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Creates a Cesium ImageryLayer that contains information about how the imagery
+       * should render in Cesium. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/ImageryLayer.html?classFilter=ImageryLay}
+       * @param {Boolean} recreate - Set recreate to true to force the function create
+       * the Cesium Model again. Otherwise, if a cesium model already exists, that is
+       * returned instead.
+       */
+      createCesiumModel: function (recreate = false) {
+        var model = this;
+        const cesiumOptions = this.getCesiumOptions();
+        var type = this.get("type");
+        var providerFunction = Cesium[type];
+
+        // If the cesium model already exists, don't create it again unless specified
+        if (!recreate && this.get("cesiumModel")) {
+          console.log("returning existing cesium model");
+          return this.get("cesiumModel");
+        }
+
+        model.resetStatus();
+
+        var initialAppearance = {
+          alpha: this.get("opacity"),
+          show: this.get("visible"),
+          saturation: this.get("saturation"),
+          // TODO: brightness, contrast, gamma, etc.
+        };
+
+        if (type === "BingMapsImageryProvider") {
+          cesiumOptions.key =
+            cesiumOptions.key || MetacatUI.AppConfig.bingMapsKey;
+        } else if (type === "IonImageryProvider") {
+          cesiumOptions.assetId = Number(cesiumOptions.ionAssetId);
+          delete cesiumOptions.ionAssetId;
+          cesiumOptions.accessToken =
+            cesiumOptions.cesiumToken || MetacatUI.appModel.get("cesiumToken");
+        } else if (type === "OpenStreetMapImageryProvider") {
+          cesiumOptions.url =
+            cesiumOptions.url || "https://a.tile.openstreetmap.org/";
+        }
+        if (cesiumOptions && cesiumOptions.tilingScheme) {
+          const ts = cesiumOptions.tilingScheme;
+          const availableTS = [
+            "GeographicTilingScheme",
+            "WebMercatorTilingScheme",
+          ];
+          if (availableTS.indexOf(ts) > -1) {
+            cesiumOptions.tilingScheme = new Cesium[ts]();
+          } else {
             console.log(
-              'There was an error initializing USGSImageryTopo in a CesiumImagery' +
-              '. Error details: ' + error
+              `${ts} is not a valid tiling scheme. Using WebMercatorTilingScheme`,
             );
+            cesiumOptions.tilingScheme = new Cesium.WebMercatorTilingScheme();
           }
-        },
-
-        /**
-         * Creates a Cesium ImageryLayer that contains information about how the imagery
-         * should render in Cesium. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/ImageryLayer.html?classFilter=ImageryLay}
-         * @param {Boolean} recreate - Set recreate to true to force the function create
-         * the Cesium Model again. Otherwise, if a cesium model already exists, that is
-         * returned instead.
-         */
-        createCesiumModel: function (recreate = false) {
-
-          var model = this;
-          const cesiumOptions = this.getCesiumOptions();
-          var type = this.get('type')
-          var providerFunction = Cesium[type]
-
-          // If the cesium model already exists, don't create it again unless specified
-          if (!recreate && this.get('cesiumModel')) {
-            console.log('returning existing cesium model');
-            return this.get('cesiumModel')
-          }
-
-          model.resetStatus();
+        }
 
-          var initialAppearance = {
-            alpha: this.get('opacity'),
-            show: this.get('visible'),
-            saturation: this.get('saturation'),
-            // TODO: brightness, contrast, gamma, etc.
-          }
-
-          if (type === 'BingMapsImageryProvider') {
-            cesiumOptions.key = cesiumOptions.key || MetacatUI.AppConfig.bingMapsKey
-          } else if (type === 'IonImageryProvider') {
-            cesiumOptions.assetId = Number(cesiumOptions.ionAssetId)
-            delete cesiumOptions.ionAssetId
-            cesiumOptions.accessToken =
-              cesiumOptions.cesiumToken || MetacatUI.appModel.get('cesiumToken');
-          } else if (type === 'OpenStreetMapImageryProvider') {
-            cesiumOptions.url = cesiumOptions.url || 'https://a.tile.openstreetmap.org/'
-          }
-          if (cesiumOptions && cesiumOptions.tilingScheme) {
-            const ts = cesiumOptions.tilingScheme
-            const availableTS = ['GeographicTilingScheme', 'WebMercatorTilingScheme']
-            if (availableTS.indexOf(ts) > -1) {
-              cesiumOptions.tilingScheme = new Cesium[ts]()
-            } else {
-              console.log(`${ts} is not a valid tiling scheme. Using WebMercatorTilingScheme`)
-              cesiumOptions.tilingScheme = new Cesium.WebMercatorTilingScheme()
-            }
-          }
-
-          if (cesiumOptions.rectangle) {
-            cesiumOptions.rectangle = Cesium.Rectangle.fromDegrees(
-              ...cesiumOptions.rectangle
-            )
-          }
-
-          if (providerFunction && typeof providerFunction === 'function') {
-            let provider = new providerFunction(cesiumOptions)
-            provider.readyPromise
-              .then(function () {
-                // Imagery must be converted from a Cesium Imagery Provider to a Cesium
-                // Imagery Layer. See
-                // https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#imagery-providers-vs-layers
-                model.set('cesiumModel', new Cesium.ImageryLayer(provider, initialAppearance))
-                model.set('status', 'ready')
-                model.setListeners()
-              })
-              .otherwise(function (error) {
-                // See https://cesium.com/learn/cesiumjs/ref-doc/RequestErrorEvent.html
-                let details = error;
-                // Write a helpful error message
-                switch (error.statusCode) {
-                  case 404:
-                    details = 'The resource was not found (error code 404).'
-                    break;
-                  case 500:
-                    details = 'There was a server error (error code 500).'
-                    break;
-                }
-                model.set('status', 'error');
-                model.set('statusDetails', details)
-              })
-          } else {
-            model.set('status', 'error')
-            model.set('statusDetails', type + ' is not a supported imagery type.')
+        if (cesiumOptions.rectangle) {
+          cesiumOptions.rectangle = Cesium.Rectangle.fromDegrees(
+            ...cesiumOptions.rectangle,
+          );
+        }
+
+        if (providerFunction && typeof providerFunction === "function") {
+          let provider = new providerFunction(cesiumOptions);
+          provider.readyPromise
+            .then(function () {
+              // Imagery must be converted from a Cesium Imagery Provider to a Cesium
+              // Imagery Layer. See
+              // https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#imagery-providers-vs-layers
+              model.set(
+                "cesiumModel",
+                new Cesium.ImageryLayer(provider, initialAppearance),
+              );
+              model.set("status", "ready");
+              model.setListeners();
+            })
+            .otherwise(function (error) {
+              // See https://cesium.com/learn/cesiumjs/ref-doc/RequestErrorEvent.html
+              let details = error;
+              // Write a helpful error message
+              switch (error.statusCode) {
+                case 404:
+                  details = "The resource was not found (error code 404).";
+                  break;
+                case 500:
+                  details = "There was a server error (error code 500).";
+                  break;
+              }
+              model.set("status", "error");
+              model.set("statusDetails", details);
+            });
+        } else {
+          model.set("status", "error");
+          model.set(
+            "statusDetails",
+            type + " is not a supported imagery type.",
+          );
+        }
+      },
+
+      /**
+       * Set listeners that update the cesium model when the backbone model is updated.
+       */
+      setListeners: function () {
+        try {
+          var cesiumModel = this.get("cesiumModel");
+
+          // Make sure the listeners are only set once!
+          this.stopListening(this);
+
+          this.listenTo(this, "change:opacity", function (model, opacity) {
+            cesiumModel.alpha = opacity;
+            // Let the map and/or other parent views know that a change has been made
+            // that requires the map to be re-rendered
+            model.trigger("appearanceChanged");
+          });
+          this.listenTo(this, "change:visible", function (model, visible) {
+            cesiumModel.show = visible;
+            // Let the map and/or other parent views know that a change has been made
+            // that requires the map to be re-rendered
+            model.trigger("appearanceChanged");
+          });
+        } catch (error) {
+          console.log(
+            "There was an error setting listeners in a cesium Imagery model" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Gets a Cesium Bounding Sphere that can be used to navigate to view the full
+       * extent of the imagery. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/BoundingSphere.html}
+       * @returns {Promise} Returns a promise that resolves to a Cesium Bounding Sphere
+       * when ready
+       */
+      getBoundingSphere: function () {
+        return this.whenReady()
+          .then(function (model) {
+            return model.get("cesiumModel").getViewableRectangle();
+          })
+          .then(function (rectangle) {
+            return Cesium.BoundingSphere.fromRectangle3D(rectangle);
+          });
+      },
+
+      /**
+       * Requests a tile from the imagery provider that is at the center of the layer's
+       * bounding box and at the minimum level. Once the image is fetched, sets its URL
+       * on the thumbnail property of this model. This function is first called when the
+       * layer initialized, but waits for the cesiumModel to be ready.
+       */
+      getThumbnail: function () {
+        try {
+          if (this.get("status") !== "ready") {
+            this.listenToOnce(this, "change:status", this.getThumbnail);
+            return;
           }
 
-        },
-
-        /**
-         * Set listeners that update the cesium model when the backbone model is updated.
-         */
-        setListeners: function () {
-          try {
-
-            var cesiumModel = this.get('cesiumModel')
+          const model = this;
+          const cesImageryLayer = this.get("cesiumModel");
+          const provider = cesImageryLayer.imageryProvider;
+          const rect = cesImageryLayer.rectangle;
+          var x = (rect.east + rect.west) / 2;
+          var y = (rect.north + rect.south) / 2;
+          var level = provider.minimumLevel;
 
-            // Make sure the listeners are only set once!
-            this.stopListening(this);
-
-            this.listenTo(this, 'change:opacity', function (model, opacity) {
-              cesiumModel.alpha = opacity
-              // Let the map and/or other parent views know that a change has been made
-              // that requires the map to be re-rendered
-              model.trigger('appearanceChanged')
-
-            })
-            this.listenTo(this, 'change:visible', function (model, visible) {
-              cesiumModel.show = visible
-              // Let the map and/or other parent views know that a change has been made
-              // that requires the map to be re-rendered
-              model.trigger('appearanceChanged')
-            })
-          }
-          catch (error) {
-            console.log(
-              'There was an error setting listeners in a cesium Imagery model' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Gets a Cesium Bounding Sphere that can be used to navigate to view the full
-         * extent of the imagery. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/BoundingSphere.html}
-         * @returns {Promise} Returns a promise that resolves to a Cesium Bounding Sphere
-         * when ready
-         */
-        getBoundingSphere: function () {
-          return this.whenReady()
-            .then(function (model) {
-              return model.get('cesiumModel').getViewableRectangle()
-            })
-            .then(function (rectangle) {
-              return Cesium.BoundingSphere.fromRectangle3D(rectangle)
-            })
-        },
-
-        /**
-         * Requests a tile from the imagery provider that is at the center of the layer's
-         * bounding box and at the minimum level. Once the image is fetched, sets its URL
-         * on the thumbnail property of this model. This function is first called when the
-         * layer initialized, but waits for the cesiumModel to be ready.
-         */
-        getThumbnail: function () {
-          try {
-            if (this.get('status') !== 'ready') {
-              this.listenToOnce(this, 'change:status', this.getThumbnail)
-              return
-            }
-
-            const model = this
-            const cesImageryLayer = this.get('cesiumModel');
-            const provider = cesImageryLayer.imageryProvider
-            const rect = cesImageryLayer.rectangle
-            var x = (rect.east + rect.west) / 2
-            var y = (rect.north + rect.south) / 2
-            var level = provider.minimumLevel
-
-            provider.requestImage(x, y, level).then(function (response) {
-
-              let data = response.blob
-              let objectURL = null
+          provider
+            .requestImage(x, y, level)
+            .then(function (response) {
+              let data = response.blob;
+              let objectURL = null;
 
               if (!data && response instanceof ImageBitmap) {
-                objectURL = model.getDataUriFromBitmap(response)
+                objectURL = model.getDataUriFromBitmap(response);
               } else {
                 objectURL = URL.createObjectURL(data);
               }
 
-              model.set('thumbnail', objectURL)
-            }).otherwise(function (e) {
-              console.log('Error requesting an image tile to use as a thumbnail for an ' +
-                'Imagery Layer. Error message: ' + e);
+              model.set("thumbnail", objectURL);
             })
-          }
-          catch (error) {
-            console.log(
-              'There was an error getting a thumbnail for a CesiumImagery layer' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Gets a data URI from a bitmap image.
-         * @param {ImageBitmap} bitmap The bitmap image to convert to a data URI
-         * @returns {String} Returns a string containing the requested data URI.
-        */
-        getDataUriFromBitmap: function (imageBitmap) {
-          try {
-            const canvas = document.createElement('canvas');
-            canvas.width = imageBitmap.width;
-            canvas.height = imageBitmap.height;
-            const ctx = canvas.getContext('2d')
-            // y-flip the image - Natural Earth II bitmaps appear upside down otherwise
-            // TODO: Test with other imagery layers
-            ctx.translate(0, imageBitmap.height);
-            ctx.scale(1, -1);
-            ctx.drawImage(imageBitmap, 0, 0);
-            return canvas.toDataURL();
-          } catch (error) {
-            console.log(
-              'There was an error converting an ImageBitmap to a data URL' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        // /**
-        //  * Parses the given input into a JSON object to be set on the model.
-        //  *
-        //  * @param {TODO} input - The raw response object
-        //  * @return {TODO} - The JSON object of all the Imagery attributes
-        //    */
-        // parse: function (input) {
-
-        //   try {
-
-        //     var modelJSON = {};
-
-        //     return modelJSON
-
-        //   }
-        //   catch (error) {console.log('There was an error parsing a Imagery model' + '.
-        //     Error details: ' + error
-        //     );
-        //   }
-
-        // },
-
-        // /**
-        //  * Overrides the default Backbone.Model.validate.function() to check if this if
-        //  * the values set on this model are valid.
-        //  * 
-        //  * @param {Object} [attrs] - A literal object of model attributes to validate.
-        //  * @param {Object} [options] - A literal object of options for this validation
-        //  * process
-        //  * 
-        //  * @return {Object} - Returns a literal object with the invalid attributes and
-        //  * their corresponding error message, if there are any. If there are no errors,
-        //  * returns nothing.
-        //    */
-        // validate: function (attrs, options) {try {
-
-        //   }
-        //   catch (error) {console.log('There was an error validating a CesiumImagery
-        //     model' + '. Error details: ' + error
-        //     );
-        //   }
-        // },
-
-        // /**
-        //  * Creates a string using the values set on this model's attributes.
-        //  * @return {string} The Imagery string
-        //    */
-        // serialize: function () {try {var serializedImagery = "";
-
-        //     return serializedImagery;
-        //   }
-        //   catch (error) {console.log('There was an error serializing a CesiumImagery
-        //     model' + '. Error details: ' + error
-        //     );
-        //   }
-        // },
-
-      });
-
-    return CesiumImagery;
-
-  }
-);
+            .otherwise(function (e) {
+              console.log(
+                "Error requesting an image tile to use as a thumbnail for an " +
+                  "Imagery Layer. Error message: " +
+                  e,
+              );
+            });
+        } catch (error) {
+          console.log(
+            "There was an error getting a thumbnail for a CesiumImagery layer" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Gets a data URI from a bitmap image.
+       * @param {ImageBitmap} bitmap The bitmap image to convert to a data URI
+       * @returns {String} Returns a string containing the requested data URI.
+       */
+      getDataUriFromBitmap: function (imageBitmap) {
+        try {
+          const canvas = document.createElement("canvas");
+          canvas.width = imageBitmap.width;
+          canvas.height = imageBitmap.height;
+          const ctx = canvas.getContext("2d");
+          // y-flip the image - Natural Earth II bitmaps appear upside down otherwise
+          // TODO: Test with other imagery layers
+          ctx.translate(0, imageBitmap.height);
+          ctx.scale(1, -1);
+          ctx.drawImage(imageBitmap, 0, 0);
+          return canvas.toDataURL();
+        } catch (error) {
+          console.log(
+            "There was an error converting an ImageBitmap to a data URL" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      // /**
+      //  * Parses the given input into a JSON object to be set on the model.
+      //  *
+      //  * @param {TODO} input - The raw response object
+      //  * @return {TODO} - The JSON object of all the Imagery attributes
+      //    */
+      // parse: function (input) {
+
+      //   try {
+
+      //     var modelJSON = {};
+
+      //     return modelJSON
+
+      //   }
+      //   catch (error) {console.log('There was an error parsing a Imagery model' + '.
+      //     Error details: ' + error
+      //     );
+      //   }
+
+      // },
+
+      // /**
+      //  * Overrides the default Backbone.Model.validate.function() to check if this if
+      //  * the values set on this model are valid.
+      //  *
+      //  * @param {Object} [attrs] - A literal object of model attributes to validate.
+      //  * @param {Object} [options] - A literal object of options for this validation
+      //  * process
+      //  *
+      //  * @return {Object} - Returns a literal object with the invalid attributes and
+      //  * their corresponding error message, if there are any. If there are no errors,
+      //  * returns nothing.
+      //    */
+      // validate: function (attrs, options) {try {
+
+      //   }
+      //   catch (error) {console.log('There was an error validating a CesiumImagery
+      //     model' + '. Error details: ' + error
+      //     );
+      //   }
+      // },
+
+      // /**
+      //  * Creates a string using the values set on this model's attributes.
+      //  * @return {string} The Imagery string
+      //    */
+      // serialize: function () {try {var serializedImagery = "";
+
+      //     return serializedImagery;
+      //   }
+      //   catch (error) {console.log('There was an error serializing a CesiumImagery
+      //     model' + '. Error details: ' + error
+      //     );
+      //   }
+      // },
+    },
+  );
+
+  return CesiumImagery;
+});
 
diff --git a/docs/docs/src_js_models_maps_assets_CesiumTerrain.js.html b/docs/docs/src_js_models_maps_assets_CesiumTerrain.js.html index d6922df3a..99d50c720 100644 --- a/docs/docs/src_js_models_maps_assets_CesiumTerrain.js.html +++ b/docs/docs/src_js_models_maps_assets_CesiumTerrain.js.html @@ -44,246 +44,235 @@

Source: src/js/models/maps/assets/CesiumTerrain.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'cesium',
-    'models/maps/assets/MapAsset'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    Cesium,
-    MapAsset
-  ) {
-    /**
-     * @classdesc A CesiumTerrain Model comprises the information required to fetch 3D
-     * terrain, such as mountain peaks and valleys, to display in Cesium. A terrain model
-     * also contains metadata about the terrain source data, such as an attribution and a
-     * description.
-     * @classcategory Models/Maps/Assets
-     * @class CesiumTerrain
-     * @name CesiumTerrain
-     * @extends MapAsset
-     * @since 2.18.0
-     * @constructor
-    */
-    var CesiumTerrain = MapAsset.extend(
-      /** @lends CesiumTerrain.prototype */ {
-
-        /**
-         * The name of this type of model
-         * @type {string}
-        */
-        type: 'CesiumTerrain',
-
-        /**
-         * Options that are supported for creating terrain in Cesium. Any properties
-         * provided here are passed to the Cesium constructor function for the Terrain
-         * Provider, so other properties that are documented in Cesium are also supported.
-         * See `options` here:
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/CesiumTerrainProvider.html?classFilter=TerrainProvider}
-         * @typedef {Object} CesiumTerrain#cesiumOptions
-         * @property {string|number} ionAssetId - If this terrain is hosted by Cesium Ion,
-         * then Ion asset ID. 
-         */
-
-        /**
-         * Default attributes for CesiumTerrain models
-         * @name CesiumTerrain#defaults
-         * @extends MapAsset#defaults
-         * @type {Object}
-         * @property {'CesiumTerrainProvider'} type A string indicating a Cesium Terrain
-         * Provider, see
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/?classFilter=TerrainProvider}
-         * @property {Cesium.TerrainProvider} cesiumModel A model created and used by
-         * Cesium that organizes the data to display in the Cesium Widget. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/TerrainProvider.html}
-         * @property {CesiumTerrain#cesiumOptions} cesiumOptions options are passed to the
-         * function that creates the Cesium model. The properties of options are specific
-         * to each type of asset
-        */
-        defaults: function () {
-          return _.extend(
-            this.constructor.__super__.defaults(),
-            {
-              type: 'CesiumTerrainProvider',
-              cesiumModel: null,
-              cesiumOptions: {},
-            }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "cesium",
+  "models/maps/assets/MapAsset",
+], function ($, _, Backbone, Cesium, MapAsset) {
+  /**
+   * @classdesc A CesiumTerrain Model comprises the information required to fetch 3D
+   * terrain, such as mountain peaks and valleys, to display in Cesium. A terrain model
+   * also contains metadata about the terrain source data, such as an attribution and a
+   * description.
+   * @classcategory Models/Maps/Assets
+   * @class CesiumTerrain
+   * @name CesiumTerrain
+   * @extends MapAsset
+   * @since 2.18.0
+   * @constructor
+   */
+  var CesiumTerrain = MapAsset.extend(
+    /** @lends CesiumTerrain.prototype */ {
+      /**
+       * The name of this type of model
+       * @type {string}
+       */
+      type: "CesiumTerrain",
+
+      /**
+       * Options that are supported for creating terrain in Cesium. Any properties
+       * provided here are passed to the Cesium constructor function for the Terrain
+       * Provider, so other properties that are documented in Cesium are also supported.
+       * See `options` here:
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/CesiumTerrainProvider.html?classFilter=TerrainProvider}
+       * @typedef {Object} CesiumTerrain#cesiumOptions
+       * @property {string|number} ionAssetId - If this terrain is hosted by Cesium Ion,
+       * then Ion asset ID.
+       */
+
+      /**
+       * Default attributes for CesiumTerrain models
+       * @name CesiumTerrain#defaults
+       * @extends MapAsset#defaults
+       * @type {Object}
+       * @property {'CesiumTerrainProvider'} type A string indicating a Cesium Terrain
+       * Provider, see
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/?classFilter=TerrainProvider}
+       * @property {Cesium.TerrainProvider} cesiumModel A model created and used by
+       * Cesium that organizes the data to display in the Cesium Widget. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/TerrainProvider.html}
+       * @property {CesiumTerrain#cesiumOptions} cesiumOptions options are passed to the
+       * function that creates the Cesium model. The properties of options are specific
+       * to each type of asset
+       */
+      defaults: function () {
+        return _.extend(this.constructor.__super__.defaults(), {
+          type: "CesiumTerrainProvider",
+          cesiumModel: null,
+          cesiumOptions: {},
+        });
+      },
+
+      /**
+       * Executed when a new CesiumTerrain model is created.
+       * @param {Object} [attributes] The initial values of the attributes, which will
+       * be set on the model.
+       * @param {Object} [options] Options for the initialize function.
+       */
+      initialize: function (attributes, options) {
+        try {
+          MapAsset.prototype.initialize.call(this, attributes, options);
+
+          this.createCesiumModel();
+        } catch (error) {
+          console.log(
+            "There was an error initializing a CesiumTerrain model" +
+              ". Error details: " +
+              error,
           );
-        },
-
-        /**
-         * Executed when a new CesiumTerrain model is created.
-         * @param {Object} [attributes] The initial values of the attributes, which will
-         * be set on the model.
-         * @param {Object} [options] Options for the initialize function.
-         */
-        initialize: function (attributes, options) {
-          try {
-            MapAsset.prototype.initialize.call(this, attributes, options);
-
-            this.createCesiumModel();
-          }
-          catch (error) {
-            console.log(
-              'There was an error initializing a CesiumTerrain model' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Creates a Cesium TerrainProvider that contains information about where the
-         * terrain data should be requested from and how to render it in Cesium. See
-         * {@link https://cesium.com/learn/cesiumjs/ref-doc/TerrainProvider.html?classFilter=terrain}
-         * @param {Boolean} recreate - Set recreate to true to force the function create
-         * the Cesium Model again. Otherwise, if a cesium model already exists, that is
-         * returned instead.
-         */
-        createCesiumModel: function (recreate = false) {
-
-          var model = this;
-          var cesiumOptions = model.getCesiumOptions();
-          var type = this.get('type')
-          var terrainFunction = Cesium[type]
-
-          // If the cesium model already exists, don't create it again unless specified
-          if (!recreate && this.get('cesiumModel')) {
-            return this.get('cesiumModel')
-          }
-
-          model.resetStatus();
-
-          // If this tileset is a Cesium Ion resource, set the url from the
-          // asset Id
-          cesiumOptions.url = this.getCesiumURL(cesiumOptions) || cesiumOptions.url;
-
-          if (terrainFunction && typeof terrainFunction === 'function') {
-            let terrain = new terrainFunction(cesiumOptions)
-            terrain.readyPromise
-              .then(function () {
-                model.set('cesiumModel', terrain)
-                model.set('status', 'ready')
-              })
-              .otherwise(function (error) {
-                model.set('status', 'error');
-                model.set('statusDetails', error)
-              })
-          } else {
-            model.set('status', 'error')
-            model.set('statusDetails', type + ' is not a supported imagery type.')
-          }
-
-        },
-
-        /**
-         * Checks whether there is an asset ID for a Cesium Ion resource and if
-         * so, return the URL to the resource.
-         * @returns {string} The URL to the Cesium Ion resource
-         * @since 2.26.0
-         */
-        getCesiumURL: function () {
-          try {
-            const cesiumOptions = this.getCesiumOptions();
-            if (!cesiumOptions || !cesiumOptions.ionAssetId) return null
-            // The Cesium Ion ID of the resource to access
-            const assetId = Number(cesiumOptions.ionAssetId)
-            // Options to pass to Cesium's fromAssetId function. Access token
-            // needs to be set before requesting cesium ion resources
-            const ionResourceOptions = {
-              accessToken: cesiumOptions.cesiumToken ||
-                MetacatUI.appModel.get('cesiumToken')
-            }
-            // Create the new URL and set it on the model options
-            return Cesium.IonResource.fromAssetId(assetId, ionResourceOptions);
-          }
-          catch (error) {
-            console.log(
-              'There was an error settings a Cesium URL in a Cesium3DTileset' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        // /**
-        //  * Parses the given input into a JSON object to be set on the model.
-        //  *
-        //  * @param {TODO} input - The raw response object
-        //  * @return {TODO} - The JSON object of all the CesiumTerrain attributes
-        //  */
-        // parse: function (input) {
-
-        //   try {
-
-        //     var modelJSON = {};
-
-        //     return modelJSON
-
-        //   }
-        //   catch (error) {
-        //     console.log(
-        //       'There was an error parsing a CesiumTerrain model' +
-        //       '. Error details: ' + error
-        //     );
-        //   }
-
-        // },
-
-        // /**
-        //  * Overrides the default Backbone.Model.validate.function() to check if this if
-        //  * the values set on this model are valid.
-        //  * 
-        //  * @param {Object} [attrs] - A literal object of model attributes to validate.
-        //  * @param {Object} [options] - A literal object of options for this validation
-        //  * process
-        //  * 
-        //  * @return {Object} - Returns a literal object with the invalid attributes and
-        //  * their corresponding error message, if there are any. If there are no errors,
-        //  * returns nothing.
-        //  */
-        // validate: function (attrs, options) {
-        //   try {
-
-        //   }
-        //   catch (error) {
-        //     console.log(
-        //       'There was an error validating a CesiumTerrain model' +
-        //       '. Error details: ' + error
-        //     );
-        //   }
-        // },
-
-        // /**
-        //  * Creates a string using the values set on this model's attributes.
-        //  * @return {string} The CesiumTerrain string
-        //  */
-        // serialize: function () {
-        //   try {
-        //     var serializedTerrain = '';
-
-        //     return serializedTerrain;
-        //   }
-        //   catch (error) {
-        //     console.log(
-        //       'There was an error serializing a CesiumTerrain model' +
-        //       '. Error details: ' + error
-        //     );
-        //   }
-        // },
-
-      });
-
-    return CesiumTerrain;
-
-  }
-);
+        }
+      },
+
+      /**
+       * Creates a Cesium TerrainProvider that contains information about where the
+       * terrain data should be requested from and how to render it in Cesium. See
+       * {@link https://cesium.com/learn/cesiumjs/ref-doc/TerrainProvider.html?classFilter=terrain}
+       * @param {Boolean} recreate - Set recreate to true to force the function create
+       * the Cesium Model again. Otherwise, if a cesium model already exists, that is
+       * returned instead.
+       */
+      createCesiumModel: function (recreate = false) {
+        var model = this;
+        var cesiumOptions = model.getCesiumOptions();
+        var type = this.get("type");
+        var terrainFunction = Cesium[type];
+
+        // If the cesium model already exists, don't create it again unless specified
+        if (!recreate && this.get("cesiumModel")) {
+          return this.get("cesiumModel");
+        }
+
+        model.resetStatus();
+
+        // If this tileset is a Cesium Ion resource, set the url from the
+        // asset Id
+        cesiumOptions.url =
+          this.getCesiumURL(cesiumOptions) || cesiumOptions.url;
+
+        if (terrainFunction && typeof terrainFunction === "function") {
+          let terrain = new terrainFunction(cesiumOptions);
+          terrain.readyPromise
+            .then(function () {
+              model.set("cesiumModel", terrain);
+              model.set("status", "ready");
+            })
+            .otherwise(function (error) {
+              model.set("status", "error");
+              model.set("statusDetails", error);
+            });
+        } else {
+          model.set("status", "error");
+          model.set(
+            "statusDetails",
+            type + " is not a supported imagery type.",
+          );
+        }
+      },
+
+      /**
+       * Checks whether there is an asset ID for a Cesium Ion resource and if
+       * so, return the URL to the resource.
+       * @returns {string} The URL to the Cesium Ion resource
+       * @since 2.26.0
+       */
+      getCesiumURL: function () {
+        try {
+          const cesiumOptions = this.getCesiumOptions();
+          if (!cesiumOptions || !cesiumOptions.ionAssetId) return null;
+          // The Cesium Ion ID of the resource to access
+          const assetId = Number(cesiumOptions.ionAssetId);
+          // Options to pass to Cesium's fromAssetId function. Access token
+          // needs to be set before requesting cesium ion resources
+          const ionResourceOptions = {
+            accessToken:
+              cesiumOptions.cesiumToken ||
+              MetacatUI.appModel.get("cesiumToken"),
+          };
+          // Create the new URL and set it on the model options
+          return Cesium.IonResource.fromAssetId(assetId, ionResourceOptions);
+        } catch (error) {
+          console.log(
+            "There was an error settings a Cesium URL in a Cesium3DTileset" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      // /**
+      //  * Parses the given input into a JSON object to be set on the model.
+      //  *
+      //  * @param {TODO} input - The raw response object
+      //  * @return {TODO} - The JSON object of all the CesiumTerrain attributes
+      //  */
+      // parse: function (input) {
+
+      //   try {
+
+      //     var modelJSON = {};
+
+      //     return modelJSON
+
+      //   }
+      //   catch (error) {
+      //     console.log(
+      //       'There was an error parsing a CesiumTerrain model' +
+      //       '. Error details: ' + error
+      //     );
+      //   }
+
+      // },
+
+      // /**
+      //  * Overrides the default Backbone.Model.validate.function() to check if this if
+      //  * the values set on this model are valid.
+      //  *
+      //  * @param {Object} [attrs] - A literal object of model attributes to validate.
+      //  * @param {Object} [options] - A literal object of options for this validation
+      //  * process
+      //  *
+      //  * @return {Object} - Returns a literal object with the invalid attributes and
+      //  * their corresponding error message, if there are any. If there are no errors,
+      //  * returns nothing.
+      //  */
+      // validate: function (attrs, options) {
+      //   try {
+
+      //   }
+      //   catch (error) {
+      //     console.log(
+      //       'There was an error validating a CesiumTerrain model' +
+      //       '. Error details: ' + error
+      //     );
+      //   }
+      // },
+
+      // /**
+      //  * Creates a string using the values set on this model's attributes.
+      //  * @return {string} The CesiumTerrain string
+      //  */
+      // serialize: function () {
+      //   try {
+      //     var serializedTerrain = '';
+
+      //     return serializedTerrain;
+      //   }
+      //   catch (error) {
+      //     console.log(
+      //       'There was an error serializing a CesiumTerrain model' +
+      //       '. Error details: ' + error
+      //     );
+      //   }
+      // },
+    },
+  );
+
+  return CesiumTerrain;
+});
 
diff --git a/docs/docs/src_js_models_maps_assets_CesiumVectorData.js.html b/docs/docs/src_js_models_maps_assets_CesiumVectorData.js.html index 933931301..82c3db6f3 100644 --- a/docs/docs/src_js_models_maps_assets_CesiumVectorData.js.html +++ b/docs/docs/src_js_models_maps_assets_CesiumVectorData.js.html @@ -61,13 +61,18 @@

Source: src/js/models/maps/assets/CesiumVectorData.js

<!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/></svg>'; const PIN_OUTLINE_WIDTH = 30; // The width of the stroke around the pin is relative to the viewBox const PIN_OUTLINE_COLOR = "white"; - const PIN_SVG = IconUtilities.formatSvgForCesiumBillboard(PIN_SVG_STRING, PIN_OUTLINE_WIDTH, PIN_OUTLINE_COLOR); + const PIN_SVG = IconUtilities.formatSvgForCesiumBillboard( + PIN_SVG_STRING, + PIN_OUTLINE_WIDTH, + PIN_OUTLINE_COLOR, + ); /** * @classdesc A CesiumVectorData Model is a vector layer (excluding @@ -168,7 +173,7 @@

Source: src/js/models/maps/assets/CesiumVectorData.js

Source: src/js/models/maps/assets/CesiumVectorData.js

Source: src/js/models/maps/assets/CesiumVectorData.js

Source: src/js/models/maps/assets/CesiumVectorData.js

Source: src/js/models/maps/assets/CesiumVectorData.jsSource: src/js/models/maps/assets/CesiumVectorData.jsSource: src/js/models/maps/assets/CesiumVectorData.jsSource: src/js/models/maps/assets/CesiumVectorData.jsSource: src/js/models/maps/assets/CesiumVectorData.jsSource: src/js/models/maps/assets/CesiumVectorData.jsSource: src/js/models/maps/assets/MapAsset.js "models/maps/AssetColorPalette", "common/IconUtilities", MetacatUI.root + "/components/dayjs.min.js", -], function (_, Backbone, PortalImage, AssetColorPalette, IconUtilities, dayjs) { +], function ( + _, + Backbone, + PortalImage, + AssetColorPalette, + IconUtilities, + dayjs, +) { /** * @classdesc A MapAsset Model comprises information required to fetch source data for * some asset or resource that is displayed in a map, such as imagery (raster) tiles, @@ -371,7 +378,7 @@

Source: src/js/models/maps/assets/MapAsset.js

if (assetConfig.colorPalette) { this.set( "colorPalette", - new AssetColorPalette(assetConfig.colorPalette) + new AssetColorPalette(assetConfig.colorPalette), ); } @@ -384,14 +391,15 @@

Source: src/js/models/maps/assets/MapAsset.js

model.set("iconStatus", "fetching"); // If the string is not an SVG then assume it is a PID and try to fetch // the SVG file. - IconUtilities.fetchIcon(assetConfig.icon) - .then(icon => model.updateIcon(icon)); + IconUtilities.fetchIcon(assetConfig.icon).then((icon) => + model.updateIcon(icon), + ); } } catch (error) { console.log( "Failed to fetch an icon for a MapAsset" + ". Error details: " + - error + error, ); model.set("iconStatus", "error"); } @@ -416,14 +424,14 @@

Source: src/js/models/maps/assets/MapAsset.js

// Write a helpful error message switch (error.statusCode) { case 404: - details = 'The resource was not found (error code 404).' + details = "The resource was not found (error code 404)."; break; case 500: - details = 'There was a server error (error code 500).' + details = "There was a server error (error code 500)."; break; } - this.set('status', 'error'); - this.set('statusDetails', details) + this.set("status", "error"); + this.set("statusDetails", details); }, /** @@ -431,8 +439,8 @@

Source: src/js/models/maps/assets/MapAsset.js

* @since 2.27.0 */ setReady: function () { - this.set('status', 'ready') - this.set('statusDetails', null) + this.set("status", "ready"); + this.set("statusDetails", null); }, /** @@ -455,8 +463,8 @@

Source: src/js/models/maps/assets/MapAsset.js

this.handleError(); return; } else { - const vis = this.get("originalVisibility") - if(typeof vis === "boolean"){ + const vis = this.get("originalVisibility"); + if (typeof vis === "boolean") { this.set("visible", vis); } } @@ -492,7 +500,7 @@

Source: src/js/models/maps/assets/MapAsset.js

this.listenToOnce( this, "change:mapModel", - this.listenToSelectedFeatures + this.listenToSelectedFeatures, ); return; } @@ -503,14 +511,14 @@

Source: src/js/models/maps/assets/MapAsset.js

this.listenToOnce( mapModel, "change:interactions", - this.listenToSelectedFeatures + this.listenToSelectedFeatures, ); return; } const selectedFeatures = mapModel.getSelectedFeatures(); - if(selectedFeatures){ + if (selectedFeatures) { this.stopListening(selectedFeatures, "update"); this.listenTo(selectedFeatures, "update", this.updateAppearance); } @@ -580,7 +588,7 @@

Source: src/js/models/maps/assets/MapAsset.js

usesFeatureType: function (feature) { const ft = this.get("featureType"); if (!feature || !ft) return false; - if (!feature instanceof ft) return false; + if ((!feature) instanceof ft) return false; return true; }, @@ -668,7 +676,7 @@

Source: src/js/models/maps/assets/MapAsset.js

console.log( "There was an error adding custom properties. Returning properties " + "unchanged. Error details: " + - error + error, ); return properties; } @@ -703,7 +711,7 @@

Source: src/js/models/maps/assets/MapAsset.js

console.log( "There was an error formatting a date for a Feature model" + ". Error details: " + - error + error, ); return ""; } @@ -737,7 +745,7 @@

Source: src/js/models/maps/assets/MapAsset.js

console.log( "There was an error formatting a string for a Feature model" + ". Error details: " + - error + error, ); return ""; } @@ -896,7 +904,7 @@

Source: src/js/models/maps/assets/MapAsset.js

this.set("visible", true); } }, - } + }, ); return MapAsset; diff --git a/docs/docs/src_js_models_maps_viewfinder_ExpansionPanelsModel.js.html b/docs/docs/src_js_models_maps_viewfinder_ExpansionPanelsModel.js.html index 58b82f627..f99cdb73a 100644 --- a/docs/docs/src_js_models_maps_viewfinder_ExpansionPanelsModel.js.html +++ b/docs/docs/src_js_models_maps_viewfinder_ExpansionPanelsModel.js.html @@ -44,60 +44,62 @@

Source: src/js/models/maps/viewfinder/ExpansionPanelsMode
-
'use strict';
+            
"use strict";
 
 define([], () => {
   /**
-  * @class ExpansionPanelsModel
-  * @classdesc ExpansionPanelsModel maintains state for multiple
-  * ExpansionPanelView instances so that only one is open at a time.
-  * @classcategory Models/Maps
-  */
+   * @class ExpansionPanelsModel
+   * @classdesc ExpansionPanelsModel maintains state for multiple
+   * ExpansionPanelView instances so that only one is open at a time.
+   * @classcategory Models/Maps
+   */
   const ExpansionPanelsModel = Backbone.Model.extend(
-    /** @lends ExpansionPanelsModel.prototype */{
+    /** @lends ExpansionPanelsModel.prototype */ {
       /**
        * @name ExpansionPanelsModel#defaults
        * @type {Object}
        * @property {ExpansionPanelView[]} panels The expansion panel views that
        * are meant to have only a single panel open at a time.
        * @property {boolean} isMulti Whether multiple panels can be open at the
-       * same time when displayed in a group of panels. 
-       * @extends Backbone.Model 
+       * same time when displayed in a group of panels.
+       * @extends Backbone.Model
        */
       defaults() {
         return {
           panels: [],
           isMulti: false,
-        }
+        };
       },
 
       /**
        * Register a panel to coordinate collapse state.
-       * @property {ExpansionPanelView} panel The expansion panel view to be 
+       * @property {ExpansionPanelView} panel The expansion panel view to be
        * tracked.
        */
       register(panel) {
-        this.set('panels', [...this.get('panels'), panel]);
+        this.set("panels", [...this.get("panels"), panel]);
       },
 
       /**
        * Collapse all panels except for the newly opened panel for certain open
        * modes.
-       * @property {ExpansionPanelView} openedPanel The expansion panel view that 
+       * @property {ExpansionPanelView} openedPanel The expansion panel view that
        * should remain open.
        */
       maybeCollapseOthers(openedPanel) {
-        const isSingleOpenMode = !this.get('isMulti');
-        for (const panel of this.get('panels')) {
+        const isSingleOpenMode = !this.get("isMulti");
+        for (const panel of this.get("panels")) {
           if (isSingleOpenMode && panel !== openedPanel) {
             panel.collapse();
           }
         }
-      }
-    });
+      },
+    },
+  );
 
   return ExpansionPanelsModel;
-});
+}); +
diff --git a/docs/docs/src_js_models_maps_viewfinder_ViewfinderModel.js.html b/docs/docs/src_js_models_maps_viewfinder_ViewfinderModel.js.html index 538b30668..2f5bdb8a5 100644 --- a/docs/docs/src_js_models_maps_viewfinder_ViewfinderModel.js.html +++ b/docs/docs/src_js_models_maps_viewfinder_ViewfinderModel.js.html @@ -44,248 +44,264 @@

Source: src/js/models/maps/viewfinder/ViewfinderModel.js<
-
'use strict';
-
-define(
-  [
-    'underscore',
-    'backbone',
-    'cesium',
-    'models/geocoder/GeocoderSearch',
-    'models/maps/GeoPoint'
-  ],
-  (_, Backbone, Cesium, GeocoderSearch, GeoPoint) => {
-    const EMAIL = MetacatUI.appModel.get('emailContact');
-    const NO_RESULTS_MESSAGE = 'No search results found, try using another place name.';
-    const API_ERROR = 'We\'re having trouble identifying locations on the map right now. Please reach out to support for help with this issue' + (EMAIL ? `: ${EMAIL}` : '.');
-    const PLACES_API_ERROR = API_ERROR;
-    const GEOCODING_API_ERROR = API_ERROR;
-
-    /**
-    * @class ViewfinderModel
-    * @classdesc ViewfinderModel maintains state for the ViewfinderView and
-    * interfaces with location searching services.
-    * @classcategory Models/Maps
-    * @since 2.28.0
-    * @extends Backbone.Model
-    */
-    const ViewfinderModel = Backbone.Model.extend(
-      /** @lends ViewfinderModel.prototype */{
-        /**
-         * @name ViewfinderModel#defaults
-         * @type {Object}
-         * @property {string} error is the current error string to be displayed
-         * in the UI.
-         * @property {number} focusIndex is the index of the element
-         * in the list of predictions that shoudl be highlighted as focus.
-         * @property {Prediction[]} predictions a list of Predictions models that
-         * correspond to the user's search query.
-         * @property {string} query the user's search query.
-         * @since 2.28.0
-         */
-        defaults() {
-          return {
-            error: '',
-            focusIndex: -1,
-            predictions: [],
-            query: '',
-            zoomPresets: [],
+            
"use strict";
+
+define([
+  "underscore",
+  "backbone",
+  "cesium",
+  "models/geocoder/GeocoderSearch",
+  "models/maps/GeoPoint",
+], (_, Backbone, Cesium, GeocoderSearch, GeoPoint) => {
+  const EMAIL = MetacatUI.appModel.get("emailContact");
+  const NO_RESULTS_MESSAGE =
+    "No search results found, try using another place name.";
+  const API_ERROR =
+    "We're having trouble identifying locations on the map right now. Please reach out to support for help with this issue" +
+    (EMAIL ? `: ${EMAIL}` : ".");
+  const PLACES_API_ERROR = API_ERROR;
+  const GEOCODING_API_ERROR = API_ERROR;
+
+  /**
+   * @class ViewfinderModel
+   * @classdesc ViewfinderModel maintains state for the ViewfinderView and
+   * interfaces with location searching services.
+   * @classcategory Models/Maps
+   * @since 2.28.0
+   * @extends Backbone.Model
+   */
+  const ViewfinderModel = Backbone.Model.extend(
+    /** @lends ViewfinderModel.prototype */ {
+      /**
+       * @name ViewfinderModel#defaults
+       * @type {Object}
+       * @property {string} error is the current error string to be displayed
+       * in the UI.
+       * @property {number} focusIndex is the index of the element
+       * in the list of predictions that shoudl be highlighted as focus.
+       * @property {Prediction[]} predictions a list of Predictions models that
+       * correspond to the user's search query.
+       * @property {string} query the user's search query.
+       * @since 2.28.0
+       */
+      defaults() {
+        return {
+          error: "",
+          focusIndex: -1,
+          predictions: [],
+          query: "",
+          zoomPresets: [],
+        };
+      },
+
+      /**
+       * @param {Map} mapModel is the Map model that the ViewfinderModel is
+       * managing for the corresponding ViewfinderView.
+       */
+      initialize({ mapModel }) {
+        this.geocoderSearch = new GeocoderSearch();
+        this.mapModel = mapModel;
+        this.allLayers = this.mapModel.getAllLayers();
+
+        this.set(
+          "zoomPresets",
+          mapModel.get("zoomPresetsCollection")?.models || [],
+        );
+      },
+
+      /**
+       * Get autocompletion predictions from the GeocoderSearch model.
+       * @param {string} rawQuery is the user's search query with spaces.
+       */
+      async autocompleteSearch(rawQuery) {
+        const query = rawQuery.trim();
+        if (this.get("query") === query) {
+          return;
+        } else if (!query) {
+          this.set({ error: "", predictions: [], query: "", focusIndex: -1 });
+          return;
+        } else if (GeoPoint.couldBeLatLong(query)) {
+          this.set({ predictions: [], query: "", focusIndex: -1 });
+          return;
+        }
+
+        // Unset error so the error will fire a change event even if it is the
+        // same error as already exists.
+        this.unset("error", { silent: true });
+
+        try {
+          // User is looking for autocompletions.
+          const predictions = await this.geocoderSearch.autocomplete(query);
+          const error = predictions.length === 0 ? NO_RESULTS_MESSAGE : "";
+          this.set({ error, focusIndex: -1, predictions, query });
+        } catch (e) {
+          if (
+            e.code === "REQUEST_DENIED" &&
+            e.endpoint === "PLACES_AUTOCOMPLETE"
+          ) {
+            this.set({
+              error: PLACES_API_ERROR,
+              focusIndex: -1,
+              predictions: [],
+              query,
+            });
+          } else {
+            this.set({
+              error: NO_RESULTS_MESSAGE,
+              focusIndex: -1,
+              predictions: [],
+              query,
+            });
           }
-        },
-
-        /**
-         * @param {Map} mapModel is the Map model that the ViewfinderModel is
-         * managing for the corresponding ViewfinderView.
-         */
-        initialize({ mapModel }) {
-          this.geocoderSearch = new GeocoderSearch();
-          this.mapModel = mapModel;
-          this.allLayers = this.mapModel.getAllLayers();
-
-          this.set('zoomPresets', mapModel.get('zoomPresetsCollection')?.models || []);
-        },
-
-        /** 
-         * Get autocompletion predictions from the GeocoderSearch model. 
-         * @param {string} rawQuery is the user's search query with spaces.
-         */
-        async autocompleteSearch(rawQuery) {
-          const query = rawQuery.trim();
-          if (this.get('query') === query) {
+        }
+      },
+
+      /**
+       * Decrement the focused index with a minimum value of 0. This corresponds
+       * to an ArrowUp key down event.
+       * Note: An ArrowUp key press while the current index is -1 will
+       * result in highlighting the first element in the list.
+       */
+      decrementFocusIndex() {
+        const currentIndex = this.get("focusIndex");
+        this.set("focusIndex", Math.max(0, currentIndex - 1));
+      },
+
+      /**
+       * Increment the focused index with a maximum value of the last value in
+       * the list. This corresponds to an ArrowDown key down event.
+       */
+      incrementFocusIndex() {
+        const currentIndex = this.get("focusIndex");
+        this.set(
+          "focusIndex",
+          Math.min(currentIndex + 1, this.get("predictions").length - 1),
+        );
+      },
+
+      /**
+       * Reset the focused index back to the initial value so that no element
+       * in the UI is highlighted.
+       */
+      resetFocusIndex() {
+        this.set("focusIndex", -1);
+      },
+
+      /**
+       * Navigate to the GeocodedLocation.
+       * @param {GeocodedLocation} geocoding is the location that corresponds
+       * to the the selected prediction.
+       */
+      goToLocation(geocoding) {
+        if (!geocoding) return;
+
+        const coords = geocoding.get("box").getCoords();
+        this.mapModel.zoomTo({
+          destination: Cesium.Rectangle.fromDegrees(
+            coords.west,
+            coords.south,
+            coords.east,
+            coords.north,
+          ),
+        });
+      },
+
+      /**
+       * Select a ZoomPresetModel from the list of presets and navigate there.
+       * This function hides all layers that are not to be visible according to
+       * the ZoomPresetModel configuration.
+       * @param {ZoomPresetModel} preset A user selected preset for which to
+       * enable layers and navigate.
+       */
+      selectZoomPreset(preset) {
+        const enabledLayerIds = preset.get("enabledLayerIds");
+        for (const layer of this.allLayers) {
+          const isVisible = enabledLayerIds.includes(layer.get("layerId"));
+          // Show or hide the layer according to the preset.
+          layer.set("visible", isVisible);
+        }
+
+        this.mapModel.zoomTo(preset.get("geoPoint"));
+      },
+
+      /**
+       * Select a prediction from the list of predictions and navigate there.
+       * @param {Prediction} prediction is the user-selected Prediction that
+       * needs to be geocoded and navigated to.
+       */
+      async selectPrediction(prediction) {
+        if (!prediction) return;
+
+        try {
+          const geocodings = await this.geocoderSearch.geocode(prediction);
+
+          if (geocodings.length === 0) {
+            this.set("error", NO_RESULTS_MESSAGE);
             return;
-          } else if (!query) {
-            this.set({ error: '', predictions: [], query: '', focusIndex: -1, });
-            return;
-          } else if (GeoPoint.couldBeLatLong(query)) {
-            this.set({ predictions: [], query: '', focusIndex: -1, });
-            return;
-          }
-
-          // Unset error so the error will fire a change event even if it is the
-          // same error as already exists.
-          this.unset('error', { silent: true });
-
-          try {
-            // User is looking for autocompletions.
-            const predictions = await this.geocoderSearch.autocomplete(query);
-            const error = predictions.length === 0 ? NO_RESULTS_MESSAGE : '';
-            this.set({ error, focusIndex: -1, predictions, query, });
-          } catch (e) {
-            if (e.code === 'REQUEST_DENIED' && e.endpoint === 'PLACES_AUTOCOMPLETE') {
-              this.set({
-                error: PLACES_API_ERROR,
-                focusIndex: -1,
-                predictions: [],
-                query,
-              });
-            } else {
-              this.set({
-                error: NO_RESULTS_MESSAGE,
-                focusIndex: -1,
-                predictions: [],
-                query,
-              });
-            }
-          }
-        },
-
-        /**
-         * Decrement the focused index with a minimum value of 0. This corresponds
-         * to an ArrowUp key down event. 
-         * Note: An ArrowUp key press while the current index is -1 will
-         * result in highlighting the first element in the list.
-         */
-        decrementFocusIndex() {
-          const currentIndex = this.get('focusIndex');
-          this.set('focusIndex', Math.max(0, currentIndex - 1));
-        },
-
-        /**
-         * Increment the focused index with a maximum value of the last value in
-         * the list. This corresponds to an ArrowDown key down event. 
-         */
-        incrementFocusIndex() {
-          const currentIndex = this.get('focusIndex');
-          this.set(
-            'focusIndex',
-            Math.min(currentIndex + 1, this.get('predictions').length - 1)
-          );
-        },
-
-        /**
-         * Reset the focused index back to the initial value so that no element
-         * in the UI is highlighted.
-         */
-        resetFocusIndex() {
-          this.set('focusIndex', -1);
-        },
-
-        /** 
-         * Navigate to the GeocodedLocation. 
-         * @param {GeocodedLocation} geocoding is the location that corresponds
-         * to the the selected prediction.
-         */
-        goToLocation(geocoding) {
-          if (!geocoding) return;
-
-          const coords = geocoding.get('box').getCoords();
-          this.mapModel.zoomTo({
-            destination: Cesium.Rectangle.fromDegrees(
-              coords.west,
-              coords.south,
-              coords.east,
-              coords.north,
-            )
-          });
-        },
-
-        /**
-         * Select a ZoomPresetModel from the list of presets and navigate there.
-         * This function hides all layers that are not to be visible according to
-         * the ZoomPresetModel configuration.
-         * @param {ZoomPresetModel} preset A user selected preset for which to 
-         * enable layers and navigate.
-         */
-        selectZoomPreset(preset) {
-          const enabledLayerIds = preset.get('enabledLayerIds');
-          for (const layer of this.allLayers) {
-            const isVisible = enabledLayerIds.includes(layer.get('layerId'));
-            // Show or hide the layer according to the preset.
-            layer.set('visible', isVisible);
           }
 
-          this.mapModel.zoomTo(preset.get('geoPoint'));
-        },
-
-        /**
-         * Select a prediction from the list of predictions and navigate there.
-         * @param {Prediction} prediction is the user-selected Prediction that
-         * needs to be geocoded and navigated to.
-         */
-        async selectPrediction(prediction) {
-          if (!prediction) return;
-
-          try {
-            const geocodings = await this.geocoderSearch.geocode(prediction);
-
-            if (geocodings.length === 0) {
-              this.set('error', NO_RESULTS_MESSAGE)
-              return;
-            }
-
-            this.trigger('selection-made', prediction.get('description'));
-            this.goToLocation(geocodings[0]);
-          } catch (e) {
-            if (e.code === 'REQUEST_DENIED' && e.endpoint === 'GEOCODER_GEOCODE') {
-              this.set({ error: GEOCODING_API_ERROR, focusIndex: -1, predictions: [] });
-            } else {
-              this.set('error', NO_RESULTS_MESSAGE)
-            }
+          this.trigger("selection-made", prediction.get("description"));
+          this.goToLocation(geocodings[0]);
+        } catch (e) {
+          if (
+            e.code === "REQUEST_DENIED" &&
+            e.endpoint === "GEOCODER_GEOCODE"
+          ) {
+            this.set({
+              error: GEOCODING_API_ERROR,
+              focusIndex: -1,
+              predictions: [],
+            });
+          } else {
+            this.set("error", NO_RESULTS_MESSAGE);
           }
-        },
-
-        /**
-         * Event handler for Backbone.View configuration that is called whenever 
-         * the user clicks the search button or hits the Enter key.
-         * @param {string} value is the query string.
-         */
-        async search(value) {
-          if (!value) return;
-
-          // This is not a lat,long value, so geocode the prediction instead.
-          if (!GeoPoint.couldBeLatLong(value)) {
-            const focusedIndex = Math.max(0, this.get("focusIndex"));
-            this.selectPrediction(this.get('predictions')[focusedIndex]);
+        }
+      },
+
+      /**
+       * Event handler for Backbone.View configuration that is called whenever
+       * the user clicks the search button or hits the Enter key.
+       * @param {string} value is the query string.
+       */
+      async search(value) {
+        if (!value) return;
+
+        // This is not a lat,long value, so geocode the prediction instead.
+        if (!GeoPoint.couldBeLatLong(value)) {
+          const focusedIndex = Math.max(0, this.get("focusIndex"));
+          this.selectPrediction(this.get("predictions")[focusedIndex]);
+          return;
+        }
+
+        // Unset error so the error will fire a change event even if it is the
+        // same error as already exists.
+        this.unset("error", { silent: true });
+
+        try {
+          const geoPoint = new GeoPoint(value, { parse: true });
+          geoPoint.set("height", 10000 /* meters */);
+          if (geoPoint.isValid()) {
+            this.set("error", "");
+            this.mapModel.zoomTo(geoPoint);
             return;
           }
 
-          // Unset error so the error will fire a change event even if it is the
-          // same error as already exists.
-          this.unset('error', { silent: true });
-
-          try {
-            const geoPoint = new GeoPoint(value, { parse: true });
-            geoPoint.set("height", 10000 /* meters */);
-            if (geoPoint.isValid()) {
-              this.set('error', '');
-              this.mapModel.zoomTo(geoPoint);
-              return;
-            }
-
-            const errors = geoPoint.validationError;
-            if (errors.latitude) {
-              this.set('error', errors.latitude);
-            } else if (errors.longitude) {
-              this.set('error', errors.longitude);
-            }
-          } catch (e) {
-            this.set('error', e.message);
+          const errors = geoPoint.validationError;
+          if (errors.latitude) {
+            this.set("error", errors.latitude);
+          } else if (errors.longitude) {
+            this.set("error", errors.longitude);
           }
-        },
-      });
-
-    return ViewfinderModel;
-  });
+ } catch (e) { + this.set("error", e.message); + } + }, + }, + ); + + return ViewfinderModel; +}); +
diff --git a/docs/docs/src_js_models_maps_viewfinder_ZoomPresetModel.js.html b/docs/docs/src_js_models_maps_viewfinder_ZoomPresetModel.js.html index 825e26cce..009c3a0e1 100644 --- a/docs/docs/src_js_models_maps_viewfinder_ZoomPresetModel.js.html +++ b/docs/docs/src_js_models_maps_viewfinder_ZoomPresetModel.js.html @@ -44,66 +44,69 @@

Source: src/js/models/maps/viewfinder/ZoomPresetModel.js<
-
'use strict';
-
-define(
-  ['underscore', 'backbone', 'models/maps/GeoPoint'],
-  (_, Backbone, GeoPoint) => {
-    /**
-    * @class ZoomPresetModel
-    * @classdesc ZoomPresetModel represents a point of interest on a map that can
-    * be configured within a MapView.
-    * @classcategory Models/Maps
-    * @extends Backbone.Model
-    * @since 2.29.0
-    */
-    const ZoomPresetModel = Backbone.Model.extend(
+            
"use strict";
+
+define(["underscore", "backbone", "models/maps/GeoPoint"], (
+  _,
+  Backbone,
+  GeoPoint,
+) => {
+  /**
+   * @class ZoomPresetModel
+   * @classdesc ZoomPresetModel represents a point of interest on a map that can
+   * be configured within a MapView.
+   * @classcategory Models/Maps
+   * @extends Backbone.Model
+   * @since 2.29.0
+   */
+  const ZoomPresetModel = Backbone.Model.extend(
     /** @lends ZoomPresetModel.prototype */ {
-
-        /**
-         * @typedef {Object} ZoomPresetModelOptions
-         * @property {string} title The displayed title for the preset.
-         * @property {GeoPoint} geoPoint The location representing this preset,
-         * including height information.
-         * @property {string} description A brief description of the layers and
-         * location.
-         * @property {string[]} enabledLayerIds A list of layer IDs which are to
-         * be enabled for this preset.
-         * @property {string[]} enabledLayerLabels A list of layer labels which
-         * are enabled for this preset.
-         */
-
-        /**
-         * @name ZoomPresetModel#defaults
-         * @type {ZoomPresetModelOptions}
-         */
-        defaults() {
-          return {
-            description: '',
-            enabledLayerIds: [],
-            enabledLayerLabels: [],
-            geoPoint: null,
-            title: '',
-          }
-        },
-
-        /**
-         * @param {Object} position The latitude, longitude, and height of this
-         * ZoomPresetModel's GeoPoint.
-         */
-        parse({ position, ...rest }) {
-          const geoPoint = new GeoPoint({
-            latitude: position.latitude,
-            longitude: position.longitude,
-            height: position.height
-          });
-
-          return { geoPoint, ...rest };
-        },
-      });
-
-    return ZoomPresetModel;
-  });
+ /** + * @typedef {Object} ZoomPresetModelOptions + * @property {string} title The displayed title for the preset. + * @property {GeoPoint} geoPoint The location representing this preset, + * including height information. + * @property {string} description A brief description of the layers and + * location. + * @property {string[]} enabledLayerIds A list of layer IDs which are to + * be enabled for this preset. + * @property {string[]} enabledLayerLabels A list of layer labels which + * are enabled for this preset. + */ + + /** + * @name ZoomPresetModel#defaults + * @type {ZoomPresetModelOptions} + */ + defaults() { + return { + description: "", + enabledLayerIds: [], + enabledLayerLabels: [], + geoPoint: null, + title: "", + }; + }, + + /** + * @param {Object} position The latitude, longitude, and height of this + * ZoomPresetModel's GeoPoint. + */ + parse({ position, ...rest }) { + const geoPoint = new GeoPoint({ + latitude: position.latitude, + longitude: position.longitude, + height: position.height, + }); + + return { geoPoint, ...rest }; + }, + }, + ); + + return ZoomPresetModel; +}); +
diff --git a/docs/docs/src_js_models_metadata_ScienceMetadata.js.html b/docs/docs/src_js_models_metadata_ScienceMetadata.js.html index 87855d02d..bd38338d3 100644 --- a/docs/docs/src_js_models_metadata_ScienceMetadata.js.html +++ b/docs/docs/src_js_models_metadata_ScienceMetadata.js.html @@ -44,11 +44,13 @@

Source: src/js/models/metadata/ScienceMetadata.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'],
-    function($, _, Backbone, DataONEObject){
-
-        /**
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+  $,
+  _,
+  Backbone,
+  DataONEObject,
+) {
+  /**
         @class ScienceMetadata
          @classdesc ScienceMetadata represents a generic science metadata document.
          It's properties are limited to those shared across subclasses,
@@ -57,190 +59,201 @@ 

Source: src/js/models/metadata/ScienceMetadata.js

* @classcategory Models/Metadata * @extends DataONEObject */ - var ScienceMetadata = DataONEObject.extend( - /** @lends ScienceMetadata.prototype */{ - - // Only add fields present in the Solr service to the defaults - defaults: function(){ return _.extend(DataONEObject.prototype.defaults(), { - abstract: [], - attribute: [], - attributeDescription: [], - attributeLabel: [], - attributeName: [], - attributeUnit: [], - author: null, - authorGivenName: null, - authoritativeMN: null, - authorLastName: [], - authorSurName: null, - beginDate: null, - changePermission: [], - contactOrganization: [], - datasource: null, - dataUrl: null, - dateModified: null, - datePublished: null, - dateUploaded: null, - decade: null, - edition: null, - endDate: null, - fileID: null, - formatType: "METADATA", - gcmdKeyword: [], - investigator: [], - isDocumentedBy: [], - isPublic: null, - keyConcept: [], - keywords: [], - mediaType: null, - mediaTypeProperty: [], - origin: [], - originator: [], - placeKey: [], - presentationCat: null, - project: null, - pubDate: null, - purpose: null, - readPermission: [], - relatedOrganizations: [], - replicaMN: [], - sensor: [], - sensorText: [], - source: [], - scientificName: [], - title: [], - type: "Metadata", - species: [], - genus: [], - family: [], - class: [], - phylum: [], - order: [], - kingdom: [], - westBoundCoord: null, - eastBoundCoord: null, - northBoundCoord: null, - southBoundCoord: null, - site: [], - namedLocation: [], - noBoundingBox: null, - geoform: null, - isSpatial: null, - sortOrder: 1, - geohash_1: [], - geohash_2: [], - geohash_3: [], - geohash_4: [], - geohash_5: [], - geohash_6: [], - geohash_7: [], - geohash_8: [], - geohash_9: [], - sem_annotated_by: [], - sem_annotates: [], - sem_annotation: [], - sem_comment: [] - }) }, - - type: "ScienceMetadata", - - nodeNameMap: function(){ return this.constructor.__super__.nodeNameMap(); }, - - /* Initialize a ScienceMetadata object */ - initialize: function(attributes) { - // Call initialize for the super class - DataONEObject.prototype.initialize.call(this, attributes); - - - // ScienceMetadata-specific init goes here - this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:changed", function(){ - if(MetacatUI.rootDataPackage.packageModel.get("changed")) - this.set("uploadStatus", "q"); - }); - - }, - - /* Construct the Solr query URL to be called */ - url: function() { - - // Build the URL to include default fields in ScienceMetadata - var fieldList = "*",//Object.keys(this.defaults), - lastField = _.last(fieldList), - searchFields = "", - query = "q=", - queryOptions = "&wt=json&fl=", - url = ""; - - // Make a list of the search fields - _.each(fieldList, function(value, key, list) { - if ( value === lastField ) { - searchFields += value; - - } else { - searchFields += value; - searchFields += ","; - - } - }); - - queryOptions += searchFields; - query += 'id:"' + encodeURIComponent(this.get("id")) + '"'; - - url = MetacatUI.appModel.get("queryServiceUrl") + query + queryOptions; - return url; - - }, - - /* Fetch the ScienceMetadata from the MN Solr service */ - fetch: function(options) { - if(!options) - var options = {}; - - //Add the authorization options - _.extend(options, MetacatUI.appUserModel.createAjaxSettings()); - - //Call Backbone.Model.fetch to retrieve the info - return Backbone.Model.prototype.fetch.call(this, options); - - }, - - - /* - * Updates the relationships with other models when this model has been updated - */ - updateRelationships: function(){ - _.each(this.get("collections"), function(collection){ - //Get the old id for this model - var oldId = this.get("oldPid"); - - if(!oldId) return; - - //Find references to the old id in the documents relationship - var outdatedModels = collection.filter(function(m){ - return _.contains(m.get("isDocumentedBy"), oldId); - }); - - //Update the documents array in each model - _.each(outdatedModels, function(model){ - var updatedDocumentedBy = _.without(model.get("isDocumentedBy"), oldId); - updatedDocumentedBy.push(this.get("id")); - - model.set("isDocumentedBy", updatedDocumentedBy); - }, this); - - }, this); - - //Update the documents relationship - if( _.contains(this.get("documents"), this.get("oldPid")) ){ - var updatedDocuments = _.without(this.get("documents"), this.get("oldPid")); - - this.set("documents", updatedDocuments); - } - } + var ScienceMetadata = DataONEObject.extend( + /** @lends ScienceMetadata.prototype */ { + // Only add fields present in the Solr service to the defaults + defaults: function () { + return _.extend(DataONEObject.prototype.defaults(), { + abstract: [], + attribute: [], + attributeDescription: [], + attributeLabel: [], + attributeName: [], + attributeUnit: [], + author: null, + authorGivenName: null, + authoritativeMN: null, + authorLastName: [], + authorSurName: null, + beginDate: null, + changePermission: [], + contactOrganization: [], + datasource: null, + dataUrl: null, + dateModified: null, + datePublished: null, + dateUploaded: null, + decade: null, + edition: null, + endDate: null, + fileID: null, + formatType: "METADATA", + gcmdKeyword: [], + investigator: [], + isDocumentedBy: [], + isPublic: null, + keyConcept: [], + keywords: [], + mediaType: null, + mediaTypeProperty: [], + origin: [], + originator: [], + placeKey: [], + presentationCat: null, + project: null, + pubDate: null, + purpose: null, + readPermission: [], + relatedOrganizations: [], + replicaMN: [], + sensor: [], + sensorText: [], + source: [], + scientificName: [], + title: [], + type: "Metadata", + species: [], + genus: [], + family: [], + class: [], + phylum: [], + order: [], + kingdom: [], + westBoundCoord: null, + eastBoundCoord: null, + northBoundCoord: null, + southBoundCoord: null, + site: [], + namedLocation: [], + noBoundingBox: null, + geoform: null, + isSpatial: null, + sortOrder: 1, + geohash_1: [], + geohash_2: [], + geohash_3: [], + geohash_4: [], + geohash_5: [], + geohash_6: [], + geohash_7: [], + geohash_8: [], + geohash_9: [], + sem_annotated_by: [], + sem_annotates: [], + sem_annotation: [], + sem_comment: [], + }); + }, + + type: "ScienceMetadata", + + nodeNameMap: function () { + return this.constructor.__super__.nodeNameMap(); + }, + + /* Initialize a ScienceMetadata object */ + initialize: function (attributes) { + // Call initialize for the super class + DataONEObject.prototype.initialize.call(this, attributes); + + // ScienceMetadata-specific init goes here + this.listenTo( + MetacatUI.rootDataPackage.packageModel, + "change:changed", + function () { + if (MetacatUI.rootDataPackage.packageModel.get("changed")) + this.set("uploadStatus", "q"); + }, + ); + }, + + /* Construct the Solr query URL to be called */ + url: function () { + // Build the URL to include default fields in ScienceMetadata + var fieldList = "*", //Object.keys(this.defaults), + lastField = _.last(fieldList), + searchFields = "", + query = "q=", + queryOptions = "&wt=json&fl=", + url = ""; + + // Make a list of the search fields + _.each(fieldList, function (value, key, list) { + if (value === lastField) { + searchFields += value; + } else { + searchFields += value; + searchFields += ","; + } }); - return ScienceMetadata; - } -); + + queryOptions += searchFields; + query += 'id:"' + encodeURIComponent(this.get("id")) + '"'; + + url = MetacatUI.appModel.get("queryServiceUrl") + query + queryOptions; + return url; + }, + + /* Fetch the ScienceMetadata from the MN Solr service */ + fetch: function (options) { + if (!options) var options = {}; + + //Add the authorization options + _.extend(options, MetacatUI.appUserModel.createAjaxSettings()); + + //Call Backbone.Model.fetch to retrieve the info + return Backbone.Model.prototype.fetch.call(this, options); + }, + + /* + * Updates the relationships with other models when this model has been updated + */ + updateRelationships: function () { + _.each( + this.get("collections"), + function (collection) { + //Get the old id for this model + var oldId = this.get("oldPid"); + + if (!oldId) return; + + //Find references to the old id in the documents relationship + var outdatedModels = collection.filter(function (m) { + return _.contains(m.get("isDocumentedBy"), oldId); + }); + + //Update the documents array in each model + _.each( + outdatedModels, + function (model) { + var updatedDocumentedBy = _.without( + model.get("isDocumentedBy"), + oldId, + ); + updatedDocumentedBy.push(this.get("id")); + + model.set("isDocumentedBy", updatedDocumentedBy); + }, + this, + ); + }, + this, + ); + + //Update the documents relationship + if (_.contains(this.get("documents"), this.get("oldPid"))) { + var updatedDocuments = _.without( + this.get("documents"), + this.get("oldPid"), + ); + + this.set("documents", updatedDocuments); + } + }, + }, + ); + return ScienceMetadata; +});
diff --git a/docs/docs/src_js_models_metadata_eml211_EML211.js.html b/docs/docs/src_js_models_metadata_eml211_EML211.js.html index cf4687ff9..02c460c95 100644 --- a/docs/docs/src_js_models_metadata_eml211_EML211.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EML211.js.html @@ -44,686 +44,740 @@

Source: src/js/models/metadata/eml211/EML211.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'uuid',
-        'collections/Units',
-        'models/metadata/ScienceMetadata',
-        'models/DataONEObject',
-        'models/metadata/eml211/EMLGeoCoverage',
-        'models/metadata/eml211/EMLKeywordSet',
-        'models/metadata/eml211/EMLTaxonCoverage',
-        'models/metadata/eml211/EMLTemporalCoverage',
-        'models/metadata/eml211/EMLDistribution',
-        'models/metadata/eml211/EMLEntity',
-        'models/metadata/eml211/EMLDataTable',
-        'models/metadata/eml211/EMLOtherEntity',
-        'models/metadata/eml211/EMLParty',
-        'models/metadata/eml211/EMLProject',
-        'models/metadata/eml211/EMLText',
-        'models/metadata/eml211/EMLMethods',
-        'collections/metadata/eml/EMLAnnotations',
-        'models/metadata/eml211/EMLAnnotation'],
-    function($, _, Backbone, uuid, Units, ScienceMetadata, DataONEObject,
-        EMLGeoCoverage, EMLKeywordSet, EMLTaxonCoverage, EMLTemporalCoverage,
-        EMLDistribution, EMLEntity, EMLDataTable, EMLOtherEntity, EMLParty,
-            EMLProject, EMLText, EMLMethods, EMLAnnotations, EMLAnnotation) {
-
-      /**
-      * @class EML211
-      * @classdesc An EML211 object represents an Ecological Metadata Language
-      * document, version 2.1.1
-      * @classcategory Models/Metadata/EML211
-      * @extends ScienceMetadata
-      */
-      var EML211 = ScienceMetadata.extend(
-        /** @lends EML211.prototype */{
-
-        type: "EML",
-
-        defaults: function(){
-          return _.extend(ScienceMetadata.prototype.defaults(), {
-            id: "urn:uuid:" + uuid.v4(),
-            formatId: "https://eml.ecoinformatics.org/eml-2.2.0",
-            objectXML: null,
-              isEditable: false,
-              alternateIdentifier: [],
-              shortName: null,
-              title: [],
-              creator: [], // array of EMLParty objects
-              metadataProvider: [], // array of EMLParty objects
-              associatedParty : [], // array of EMLParty objects
-              contact: [], // array of EMLParty objects
-              publisher: [], // array of EMLParty objects
-              pubDate: null,
-              language: null,
-              series: null,
-              abstract: [], //array of EMLText objects
-              keywordSets: [], //array of EMLKeywordSet objects
-              additionalInfo: [],
-              intellectualRights: "This work is dedicated to the public domain under the Creative Commons Universal 1.0 Public Domain Dedication. To view a copy of this dedication, visit https://creativecommons.org/publicdomain/zero/1.0/.",
-              distribution: [], // array of EMLDistribution objects
-              geoCoverage : [], //an array for EMLGeoCoverages
-              temporalCoverage : [], //an array of EMLTempCoverage models
-              taxonCoverage : [], //an array of EMLTaxonCoverages
-              purpose: [],
-              entities: [], //An array of EMLEntities
-              pubplace: null,
-              methods: new EMLMethods(), // An EMLMethods objects
-              project: null, // An EMLProject object,
-              annotations: null, // Dataset-level annotations
-              dataSensitivityPropertyURI: "http://purl.dataone.org/odo/SENSO_00000005",
-              nodeOrder: [
-                "alternateidentifier",
-                "shortname",
-                "title",
-                "creator",
-                "metadataprovider",
-                "associatedparty",
-                "pubdate",
-                "language",
-                "series",
-                "abstract",
-                "keywordset",
-                "additionalinfo",
-                "intellectualrights",
-                "licensed",
-                "distribution",
-                "coverage",
-                "annotation",
-                "purpose",
-                "introduction",
-                "gettingstarted",
-                "acknowledgements",
-                "maintenance",
-                "contact",
-                "publisher",
-                "pubplace",
-                "methods",
-                "project",
-                "datatable",
-                "spatialraster",
-                "spatialvector",
-                "storedprocedure",
-                "view",
-                "otherentity",
-                "referencepublications",
-                "usagecitations",
-                "literaturecited",
-              ]
-          });
-        },
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "uuid",
+  "collections/Units",
+  "models/metadata/ScienceMetadata",
+  "models/DataONEObject",
+  "models/metadata/eml211/EMLGeoCoverage",
+  "models/metadata/eml211/EMLKeywordSet",
+  "models/metadata/eml211/EMLTaxonCoverage",
+  "models/metadata/eml211/EMLTemporalCoverage",
+  "models/metadata/eml211/EMLDistribution",
+  "models/metadata/eml211/EMLEntity",
+  "models/metadata/eml211/EMLDataTable",
+  "models/metadata/eml211/EMLOtherEntity",
+  "models/metadata/eml211/EMLParty",
+  "models/metadata/eml211/EMLProject",
+  "models/metadata/eml211/EMLText",
+  "models/metadata/eml211/EMLMethods",
+  "collections/metadata/eml/EMLAnnotations",
+  "models/metadata/eml211/EMLAnnotation",
+], function (
+  $,
+  _,
+  Backbone,
+  uuid,
+  Units,
+  ScienceMetadata,
+  DataONEObject,
+  EMLGeoCoverage,
+  EMLKeywordSet,
+  EMLTaxonCoverage,
+  EMLTemporalCoverage,
+  EMLDistribution,
+  EMLEntity,
+  EMLDataTable,
+  EMLOtherEntity,
+  EMLParty,
+  EMLProject,
+  EMLText,
+  EMLMethods,
+  EMLAnnotations,
+  EMLAnnotation,
+) {
+  /**
+   * @class EML211
+   * @classdesc An EML211 object represents an Ecological Metadata Language
+   * document, version 2.1.1
+   * @classcategory Models/Metadata/EML211
+   * @extends ScienceMetadata
+   */
+  var EML211 = ScienceMetadata.extend(
+    /** @lends EML211.prototype */ {
+      type: "EML",
+
+      defaults: function () {
+        return _.extend(ScienceMetadata.prototype.defaults(), {
+          id: "urn:uuid:" + uuid.v4(),
+          formatId: "https://eml.ecoinformatics.org/eml-2.2.0",
+          objectXML: null,
+          isEditable: false,
+          alternateIdentifier: [],
+          shortName: null,
+          title: [],
+          creator: [], // array of EMLParty objects
+          metadataProvider: [], // array of EMLParty objects
+          associatedParty: [], // array of EMLParty objects
+          contact: [], // array of EMLParty objects
+          publisher: [], // array of EMLParty objects
+          pubDate: null,
+          language: null,
+          series: null,
+          abstract: [], //array of EMLText objects
+          keywordSets: [], //array of EMLKeywordSet objects
+          additionalInfo: [],
+          intellectualRights:
+            "This work is dedicated to the public domain under the Creative Commons Universal 1.0 Public Domain Dedication. To view a copy of this dedication, visit https://creativecommons.org/publicdomain/zero/1.0/.",
+          distribution: [], // array of EMLDistribution objects
+          geoCoverage: [], //an array for EMLGeoCoverages
+          temporalCoverage: [], //an array of EMLTempCoverage models
+          taxonCoverage: [], //an array of EMLTaxonCoverages
+          purpose: [],
+          entities: [], //An array of EMLEntities
+          pubplace: null,
+          methods: new EMLMethods(), // An EMLMethods objects
+          project: null, // An EMLProject object,
+          annotations: null, // Dataset-level annotations
+          dataSensitivityPropertyURI:
+            "http://purl.dataone.org/odo/SENSO_00000005",
+          nodeOrder: [
+            "alternateidentifier",
+            "shortname",
+            "title",
+            "creator",
+            "metadataprovider",
+            "associatedparty",
+            "pubdate",
+            "language",
+            "series",
+            "abstract",
+            "keywordset",
+            "additionalinfo",
+            "intellectualrights",
+            "licensed",
+            "distribution",
+            "coverage",
+            "annotation",
+            "purpose",
+            "introduction",
+            "gettingstarted",
+            "acknowledgements",
+            "maintenance",
+            "contact",
+            "publisher",
+            "pubplace",
+            "methods",
+            "project",
+            "datatable",
+            "spatialraster",
+            "spatialvector",
+            "storedprocedure",
+            "view",
+            "otherentity",
+            "referencepublications",
+            "usagecitations",
+            "literaturecited",
+          ],
+        });
+      },
 
-        units: new Units(),
+      units: new Units(),
 
-        initialize: function(attributes) {
-            // Call initialize for the super class
-            ScienceMetadata.prototype.initialize.call(this, attributes);
+      initialize: function (attributes) {
+        // Call initialize for the super class
+        ScienceMetadata.prototype.initialize.call(this, attributes);
 
-            // EML211-specific init goes here
-            // this.set("objectXML", this.createXML());
-            this.parse(this.createXML());
+        // EML211-specific init goes here
+        // this.set("objectXML", this.createXML());
+        this.parse(this.createXML());
 
-            this.on("sync", function(){
-              this.set("synced", true);
-            });
+        this.on("sync", function () {
+          this.set("synced", true);
+        });
 
-            //Create a Unit collection
-            if(!this.units.length)
-              this.createUnits();
-        },
+        //Create a Unit collection
+        if (!this.units.length) this.createUnits();
+      },
 
-        url: function(options) {
-            var identifier;
-            if ( options && options.update ) {
-                identifier = this.get("oldPid") || this.get("seriesid");
-            } else {
-                identifier = this.get("id") || this.get("seriesid");
-            }
-            return MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(identifier);
-        },
-
-        /*
-         * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
-         * Used during parse() and serialize()
-         */
-        nodeNameMap: function(){
-          return _.extend(
-              this.constructor.__super__.nodeNameMap(),
-              EMLDistribution.prototype.nodeNameMap(),
-              EMLGeoCoverage.prototype.nodeNameMap(),
-              EMLKeywordSet.prototype.nodeNameMap(),
-              EMLParty.prototype.nodeNameMap(),
-              EMLProject.prototype.nodeNameMap(),
-              EMLTaxonCoverage.prototype.nodeNameMap(),
-              EMLTemporalCoverage.prototype.nodeNameMap(),
-              EMLMethods.prototype.nodeNameMap(),
-              {
-                "accuracyreport" : "accuracyReport",
-                "actionlist" : "actionList",
-                "additionalclassifications" : "additionalClassifications",
-                "additionalinfo" : "additionalInfo",
-                "additionallinks" : "additionalLinks",
-                "additionalmetadata" : "additionalMetadata",
-                "allowfirst" : "allowFirst",
-                "alternateidentifier" : "alternateIdentifier",
-                "altitudedatumname" : "altitudeDatumName",
-                "altitudedistanceunits" : "altitudeDistanceUnits",
-                "altituderesolution" : "altitudeResolution",
-                "altitudeencodingmethod" : "altitudeEncodingMethod",
-                "altitudesysdef" : "altitudeSysDef",
-                "asneeded" : "asNeeded",
-                "associatedparty" : "associatedParty",
-                "attributeaccuracyexplanation" : "attributeAccuracyExplanation",
-                "attributeaccuracyreport" : "attributeAccuracyReport",
-                "attributeaccuracyvalue" : "attributeAccuracyValue",
-                "attributedefinition" : "attributeDefinition",
-                "attributelabel" : "attributeLabel",
-                "attributelist" : "attributeList",
-                "attributename" : "attributeName",
-                "attributeorientation" : "attributeOrientation",
-                "attributereference" : "attributeReference",
-                "awardnumber" : "awardNumber",
-                "awardurl" : "awardUrl",
-                "audiovisual" : "audioVisual",
-                "authsystem" : "authSystem",
-                "banddescription" : "bandDescription",
-                "bilinearfit" : "bilinearFit",
-                "binaryrasterformat" : "binaryRasterFormat",
-                "blockedmembernode" : "blockedMemberNode",
-                "booktitle" : "bookTitle",
-                "cameracalibrationinformationavailability" : "cameraCalibrationInformationAvailability",
-                "casesensitive" : "caseSensitive",
-                "cellgeometry" : "cellGeometry",
-                "cellsizexdirection" : "cellSizeXDirection",
-                "cellsizeydirection" : "cellSizeYDirection",
-                "changehistory" : "changeHistory",
-                "changedate" : "changeDate",
-                "changescope" : "changeScope",
-                "chapternumber" : "chapterNumber",
-                "characterencoding" : "characterEncoding",
-                "checkcondition" : "checkCondition",
-                "checkconstraint" : "checkConstraint",
-                "childoccurences" : "childOccurences",
-                "citableclassificationsystem" : "citableClassificationSystem",
-                "cloudcoverpercentage" : "cloudCoverPercentage",
-                "codedefinition" : "codeDefinition",
-                "codeexplanation" : "codeExplanation",
-                "codesetname" : "codesetName",
-                "codeseturl" : "codesetURL",
-                "collapsedelimiters" : "collapseDelimiters",
-                "communicationtype" : "communicationType",
-                "compressiongenerationquality" : "compressionGenerationQuality",
-                "compressionmethod" : "compressionMethod",
-                "conferencedate" : "conferenceDate",
-                "conferencelocation" : "conferenceLocation",
-                "conferencename" : "conferenceName",
-                "conferenceproceedings" : "conferenceProceedings",
-                "constraintdescription" : "constraintDescription",
-                "constraintname" : "constraintName",
-                "constanttosi" : "constantToSI",
-                "controlpoint" : "controlPoint",
-                "cornerpoint" : "cornerPoint",
-                "customunit" : "customUnit",
-                "dataformat" : "dataFormat",
-                "datasetgpolygon" : "datasetGPolygon",
-                "datasetgpolygonoutergring" : "datasetGPolygonOuterGRing",
-                "datasetgpolygonexclusiongring" : "datasetGPolygonExclusionGRing",
-                "datatable" : "dataTable",
-                "datatype" : "dataType",
-                "datetime" : "dateTime",
-                "datetimedomain" : "dateTimeDomain",
-                "datetimeprecision" : "dateTimePrecision",
-                "defaultvalue" : "defaultValue",
-                "definitionattributereference" : "definitionAttributeReference",
-                "denomflatratio" : "denomFlatRatio",
-                "depthsysdef" : "depthSysDef",
-                "depthdatumname" : "depthDatumName",
-                "depthdistanceunits" : "depthDistanceUnits",
-                "depthencodingmethod" : "depthEncodingMethod",
-                "depthresolution" : "depthResolution",
-                "descriptorvalue" : "descriptorValue",
-                "dictref" : "dictRef",
-                "diskusage" : "diskUsage",
-                "domainDescription" : "domainDescription",
-                "editedbook" : "editedBook",
-                "encodingmethod" : "encodingMethod",
-                "endcondition" : "endCondition",
-                "entitycodelist" : "entityCodeList",
-                "entitydescription" : "entityDescription",
-                "entityname" : "entityName",
-                "entityreference" : "entityReference",
-                "entitytype" : "entityType",
-                "enumerateddomain" : "enumeratedDomain",
-                "errorbasis" : "errorBasis",
-                "errorvalues" : "errorValues",
-                "externalcodeset" : "externalCodeSet",
-                "externallydefinedformat" : "externallyDefinedFormat",
-                "fielddelimiter" : "fieldDelimiter",
-                "fieldstartcolumn" : "fieldStartColumn",
-                "fieldwidth" : "fieldWidth",
-                "filmdistortioninformationavailability" : "filmDistortionInformationAvailability",
-                "foreignkey" : "foreignKey",
-                "formatname" : "formatName",
-                "formatstring" : "formatString",
-                "formatversion" : "formatVersion",
-                "fractiondigits" : "fractionDigits",
-                "fundername" : "funderName",
-                "funderidentifier" : "funderIdentifier",
-                "gettingstarted" : "gettingStarted",
-                "gring" : "gRing",
-                "gringpoint" : "gRingPoint",
-                "gringlatitude" : "gRingLatitude",
-                "gringlongitude" : "gRingLongitude",
-                "geogcoordsys" : "geogCoordSys",
-                "geometricobjectcount" : "geometricObjectCount",
-                "georeferenceinfo" : "georeferenceInfo",
-                "highwavelength" : "highWavelength",
-                "horizontalaccuracy" : "horizontalAccuracy",
-                "horizcoordsysdef" : "horizCoordSysDef",
-                "horizcoordsysname" : "horizCoordSysName",
-                "identifiername" : "identifierName",
-                "illuminationazimuthangle" : "illuminationAzimuthAngle",
-                "illuminationelevationangle" : "illuminationElevationAngle",
-                "imagingcondition" : "imagingCondition",
-                "imagequalitycode" : "imageQualityCode",
-                "imageorientationangle" : "imageOrientationAngle",
-                "intellectualrights" : "intellectualRights",
-                "imagedescription" : "imageDescription",
-                "isbn" : "ISBN",
-                "issn" : "ISSN",
-                "joincondition" : "joinCondition",
-                "keywordtype" : "keywordType",
-                "languagevalue" : "LanguageValue",
-                "languagecodestandard" : "LanguageCodeStandard",
-                "lensdistortioninformationavailability" : "lensDistortionInformationAvailability",
-                "licensename" : "licenseName",
-                "licenseurl" : "licenseURL",
-                "linenumber" : "lineNumber",
-                "literalcharacter" : "literalCharacter",
-                "literallayout" : "literalLayout",
-                "literaturecited" : "literatureCited",
-                "lowwavelength" : "lowWaveLength",
-                "machineprocessor" : "machineProcessor",
-                "maintenanceupdatefrequency" : "maintenanceUpdateFrequency",
-                "matrixtype" : "matrixType",
-                "maxexclusive" : "maxExclusive",
-                "maxinclusive" : "maxInclusive",
-                "maxlength" : "maxLength",
-                "maxrecordlength" : "maxRecordLength",
-                "maxvalues" : "maxValues",
-                "measurementscale" : "measurementScale",
-                "metadatalist" : "metadataList",
-                "methodstep" : "methodStep",
-                "minexclusive" : "minExclusive",
-                "mininclusive" : "minInclusive",
-                "minlength" : "minLength",
-                "minvalues" : "minValues",
-                "missingvaluecode" : "missingValueCode",
-                "moduledocs" : "moduleDocs",
-                "modulename" : "moduleName",
-                "moduledescription" : "moduleDescription",
-                "multiband" : "multiBand",
-                "multipliertosi" : "multiplierToSI",
-                "nonnumericdomain" : "nonNumericDomain",
-                "notnullconstraint" : "notNullConstraint",
-                "notplanned" : "notPlanned",
-                "numberofbands" : "numberOfBands",
-                "numbertype" : "numberType",
-                "numericdomain" : "numericDomain",
-                "numfooterlines" : "numFooterLines",
-                "numheaderlines" : "numHeaderLines",
-                "numberofrecords" : "numberOfRecords",
-                "numberofvolumes" : "numberOfVolumes",
-                "numphysicallinesperrecord" : "numPhysicalLinesPerRecord",
-                "objectname" : "objectName",
-                "oldvalue" : "oldValue",
-                "operatingsystem" : "operatingSystem",
-                "orderattributereference" : "orderAttributeReference",
-                "originalpublication" : "originalPublication",
-                "otherentity" : "otherEntity",
-                "othermaintenanceperiod" : "otherMaintenancePeriod",
-                "parameterdefinition" : "parameterDefinition",
-                "packageid" : "packageId",
-                "pagerange" : "pageRange",
-                "parentoccurences" : "parentOccurences",
-                "parentsi" : "parentSI",
-                "peakresponse" : "peakResponse",
-                "personalcommunication" : "personalCommunication",
-                "physicallinedelimiter" : "physicalLineDelimiter",
-                "pointinpixel" : "pointInPixel",
-                "preferredmembernode" : "preferredMemberNode",
-                "preprocessingtypecode" : "preProcessingTypeCode",
-                "primarykey" : "primaryKey",
-                "primemeridian" : "primeMeridian",
-                "proceduralstep" : "proceduralStep",
-                "programminglanguage" : "programmingLanguage",
-                "projcoordsys" : "projCoordSys",
-                "projectionlist" : "projectionList",
-                "propertyuri" : "propertyURI",
-                "pubdate" : "pubDate",
-                "pubplace" : "pubPlace",
-                "publicationplace" : "publicationPlace",
-                "quantitativeaccuracyreport" : "quantitativeAccuracyReport",
-                "quantitativeaccuracyvalue" : "quantitativeAccuracyValue",
-                "quantitativeaccuracymethod" : "quantitativeAccuracyMethod",
-                "quantitativeattributeaccuracyassessment" : "quantitativeAttributeAccuracyAssessment",
-                "querystatement" : "queryStatement",
-                "quotecharacter" : "quoteCharacter",
-                "radiometricdataavailability" : "radiometricDataAvailability",
-                "rasterorigin" : "rasterOrigin",
-                "recommendedunits" : "recommendedUnits",
-                "recommendedusage" : "recommendedUsage",
-                "referencedkey" : "referencedKey",
-                "referencetype" : "referenceType",
-                "relatedentry" : "relatedEntry",
-                "relationshiptype" : "relationshipType",
-                "reportnumber" : "reportNumber",
-                "reprintedition" : "reprintEdition",
-                "researchproject" : "researchProject",
-                "researchtopic" : "researchTopic",
-                "recorddelimiter" : "recordDelimiter",
-                "referencepublication" : "referencePublication",
-                "revieweditem" : "reviewedItem",
-                "rowcolumnorientation" : "rowColumnOrientation",
-                "runtimememoryusage" : "runtimeMemoryUsage",
-                "samplingdescription" : "samplingDescription",
-                "scalefactor" : "scaleFactor",
-                "sequenceidentifier" : "sequenceIdentifier",
-                "semiaxismajor" : "semiAxisMajor",
-                "shortname" : "shortName",
-                "simpledelimited" : "simpleDelimited",
-                "spatialraster" : "spatialRaster",
-                "spatialreference" : "spatialReference",
-                "spatialvector" : "spatialVector",
-                "standalone" : "standAlone",
-                "standardunit" : "standardUnit",
-                "startcondition" : "startCondition",
-                "studyareadescription" : "studyAreaDescription",
-                "storagetype" : "storageType",
-                "studyextent" : "studyExtent",
-                "studytype" : "studyType",
-                "textdelimited" : "textDelimited",
-                "textdomain" : "textDomain",
-                "textfixed" : "textFixed",
-                "textformat" : "textFormat",
-                "topologylevel" : "topologyLevel",
-                "tonegradation" : "toneGradation",
-                "totaldigits" : "totalDigits",
-                "totalfigures" : "totalFigures",
-                "totalpages" : "totalPages",
-                "totaltables" : "totalTables",
-                "triangulationindicator" : "triangulationIndicator",
-                "typesystem" : "typeSystem",
-                "uniquekey" : "uniqueKey",
-                "unittype" : "unitType",
-                "unitlist" : "unitList",
-                "usagecitation" : "usageCitation",
-                "valueuri" : "valueURI",
-                "valueattributereference" : "valueAttributeReference",
-                "verticalaccuracy" : "verticalAccuracy",
-                "vertcoordsys" : "vertCoordSys",
-                "virtualmachine" : "virtualMachine",
-                "wavelengthunits" : "waveLengthUnits",
-                "whitespace" : "whiteSpace",
-                "xintercept" : "xIntercept",
-                "xcoordinate" : "xCoordinate",
-                "xsi:schemalocation" : "xsi:schemaLocation",
-                "xslope" : "xSlope",
-                "ycoordinate" : "yCoordinate",
-                "yintercept" : "yIntercept",
-                "yslope" : "ySlope"
-              }
-          );
-        },
+      url: function (options) {
+        var identifier;
+        if (options && options.update) {
+          identifier = this.get("oldPid") || this.get("seriesid");
+        } else {
+          identifier = this.get("id") || this.get("seriesid");
+        }
+        return (
+          MetacatUI.appModel.get("objectServiceUrl") +
+          encodeURIComponent(identifier)
+        );
+      },
 
-        /**
-        * Fetch the EML from the MN object service
-        * @param {object} [options] - A set of options for this fetch()
-        * @property {boolean} [options.systemMetadataOnly=false] - If true, only the system metadata will be fetched.
-        * If false, the system metadata AND EML document will be fetched.
-        */
-        fetch: function(options) {
-          if( ! options ) var options = {};
+      /*
+       * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
+       * Used during parse() and serialize()
+       */
+      nodeNameMap: function () {
+        return _.extend(
+          this.constructor.__super__.nodeNameMap(),
+          EMLDistribution.prototype.nodeNameMap(),
+          EMLGeoCoverage.prototype.nodeNameMap(),
+          EMLKeywordSet.prototype.nodeNameMap(),
+          EMLParty.prototype.nodeNameMap(),
+          EMLProject.prototype.nodeNameMap(),
+          EMLTaxonCoverage.prototype.nodeNameMap(),
+          EMLTemporalCoverage.prototype.nodeNameMap(),
+          EMLMethods.prototype.nodeNameMap(),
+          {
+            accuracyreport: "accuracyReport",
+            actionlist: "actionList",
+            additionalclassifications: "additionalClassifications",
+            additionalinfo: "additionalInfo",
+            additionallinks: "additionalLinks",
+            additionalmetadata: "additionalMetadata",
+            allowfirst: "allowFirst",
+            alternateidentifier: "alternateIdentifier",
+            altitudedatumname: "altitudeDatumName",
+            altitudedistanceunits: "altitudeDistanceUnits",
+            altituderesolution: "altitudeResolution",
+            altitudeencodingmethod: "altitudeEncodingMethod",
+            altitudesysdef: "altitudeSysDef",
+            asneeded: "asNeeded",
+            associatedparty: "associatedParty",
+            attributeaccuracyexplanation: "attributeAccuracyExplanation",
+            attributeaccuracyreport: "attributeAccuracyReport",
+            attributeaccuracyvalue: "attributeAccuracyValue",
+            attributedefinition: "attributeDefinition",
+            attributelabel: "attributeLabel",
+            attributelist: "attributeList",
+            attributename: "attributeName",
+            attributeorientation: "attributeOrientation",
+            attributereference: "attributeReference",
+            awardnumber: "awardNumber",
+            awardurl: "awardUrl",
+            audiovisual: "audioVisual",
+            authsystem: "authSystem",
+            banddescription: "bandDescription",
+            bilinearfit: "bilinearFit",
+            binaryrasterformat: "binaryRasterFormat",
+            blockedmembernode: "blockedMemberNode",
+            booktitle: "bookTitle",
+            cameracalibrationinformationavailability:
+              "cameraCalibrationInformationAvailability",
+            casesensitive: "caseSensitive",
+            cellgeometry: "cellGeometry",
+            cellsizexdirection: "cellSizeXDirection",
+            cellsizeydirection: "cellSizeYDirection",
+            changehistory: "changeHistory",
+            changedate: "changeDate",
+            changescope: "changeScope",
+            chapternumber: "chapterNumber",
+            characterencoding: "characterEncoding",
+            checkcondition: "checkCondition",
+            checkconstraint: "checkConstraint",
+            childoccurences: "childOccurences",
+            citableclassificationsystem: "citableClassificationSystem",
+            cloudcoverpercentage: "cloudCoverPercentage",
+            codedefinition: "codeDefinition",
+            codeexplanation: "codeExplanation",
+            codesetname: "codesetName",
+            codeseturl: "codesetURL",
+            collapsedelimiters: "collapseDelimiters",
+            communicationtype: "communicationType",
+            compressiongenerationquality: "compressionGenerationQuality",
+            compressionmethod: "compressionMethod",
+            conferencedate: "conferenceDate",
+            conferencelocation: "conferenceLocation",
+            conferencename: "conferenceName",
+            conferenceproceedings: "conferenceProceedings",
+            constraintdescription: "constraintDescription",
+            constraintname: "constraintName",
+            constanttosi: "constantToSI",
+            controlpoint: "controlPoint",
+            cornerpoint: "cornerPoint",
+            customunit: "customUnit",
+            dataformat: "dataFormat",
+            datasetgpolygon: "datasetGPolygon",
+            datasetgpolygonoutergring: "datasetGPolygonOuterGRing",
+            datasetgpolygonexclusiongring: "datasetGPolygonExclusionGRing",
+            datatable: "dataTable",
+            datatype: "dataType",
+            datetime: "dateTime",
+            datetimedomain: "dateTimeDomain",
+            datetimeprecision: "dateTimePrecision",
+            defaultvalue: "defaultValue",
+            definitionattributereference: "definitionAttributeReference",
+            denomflatratio: "denomFlatRatio",
+            depthsysdef: "depthSysDef",
+            depthdatumname: "depthDatumName",
+            depthdistanceunits: "depthDistanceUnits",
+            depthencodingmethod: "depthEncodingMethod",
+            depthresolution: "depthResolution",
+            descriptorvalue: "descriptorValue",
+            dictref: "dictRef",
+            diskusage: "diskUsage",
+            domainDescription: "domainDescription",
+            editedbook: "editedBook",
+            encodingmethod: "encodingMethod",
+            endcondition: "endCondition",
+            entitycodelist: "entityCodeList",
+            entitydescription: "entityDescription",
+            entityname: "entityName",
+            entityreference: "entityReference",
+            entitytype: "entityType",
+            enumerateddomain: "enumeratedDomain",
+            errorbasis: "errorBasis",
+            errorvalues: "errorValues",
+            externalcodeset: "externalCodeSet",
+            externallydefinedformat: "externallyDefinedFormat",
+            fielddelimiter: "fieldDelimiter",
+            fieldstartcolumn: "fieldStartColumn",
+            fieldwidth: "fieldWidth",
+            filmdistortioninformationavailability:
+              "filmDistortionInformationAvailability",
+            foreignkey: "foreignKey",
+            formatname: "formatName",
+            formatstring: "formatString",
+            formatversion: "formatVersion",
+            fractiondigits: "fractionDigits",
+            fundername: "funderName",
+            funderidentifier: "funderIdentifier",
+            gettingstarted: "gettingStarted",
+            gring: "gRing",
+            gringpoint: "gRingPoint",
+            gringlatitude: "gRingLatitude",
+            gringlongitude: "gRingLongitude",
+            geogcoordsys: "geogCoordSys",
+            geometricobjectcount: "geometricObjectCount",
+            georeferenceinfo: "georeferenceInfo",
+            highwavelength: "highWavelength",
+            horizontalaccuracy: "horizontalAccuracy",
+            horizcoordsysdef: "horizCoordSysDef",
+            horizcoordsysname: "horizCoordSysName",
+            identifiername: "identifierName",
+            illuminationazimuthangle: "illuminationAzimuthAngle",
+            illuminationelevationangle: "illuminationElevationAngle",
+            imagingcondition: "imagingCondition",
+            imagequalitycode: "imageQualityCode",
+            imageorientationangle: "imageOrientationAngle",
+            intellectualrights: "intellectualRights",
+            imagedescription: "imageDescription",
+            isbn: "ISBN",
+            issn: "ISSN",
+            joincondition: "joinCondition",
+            keywordtype: "keywordType",
+            languagevalue: "LanguageValue",
+            languagecodestandard: "LanguageCodeStandard",
+            lensdistortioninformationavailability:
+              "lensDistortionInformationAvailability",
+            licensename: "licenseName",
+            licenseurl: "licenseURL",
+            linenumber: "lineNumber",
+            literalcharacter: "literalCharacter",
+            literallayout: "literalLayout",
+            literaturecited: "literatureCited",
+            lowwavelength: "lowWaveLength",
+            machineprocessor: "machineProcessor",
+            maintenanceupdatefrequency: "maintenanceUpdateFrequency",
+            matrixtype: "matrixType",
+            maxexclusive: "maxExclusive",
+            maxinclusive: "maxInclusive",
+            maxlength: "maxLength",
+            maxrecordlength: "maxRecordLength",
+            maxvalues: "maxValues",
+            measurementscale: "measurementScale",
+            metadatalist: "metadataList",
+            methodstep: "methodStep",
+            minexclusive: "minExclusive",
+            mininclusive: "minInclusive",
+            minlength: "minLength",
+            minvalues: "minValues",
+            missingvaluecode: "missingValueCode",
+            moduledocs: "moduleDocs",
+            modulename: "moduleName",
+            moduledescription: "moduleDescription",
+            multiband: "multiBand",
+            multipliertosi: "multiplierToSI",
+            nonnumericdomain: "nonNumericDomain",
+            notnullconstraint: "notNullConstraint",
+            notplanned: "notPlanned",
+            numberofbands: "numberOfBands",
+            numbertype: "numberType",
+            numericdomain: "numericDomain",
+            numfooterlines: "numFooterLines",
+            numheaderlines: "numHeaderLines",
+            numberofrecords: "numberOfRecords",
+            numberofvolumes: "numberOfVolumes",
+            numphysicallinesperrecord: "numPhysicalLinesPerRecord",
+            objectname: "objectName",
+            oldvalue: "oldValue",
+            operatingsystem: "operatingSystem",
+            orderattributereference: "orderAttributeReference",
+            originalpublication: "originalPublication",
+            otherentity: "otherEntity",
+            othermaintenanceperiod: "otherMaintenancePeriod",
+            parameterdefinition: "parameterDefinition",
+            packageid: "packageId",
+            pagerange: "pageRange",
+            parentoccurences: "parentOccurences",
+            parentsi: "parentSI",
+            peakresponse: "peakResponse",
+            personalcommunication: "personalCommunication",
+            physicallinedelimiter: "physicalLineDelimiter",
+            pointinpixel: "pointInPixel",
+            preferredmembernode: "preferredMemberNode",
+            preprocessingtypecode: "preProcessingTypeCode",
+            primarykey: "primaryKey",
+            primemeridian: "primeMeridian",
+            proceduralstep: "proceduralStep",
+            programminglanguage: "programmingLanguage",
+            projcoordsys: "projCoordSys",
+            projectionlist: "projectionList",
+            propertyuri: "propertyURI",
+            pubdate: "pubDate",
+            pubplace: "pubPlace",
+            publicationplace: "publicationPlace",
+            quantitativeaccuracyreport: "quantitativeAccuracyReport",
+            quantitativeaccuracyvalue: "quantitativeAccuracyValue",
+            quantitativeaccuracymethod: "quantitativeAccuracyMethod",
+            quantitativeattributeaccuracyassessment:
+              "quantitativeAttributeAccuracyAssessment",
+            querystatement: "queryStatement",
+            quotecharacter: "quoteCharacter",
+            radiometricdataavailability: "radiometricDataAvailability",
+            rasterorigin: "rasterOrigin",
+            recommendedunits: "recommendedUnits",
+            recommendedusage: "recommendedUsage",
+            referencedkey: "referencedKey",
+            referencetype: "referenceType",
+            relatedentry: "relatedEntry",
+            relationshiptype: "relationshipType",
+            reportnumber: "reportNumber",
+            reprintedition: "reprintEdition",
+            researchproject: "researchProject",
+            researchtopic: "researchTopic",
+            recorddelimiter: "recordDelimiter",
+            referencepublication: "referencePublication",
+            revieweditem: "reviewedItem",
+            rowcolumnorientation: "rowColumnOrientation",
+            runtimememoryusage: "runtimeMemoryUsage",
+            samplingdescription: "samplingDescription",
+            scalefactor: "scaleFactor",
+            sequenceidentifier: "sequenceIdentifier",
+            semiaxismajor: "semiAxisMajor",
+            shortname: "shortName",
+            simpledelimited: "simpleDelimited",
+            spatialraster: "spatialRaster",
+            spatialreference: "spatialReference",
+            spatialvector: "spatialVector",
+            standalone: "standAlone",
+            standardunit: "standardUnit",
+            startcondition: "startCondition",
+            studyareadescription: "studyAreaDescription",
+            storagetype: "storageType",
+            studyextent: "studyExtent",
+            studytype: "studyType",
+            textdelimited: "textDelimited",
+            textdomain: "textDomain",
+            textfixed: "textFixed",
+            textformat: "textFormat",
+            topologylevel: "topologyLevel",
+            tonegradation: "toneGradation",
+            totaldigits: "totalDigits",
+            totalfigures: "totalFigures",
+            totalpages: "totalPages",
+            totaltables: "totalTables",
+            triangulationindicator: "triangulationIndicator",
+            typesystem: "typeSystem",
+            uniquekey: "uniqueKey",
+            unittype: "unitType",
+            unitlist: "unitList",
+            usagecitation: "usageCitation",
+            valueuri: "valueURI",
+            valueattributereference: "valueAttributeReference",
+            verticalaccuracy: "verticalAccuracy",
+            vertcoordsys: "vertCoordSys",
+            virtualmachine: "virtualMachine",
+            wavelengthunits: "waveLengthUnits",
+            whitespace: "whiteSpace",
+            xintercept: "xIntercept",
+            xcoordinate: "xCoordinate",
+            "xsi:schemalocation": "xsi:schemaLocation",
+            xslope: "xSlope",
+            ycoordinate: "yCoordinate",
+            yintercept: "yIntercept",
+            yslope: "ySlope",
+          },
+        );
+      },
 
-          //Add the authorization header and other AJAX settings
-           _.extend(options, MetacatUI.appUserModel.createAjaxSettings(), {dataType: "text"});
+      /**
+       * Fetch the EML from the MN object service
+       * @param {object} [options] - A set of options for this fetch()
+       * @property {boolean} [options.systemMetadataOnly=false] - If true, only the system metadata will be fetched.
+       * If false, the system metadata AND EML document will be fetched.
+       */
+      fetch: function (options) {
+        if (!options) var options = {};
 
-            // Merge the system metadata into the object first
-            _.extend(options, {merge: true});
-            this.fetchSystemMetadata(options);
+        //Add the authorization header and other AJAX settings
+        _.extend(options, MetacatUI.appUserModel.createAjaxSettings(), {
+          dataType: "text",
+        });
 
-            //If we are retrieving system metadata only, then exit now
-            if(options.systemMetadataOnly)
-              return;
+        // Merge the system metadata into the object first
+        _.extend(options, { merge: true });
+        this.fetchSystemMetadata(options);
 
-          //Call Backbone.Model.fetch to retrieve the info
-            return Backbone.Model.prototype.fetch.call(this, options);
+        //If we are retrieving system metadata only, then exit now
+        if (options.systemMetadataOnly) return;
 
-        },
+        //Call Backbone.Model.fetch to retrieve the info
+        return Backbone.Model.prototype.fetch.call(this, options);
+      },
 
-        /*
+      /*
          Deserialize an EML 2.1.1 XML document
         */
-        parse: function(response) {
-          // Save a reference to this model for use in setting the
-          // parentModel inside anonymous functions
-          var model = this;
-
-          //If the response is XML
-          if((typeof response == "string") && response.indexOf("<") == 0){
-            //Look for a system metadata tag and call DataONEObject parse instead
-            if(response.indexOf("systemMetadata>") > -1)
-              return DataONEObject.prototype.parse.call(this, response);
-
-            response = this.cleanUpXML(response);
-                response = this.dereference(response);
-            this.set("objectXML", response);
-            var emlElement = $($.parseHTML(response)).filter("eml\\:eml");
-          }
-
-          var datasetEl;
-          if(emlElement[0])
-            datasetEl = $(emlElement[0]).find("dataset");
-
-          if(!datasetEl || !datasetEl.length)
-            return {};
-
-          var emlParties = ["metadataprovider", "associatedparty", "creator", "contact", "publisher"],
-              emlDistribution = ["distribution"],
-              emlEntities = ["datatable", "otherentity", "spatialvector", "spatialraster", "storedprocedure", "view"],
-              emlText = ["abstract", "additionalinfo"],
-              emlMethods = ["methods"];
-
-          var nodes = datasetEl.children(),
-              modelJSON = {};
-
-          for(var i=0; i<nodes.length; i++){
-
-            var thisNode = nodes[i];
-            var convertedName = this.nodeNameMap()[thisNode.localName] || thisNode.localName;
-
-            //EML Party modules are stored in EMLParty models
-            if(_.contains(emlParties, thisNode.localName)){
-              if(thisNode.localName == "metadataprovider")
-                var attributeName = "metadataProvider";
-              else if(thisNode.localName == "associatedparty")
-                var attributeName = "associatedParty";
-              else
-                var attributeName = thisNode.localName;
-
-              if(typeof modelJSON[attributeName] == "undefined") modelJSON[attributeName] = [];
+      parse: function (response) {
+        // Save a reference to this model for use in setting the
+        // parentModel inside anonymous functions
+        var model = this;
+
+        //If the response is XML
+        if (typeof response == "string" && response.indexOf("<") == 0) {
+          //Look for a system metadata tag and call DataONEObject parse instead
+          if (response.indexOf("systemMetadata>") > -1)
+            return DataONEObject.prototype.parse.call(this, response);
+
+          response = this.cleanUpXML(response);
+          response = this.dereference(response);
+          this.set("objectXML", response);
+          var emlElement = $($.parseHTML(response)).filter("eml\\:eml");
+        }
 
-              modelJSON[attributeName].push(new EMLParty({
+        var datasetEl;
+        if (emlElement[0]) datasetEl = $(emlElement[0]).find("dataset");
+
+        if (!datasetEl || !datasetEl.length) return {};
+
+        var emlParties = [
+            "metadataprovider",
+            "associatedparty",
+            "creator",
+            "contact",
+            "publisher",
+          ],
+          emlDistribution = ["distribution"],
+          emlEntities = [
+            "datatable",
+            "otherentity",
+            "spatialvector",
+            "spatialraster",
+            "storedprocedure",
+            "view",
+          ],
+          emlText = ["abstract", "additionalinfo"],
+          emlMethods = ["methods"];
+
+        var nodes = datasetEl.children(),
+          modelJSON = {};
+
+        for (var i = 0; i < nodes.length; i++) {
+          var thisNode = nodes[i];
+          var convertedName =
+            this.nodeNameMap()[thisNode.localName] || thisNode.localName;
+
+          //EML Party modules are stored in EMLParty models
+          if (_.contains(emlParties, thisNode.localName)) {
+            if (thisNode.localName == "metadataprovider")
+              var attributeName = "metadataProvider";
+            else if (thisNode.localName == "associatedparty")
+              var attributeName = "associatedParty";
+            else var attributeName = thisNode.localName;
+
+            if (typeof modelJSON[attributeName] == "undefined")
+              modelJSON[attributeName] = [];
+
+            modelJSON[attributeName].push(
+              new EMLParty({
                 objectDOM: thisNode,
                 parentModel: model,
-                type: attributeName
-              }));
-            }
-            //EML Distribution modules are stored in EMLDistribution models
-            else if(_.contains(emlDistribution, thisNode.localName)) {
-              if(typeof modelJSON[thisNode.localName] == "undefined") modelJSON[thisNode.localName] = [];
-
-              modelJSON[thisNode.localName].push(new EMLDistribution({
-                objectDOM: thisNode,
-                parentModel: model
-              }, { parse: true }));
+                type: attributeName,
+              }),
+            );
+          }
+          //EML Distribution modules are stored in EMLDistribution models
+          else if (_.contains(emlDistribution, thisNode.localName)) {
+            if (typeof modelJSON[thisNode.localName] == "undefined")
+              modelJSON[thisNode.localName] = [];
+
+            modelJSON[thisNode.localName].push(
+              new EMLDistribution(
+                {
+                  objectDOM: thisNode,
+                  parentModel: model,
+                },
+                { parse: true },
+              ),
+            );
+          }
+          //The EML Project is stored in the EMLProject model
+          else if (thisNode.localName == "project") {
+            modelJSON.project = new EMLProject({
+              objectDOM: thisNode,
+              parentModel: model,
+            });
+          }
+          //EML Temporal, Taxonomic, and Geographic Coverage modules are stored in their own models
+          else if (thisNode.localName == "coverage") {
+            var temporal = $(thisNode).children("temporalcoverage"),
+              geo = $(thisNode).children("geographiccoverage"),
+              taxon = $(thisNode).children("taxonomiccoverage");
+
+            if (temporal.length) {
+              modelJSON.temporalCoverage = [];
+
+              _.each(temporal, function (t) {
+                modelJSON.temporalCoverage.push(
+                  new EMLTemporalCoverage({
+                    objectDOM: t,
+                    parentModel: model,
+                  }),
+                );
+              });
             }
-            //The EML Project is stored in the EMLProject model
-            else if(thisNode.localName == "project"){
-
-              modelJSON.project = new EMLProject({
-                objectDOM: thisNode,
-                parentModel: model
-               });
 
+            if (geo.length) {
+              modelJSON.geoCoverage = [];
+              _.each(geo, function (g) {
+                modelJSON.geoCoverage.push(
+                  new EMLGeoCoverage({
+                    objectDOM: g,
+                    parentModel: model,
+                  }),
+                );
+              });
             }
-            //EML Temporal, Taxonomic, and Geographic Coverage modules are stored in their own models
-            else if(thisNode.localName == "coverage"){
 
-              var temporal = $(thisNode).children("temporalcoverage"),
-                geo      = $(thisNode).children("geographiccoverage"),
-                taxon    = $(thisNode).children("taxonomiccoverage");
-
-              if(temporal.length){
-                modelJSON.temporalCoverage = [];
-
-                _.each(temporal, function(t){
-                  modelJSON.temporalCoverage.push(new EMLTemporalCoverage({
+            if (taxon.length) {
+              modelJSON.taxonCoverage = [];
+              _.each(taxon, function (t) {
+                modelJSON.taxonCoverage.push(
+                  new EMLTaxonCoverage({
                     objectDOM: t,
-                    parentModel: model
-                      }));
-                });
-              }
-
-              if(geo.length){
-                modelJSON.geoCoverage = [];
-                _.each(geo, function(g){
-                    modelJSON.geoCoverage.push(new EMLGeoCoverage({
-                      objectDOM: g,
-                      parentModel: model
-                      }));
-                });
-
-              }
-
-              if(taxon.length){
-                modelJSON.taxonCoverage = [];
-                _.each(taxon, function(t){
-                    modelJSON.taxonCoverage.push(new EMLTaxonCoverage({
-                      objectDOM: t,
-                      parentModel: model
-                        }));
-                });
-
-              }
-
+                    parentModel: model,
+                  }),
+                );
+              });
             }
-            //Parse EMLText modules
-            else if(_.contains(emlText, thisNode.localName)){
-              if(typeof modelJSON[convertedName] == "undefined") modelJSON[convertedName] = [];
-
-              modelJSON[convertedName].push(new EMLText({
-                  objectDOM: thisNode,
-                  parentModel: model
-                }));
+          }
+          //Parse EMLText modules
+          else if (_.contains(emlText, thisNode.localName)) {
+            if (typeof modelJSON[convertedName] == "undefined")
+              modelJSON[convertedName] = [];
 
-            }
-          else if(_.contains(emlMethods, thisNode.localName)) {
-            if(typeof modelJSON[thisNode.localName] === "undefined") modelJSON[thisNode.localName] = [];
+            modelJSON[convertedName].push(
+              new EMLText({
+                objectDOM: thisNode,
+                parentModel: model,
+              }),
+            );
+          } else if (_.contains(emlMethods, thisNode.localName)) {
+            if (typeof modelJSON[thisNode.localName] === "undefined")
+              modelJSON[thisNode.localName] = [];
 
-            modelJSON[thisNode.localName] =  new EMLMethods({
+            modelJSON[thisNode.localName] = new EMLMethods({
               objectDOM: thisNode,
-              parentModel: model
+              parentModel: model,
             });
-
           }
           //Parse keywords
-          else if(thisNode.localName == "keywordset"){
+          else if (thisNode.localName == "keywordset") {
             //Start an array of keyword sets
-            if(typeof modelJSON["keywordSets"] == "undefined") modelJSON["keywordSets"] = [];
+            if (typeof modelJSON["keywordSets"] == "undefined")
+              modelJSON["keywordSets"] = [];
 
-            modelJSON["keywordSets"].push(new EMLKeywordSet({
-              objectDOM: thisNode,
-              parentModel: model
-            }));
+            modelJSON["keywordSets"].push(
+              new EMLKeywordSet({
+                objectDOM: thisNode,
+                parentModel: model,
+              }),
+            );
           }
           //Parse intellectual rights
-          else if(thisNode.localName == "intellectualrights"){
+          else if (thisNode.localName == "intellectualrights") {
             var value = "";
 
-            if($(thisNode).children("para").length == 1)
+            if ($(thisNode).children("para").length == 1)
               value = $(thisNode).children("para").first().text().trim();
-            else
-              $(thisNode).text().trim();
+            else $(thisNode).text().trim();
 
             //If the value is one of our pre-defined options, then add it to the model
             //if(_.contains(this.get("intellRightsOptions"), value))
             modelJSON["intellectualRights"] = value;
-
           }
           //Parse Entities
-          else if(_.contains(emlEntities, thisNode.localName)){
-
+          else if (_.contains(emlEntities, thisNode.localName)) {
             //Start an array of Entities
-            if(typeof modelJSON["entities"] == "undefined")
+            if (typeof modelJSON["entities"] == "undefined")
               modelJSON["entities"] = [];
 
             //Create the model
             var entityModel;
-            if(thisNode.localName == "otherentity"){
-              entityModel = new EMLOtherEntity({
+            if (thisNode.localName == "otherentity") {
+              entityModel = new EMLOtherEntity(
+                {
                   objectDOM: thisNode,
-                  parentModel: model
-                }, {
-                  parse: true
-                });
-            } else if ( thisNode.localName == "datatable") {
-                entityModel = new EMLDataTable({
-                    objectDOM: thisNode,
-                    parentModel: model
-                }, {
-                    parse: true
-                });
-            }
-            else {
-              entityModel = new EMLEntity({
+                  parentModel: model,
+                },
+                {
+                  parse: true,
+                },
+              );
+            } else if (thisNode.localName == "datatable") {
+              entityModel = new EMLDataTable(
+                {
+                  objectDOM: thisNode,
+                  parentModel: model,
+                },
+                {
+                  parse: true,
+                },
+              );
+            } else {
+              entityModel = new EMLEntity(
+                {
                   objectDOM: thisNode,
                   parentModel: model,
                   entityType: "application/octet-stream",
-                  type: thisNode.localName
-                }, {
-                  parse: true
-                });
+                  type: thisNode.localName,
+                },
+                {
+                  parse: true,
+                },
+              );
             }
 
             modelJSON["entities"].push(entityModel);
           }
           //Parse dataset-level annotations
           else if (thisNode.localName === "annotation") {
-            if( !modelJSON["annotations"] ) {
+            if (!modelJSON["annotations"]) {
               modelJSON["annotations"] = new EMLAnnotations();
             }
 
-            var annotationModel = new EMLAnnotation({
-              objectDOM: thisNode
-            }, { parse: true });
+            var annotationModel = new EMLAnnotation(
+              {
+                objectDOM: thisNode,
+              },
+              { parse: true },
+            );
 
             modelJSON["annotations"].add(annotationModel);
-          }
-          else{
+          } else {
             //Is this a multi-valued field in EML?
-            if(Array.isArray(this.get(convertedName))){
+            if (Array.isArray(this.get(convertedName))) {
               //If we already have a value for this field, then add this value to the array
-              if(Array.isArray(modelJSON[convertedName]))
+              if (Array.isArray(modelJSON[convertedName]))
                 modelJSON[convertedName].push(this.toJson(thisNode));
               //If it's the first value for this field, then create a new array
-              else
-                modelJSON[convertedName] = [this.toJson(thisNode)];
-            }
-            else
-              modelJSON[convertedName] = this.toJson(thisNode);
+              else modelJSON[convertedName] = [this.toJson(thisNode)];
+            } else modelJSON[convertedName] = this.toJson(thisNode);
           }
-
         }
 
         return modelJSON;
@@ -733,12 +787,12 @@ 

Source: src/js/models/metadata/eml211/EML211.js

* Retireves the model attributes and serializes into EML XML, to produce the new or modified EML document. * Returns the EML XML as a string. */ - serialize: function(){ + serialize: function () { //Get the EML document - var xmlString = this.get("objectXML"), - html = $.parseHTML(xmlString), - eml = $(html).filter("eml\\:eml"), - datasetNode = $(eml).find("dataset"); + var xmlString = this.get("objectXML"), + html = $.parseHTML(xmlString), + eml = $(html).filter("eml\\:eml"), + datasetNode = $(eml).find("dataset"); //Update the packageId on the eml node with the EML id $(eml).attr("packageId", this.get("id")); @@ -749,14 +803,18 @@

Source: src/js/models/metadata/eml211/EML211.js

} // Set schema version - $(eml).attr("xmlns:eml", + $(eml).attr( + "xmlns:eml", MetacatUI.appModel.get("editorSerializationFormat") || - "https://eml.ecoinformatics.org/eml-2.2.0"); + "https://eml.ecoinformatics.org/eml-2.2.0", + ); // Set formatID - this.set("formatId", + this.set( + "formatId", MetacatUI.appModel.get("editorSerializationFormat") || - "https://eml.ecoinformatics.org/eml-2.2.0"); + "https://eml.ecoinformatics.org/eml-2.2.0", + ); // Ensure xsi:schemaLocation has a value for the current format eml = this.setSchemaLocation(eml); @@ -765,510 +823,553 @@

Source: src/js/models/metadata/eml211/EML211.js

//Serialize the basic text fields var basicText = ["alternateIdentifier", "title"]; - _.each(basicText, function(fieldName){ - var basicTextValues = this.get(fieldName); - - if(!Array.isArray(basicTextValues)) - basicTextValues = [basicTextValues]; + _.each( + basicText, + function (fieldName) { + var basicTextValues = this.get(fieldName); + + if (!Array.isArray(basicTextValues)) + basicTextValues = [basicTextValues]; + + // Remove existing nodes + datasetNode.children(fieldName.toLowerCase()).remove(); + + // Create new nodes + var nodes = _.map(basicTextValues, function (value) { + if (value) { + var node = document.createElement(fieldName.toLowerCase()); + $(node).text(value); + return node; + } else { + return ""; + } + }); - // Remove existing nodes - datasetNode.children(fieldName.toLowerCase()).remove(); + var insertAfter = this.getEMLPosition(eml, fieldName.toLowerCase()); - // Create new nodes - var nodes = _.map(basicTextValues, function(value) { + if (insertAfter) { + insertAfter.after(nodes); + } else { + datasetNode.prepend(nodes); + } + }, + this, + ); - if(value){ + // Serialize pubDate + // This one is special because it has a default behavior, unlike + // the others: When no pubDate is set, it should be set to + // the current year + var pubDate = this.get("pubDate"); - var node = document.createElement(fieldName.toLowerCase()); - $(node).text(value); - return node; + datasetNode.find("pubdate").remove(); - } - else{ - return ""; - } - }); + if (pubDate != null && pubDate.length > 0) { + var pubDateEl = document.createElement("pubdate"); - var insertAfter = this.getEMLPosition(eml, fieldName.toLowerCase()); + $(pubDateEl).text(pubDate); - if(insertAfter){ - insertAfter.after(nodes); - } - else{ - datasetNode.prepend(nodes); + this.getEMLPosition(eml, "pubdate").after(pubDateEl); } - }, this); - // Serialize pubDate - // This one is special because it has a default behavior, unlike - // the others: When no pubDate is set, it should be set to - // the current year - var pubDate = this.get('pubDate'); - - datasetNode.find('pubdate').remove(); - - if (pubDate != null && pubDate.length > 0) { - - var pubDateEl = document.createElement('pubdate'); - - $(pubDateEl).text(pubDate); - - this.getEMLPosition(eml, 'pubdate').after(pubDateEl); - } + // Serialize the parts of EML that are eml-text modules + var textFields = ["abstract", "additionalInfo"]; + + _.each( + textFields, + function (field) { + var fieldName = this.nodeNameMap()[field] || field; + + // Get the EMLText model + var emlTextModels = Array.isArray(this.get(field)) + ? this.get(field) + : [this.get(field)]; + if (!emlTextModels.length) return; + + // Get the node from the EML doc + var nodes = datasetNode.find(fieldName); + + // Update the DOMs for each model + _.each( + emlTextModels, + function (thisTextModel, i) { + //Don't serialize falsey values + if (!thisTextModel) return; + + var node; + + //Get the existing node or create a new one + if (nodes.length < i + 1) { + node = document.createElement(fieldName); + this.getEMLPosition(eml, fieldName).after(node); + } else { + node = nodes[i]; + } - // Serialize the parts of EML that are eml-text modules - var textFields = ["abstract", "additionalInfo"]; + $(node).html($(thisTextModel.updateDOM()).html()); + }, + this, + ); - _.each(textFields, function(field){ + // Remove the extra nodes + this.removeExtraNodes(nodes, emlTextModels); + }, + this, + ); + + //Create a <coverage> XML node if there isn't one + if (datasetNode.children("coverage").length === 0) { + var coverageNode = $(document.createElement("coverage")), + coveragePosition = this.getEMLPosition(eml, "coverage"); + + if (coveragePosition) coveragePosition.after(coverageNode); + else datasetNode.append(coverageNode); + } else { + var coverageNode = datasetNode.children("coverage").first(); + } - var fieldName = this.nodeNameMap()[field] || field; + //Serialize the geographic coverage + if ( + typeof this.get("geoCoverage") !== "undefined" && + this.get("geoCoverage").length > 0 + ) { + // Don't serialize if geoCoverage is invalid + var validCoverages = _.filter( + this.get("geoCoverage"), + function (cov) { + return cov.isValid(); + }, + ); - // Get the EMLText model - var emlTextModels = Array.isArray(this.get(field)) ? this.get(field) : [this.get(field)]; - if( ! emlTextModels.length ) return; + //Get the existing geo coverage nodes from the EML + var existingGeoCov = datasetNode.find("geographiccoverage"); - // Get the node from the EML doc - var nodes = datasetNode.find(fieldName); + //Update the DOM of each model + _.each( + validCoverages, + function (cov, position) { + //Update the existing node if it exists + if (existingGeoCov.length - 1 >= position) { + $(existingGeoCov[position]).replaceWith(cov.updateDOM()); + } + //Or, append new nodes + else { + var insertAfter = existingGeoCov.length + ? datasetNode.find("geographiccoverage").last() + : null; + + if (insertAfter) insertAfter.after(cov.updateDOM()); + else coverageNode.append(cov.updateDOM()); + } + }, + this, + ); - // Update the DOMs for each model - _.each(emlTextModels, function(thisTextModel, i){ - //Don't serialize falsey values - if(!thisTextModel) return; + //Remove existing taxon coverage nodes that don't have an accompanying model + this.removeExtraNodes( + datasetNode.find("geographiccoverage"), + validCoverages, + ); + } else { + //If there are no geographic coverages, remove the nodes + coverageNode.children("geographiccoverage").remove(); + } - var node; + //Serialize the taxonomic coverage + if ( + typeof this.get("taxonCoverage") !== "undefined" && + this.get("taxonCoverage").length > 0 + ) { + // Group the taxonomic coverage models into empty and non-empty + var sortedTaxonModels = _.groupBy( + this.get("taxonCoverage"), + function (t) { + if (_.flatten(t.get("taxonomicClassification")).length > 0) { + return "notEmpty"; + } else { + return "empty"; + } + }, + ); - //Get the existing node or create a new one - if(nodes.length < i+1){ - node = document.createElement(fieldName); - this.getEMLPosition(eml, fieldName).after(node); + //Get the existing taxon coverage nodes from the EML + var existingTaxonCov = coverageNode.children("taxonomiccoverage"); + + //Iterate over each taxon coverage and update it's DOM + if ( + sortedTaxonModels["notEmpty"] && + sortedTaxonModels["notEmpty"].length > 0 + ) { + //Update the DOM of each model + _.each( + sortedTaxonModels["notEmpty"], + function (taxonCoverage, position) { + //Update the existing taxonCoverage node if it exists + if (existingTaxonCov.length - 1 >= position) { + $(existingTaxonCov[position]).replaceWith( + taxonCoverage.updateDOM(), + ); + } + //Or, append new nodes + else { + coverageNode.append(taxonCoverage.updateDOM()); + } + }, + ); + //Remove existing taxon coverage nodes that don't have an accompanying model + this.removeExtraNodes(existingTaxonCov, this.get("taxonCoverage")); } - else { - node = nodes[i]; + //If all the taxon coverages are empty, remove the parent taxonomicCoverage node + else if ( + !sortedTaxonModels["notEmpty"] || + sortedTaxonModels["notEmpty"].length == 0 + ) { + existingTaxonCov.remove(); } + } - $(node).html( $(thisTextModel.updateDOM() ).html()); - - }, this); - - // Remove the extra nodes - this.removeExtraNodes(nodes, emlTextModels); - - }, this); - - //Create a <coverage> XML node if there isn't one - if( datasetNode.children('coverage').length === 0 ) { - var coverageNode = $(document.createElement('coverage')), - coveragePosition = this.getEMLPosition(eml, 'coverage'); - - if(coveragePosition) - coveragePosition.after(coverageNode); - else - datasetNode.append(coverageNode); - } - else{ - var coverageNode = datasetNode.children("coverage").first(); - } - - //Serialize the geographic coverage - if ( typeof this.get('geoCoverage') !== 'undefined' && this.get('geoCoverage').length > 0) { - - // Don't serialize if geoCoverage is invalid - var validCoverages = _.filter(this.get('geoCoverage'), function(cov) { - return cov.isValid(); - }); - - //Get the existing geo coverage nodes from the EML - var existingGeoCov = datasetNode.find("geographiccoverage"); + //Serialize the temporal coverage + var existingTemporalCoverages = datasetNode.find("temporalcoverage"); //Update the DOM of each model - _.each(validCoverages, function(cov, position){ - - //Update the existing node if it exists - if(existingGeoCov.length-1 >= position){ - $(existingGeoCov[position]).replaceWith(cov.updateDOM()); - } - //Or, append new nodes - else{ - var insertAfter = existingGeoCov.length? datasetNode.find("geographiccoverage").last() : null; - - if(insertAfter) - insertAfter.after(cov.updateDOM()); - else - coverageNode.append(cov.updateDOM()); - } - }, this); - - //Remove existing taxon coverage nodes that don't have an accompanying model - this.removeExtraNodes(datasetNode.find("geographiccoverage"), validCoverages); - } - else{ - //If there are no geographic coverages, remove the nodes - coverageNode.children("geographiccoverage").remove(); - } - - //Serialize the taxonomic coverage - if ( typeof this.get('taxonCoverage') !== 'undefined' && this.get('taxonCoverage').length > 0) { - - // Group the taxonomic coverage models into empty and non-empty - var sortedTaxonModels = _.groupBy(this.get('taxonCoverage'), function(t) { - if( _.flatten(t.get('taxonomicClassification')).length > 0 ){ - return "notEmpty"; - } - else{ - return "empty"; - } - }); - - //Get the existing taxon coverage nodes from the EML - var existingTaxonCov = coverageNode.children("taxonomiccoverage"); - - //Iterate over each taxon coverage and update it's DOM - if(sortedTaxonModels["notEmpty"] && sortedTaxonModels["notEmpty"].length > 0) { - - //Update the DOM of each model - _.each(sortedTaxonModels["notEmpty"], function(taxonCoverage, position){ - - //Update the existing taxonCoverage node if it exists - if(existingTaxonCov.length-1 >= position){ - $(existingTaxonCov[position]).replaceWith(taxonCoverage.updateDOM()); + _.each( + this.get("temporalCoverage"), + function (temporalCoverage, position) { + //Update the existing temporalCoverage node if it exists + if (existingTemporalCoverages.length - 1 >= position) { + $(existingTemporalCoverages[position]).replaceWith( + temporalCoverage.updateDOM(), + ); } //Or, append new nodes - else{ - coverageNode.append(taxonCoverage.updateDOM()); + else { + coverageNode.append(temporalCoverage.updateDOM()); } - }); + }, + ); - //Remove existing taxon coverage nodes that don't have an accompanying model - this.removeExtraNodes(existingTaxonCov, this.get("taxonCoverage")); - - } - //If all the taxon coverages are empty, remove the parent taxonomicCoverage node - else if( !sortedTaxonModels["notEmpty"] || sortedTaxonModels["notEmpty"].length == 0 ){ - existingTaxonCov.remove(); + //Remove existing taxon coverage nodes that don't have an accompanying model + this.removeExtraNodes( + existingTemporalCoverages, + this.get("temporalCoverage"), + ); + + //Remove the temporal coverage if it is empty + if (!coverageNode.children("temporalcoverage").children().length) { + coverageNode.children("temporalcoverage").remove(); } - } - - //Serialize the temporal coverage - var existingTemporalCoverages = datasetNode.find("temporalcoverage"); - - //Update the DOM of each model - _.each(this.get("temporalCoverage"), function(temporalCoverage, position){ - - //Update the existing temporalCoverage node if it exists - if(existingTemporalCoverages.length-1 >= position){ - $(existingTemporalCoverages[position]).replaceWith(temporalCoverage.updateDOM()); + //Remove the <coverage> node if it's empty + if (coverageNode.children().length == 0) { + coverageNode.remove(); } - //Or, append new nodes - else{ - coverageNode.append(temporalCoverage.updateDOM()); - } - }); - - //Remove existing taxon coverage nodes that don't have an accompanying model - this.removeExtraNodes(existingTemporalCoverages, this.get("temporalCoverage")); - - //Remove the temporal coverage if it is empty - if( !coverageNode.children("temporalcoverage").children().length ){ - coverageNode.children("temporalcoverage").remove(); - } - //Remove the <coverage> node if it's empty - if(coverageNode.children().length == 0){ - coverageNode.remove(); - } + // Dataset-level annotations + datasetNode.children("annotation").remove(); - // Dataset-level annotations - datasetNode.children("annotation").remove(); - - if( this.get("annotations") ){ - this.get("annotations").each(function(annotation) { - if (annotation.isEmpty()) { - return; - } + if (this.get("annotations")) { + this.get("annotations").each(function (annotation) { + if (annotation.isEmpty()) { + return; + } - var after = this.getEMLPosition(eml, "annotation"); + var after = this.getEMLPosition(eml, "annotation"); - $(after).after(annotation.updateDOM()); - }, this); + $(after).after(annotation.updateDOM()); + }, this); - //Since there is at least one annotation, the dataset node needs to have an id attribute. - datasetNode.attr("id", this.getUniqueEntityId(this)); - } + //Since there is at least one annotation, the dataset node needs to have an id attribute. + datasetNode.attr("id", this.getUniqueEntityId(this)); + } - //If there is no creator, create one from the user - if(!this.get("creator").length){ - var party = new EMLParty({ parentModel: this, type: "creator" }); + //If there is no creator, create one from the user + if (!this.get("creator").length) { + var party = new EMLParty({ parentModel: this, type: "creator" }); - party.createFromUser(); + party.createFromUser(); - this.set("creator", [party]); - } + this.set("creator", [party]); + } - //Serialize the creators - this.serializeParties(eml, "creator"); + //Serialize the creators + this.serializeParties(eml, "creator"); - //Serialize the metadata providers - this.serializeParties(eml, "metadataProvider"); + //Serialize the metadata providers + this.serializeParties(eml, "metadataProvider"); - //Serialize the associated parties - this.serializeParties(eml, "associatedParty"); + //Serialize the associated parties + this.serializeParties(eml, "associatedParty"); - //Serialize the contacts - this.serializeParties(eml, "contact"); + //Serialize the contacts + this.serializeParties(eml, "contact"); - //Serialize the publishers - this.serializeParties(eml, "publisher"); + //Serialize the publishers + this.serializeParties(eml, "publisher"); - // Serialize methods - if(this.get('methods')) { + // Serialize methods + if (this.get("methods")) { + //If the methods model is empty, remove it from the EML + if (this.get("methods").isEmpty()) + datasetNode.find("methods").remove(); + else { + //Serialize the methods model + var methodsEl = this.get("methods").updateDOM(); - //If the methods model is empty, remove it from the EML - if( this.get("methods").isEmpty() ) - datasetNode.find("methods").remove(); - else{ + //If the methodsEl is an empty string or other falsey value, then remove the methods node + if (!methodsEl || !$(methodsEl).children().length) { + datasetNode.find("methods").remove(); + } else { + //Add the <methods> node to the EML + datasetNode.find("methods").detach(); - //Serialize the methods model - var methodsEl = this.get('methods').updateDOM(); + var insertAfter = this.getEMLPosition(eml, "methods"); - //If the methodsEl is an empty string or other falsey value, then remove the methods node - if( !methodsEl || !$(methodsEl).children().length ){ + if (insertAfter) insertAfter.after(methodsEl); + else datasetNode.append(methodsEl); + } + } + } + //If there are no methods, then remove the methods nodes + else { + if (datasetNode.find("methods").length > 0) { datasetNode.find("methods").remove(); } - else{ - - //Add the <methods> node to the EML - datasetNode.find("methods").detach(); + } - var insertAfter = this.getEMLPosition(eml, "methods"); + //Serialize the keywords + this.serializeKeywords(eml, "keywordSets"); - if(insertAfter) - insertAfter.after(methodsEl); - else - datasetNode.append(methodsEl); + //Serialize the intellectual rights + if (this.get("intellectualRights")) { + if (datasetNode.find("intellectualRights").length) + datasetNode + .find("intellectualRights") + .html("<para>" + this.get("intellectualRights") + "</para>"); + else { + this.getEMLPosition(eml, "intellectualrights").after( + $(document.createElement("intellectualRights")).html( + "<para>" + this.get("intellectualRights") + "</para>", + ), + ); } } - } - //If there are no methods, then remove the methods nodes - else{ - if( datasetNode.find("methods").length > 0 ){ - datasetNode.find("methods").remove(); + // Serialize the distribution + const distributions = this.get("distribution"); + if (distributions && distributions.length > 0) { + // Remove existing nodes + datasetNode.children("distribution").remove(); + // Get the updated DOMs + const distributionDOMs = distributions.map((d) => d.updateDOM()); + // Insert the updated DOMs in their correct positions + distributionDOMs.forEach((dom, i) => { + const insertAfter = this.getEMLPosition(eml, "distribution"); + if (insertAfter) { + insertAfter.after(dom); + } else { + datasetNode.append(dom); + } + }); } - } - - //Serialize the keywords - this.serializeKeywords(eml, "keywordSets"); - - //Serialize the intellectual rights - if(this.get("intellectualRights")){ - if(datasetNode.find("intellectualRights").length) - datasetNode.find("intellectualRights").html("<para>" + this.get("intellectualRights") + "</para>") - else{ - - this.getEMLPosition(eml, "intellectualrights").after( - $(document.createElement("intellectualRights")) - .html("<para>" + this.get("intellectualRights") + "</para>")); + //Detach the project elements from the DOM + if (datasetNode.find("project").length) { + datasetNode.find("project").detach(); } - } - - // Serialize the distribution - const distributions = this.get('distribution'); - if (distributions && distributions.length > 0) { - // Remove existing nodes - datasetNode.children('distribution').remove(); - // Get the updated DOMs - const distributionDOMs = distributions.map(d => d.updateDOM()); - // Insert the updated DOMs in their correct positions - distributionDOMs.forEach((dom, i) => { - const insertAfter = this.getEMLPosition(eml, 'distribution'); - if (insertAfter) { - insertAfter.after(dom); - } else { - datasetNode.append(dom); - } - }); - } - - //Detach the project elements from the DOM - if(datasetNode.find("project").length){ - - datasetNode.find("project").detach(); - - } - - //If there is an EMLProject, update its DOM - if(this.get("project")){ - - this.getEMLPosition(eml, "project").after(this.get("project").updateDOM()); - } - - //Get the existing taxon coverage nodes from the EML - var existingEntities = datasetNode.find("otherEntity, dataTable, spatialRaster, spatialVector, storedProcedure, view"); - - //Serialize the entities - _.each(this.get("entities"), function(entity, position) { - - //Update the existing node if it exists - if(existingEntities.length - 1 >= position) { - //Remove the entity from the EML - $(existingEntities[position]).detach(); - //Insert it into the correct position - this.getEMLPosition(eml, entity.get("type").toLowerCase()).after(entity.updateDOM()); } - //Or, append new nodes - else { - //Inser the entity into the correct position - this.getEMLPosition(eml, entity.get("type").toLowerCase()).after(entity.updateDOM()); + //If there is an EMLProject, update its DOM + if (this.get("project")) { + this.getEMLPosition(eml, "project").after( + this.get("project").updateDOM(), + ); } - }, this); - - //Remove extra entities that have been removed - var numExtraEntities = existingEntities.length - this.get("entities").length; - for( var i = (existingEntities.length - numExtraEntities); i<existingEntities.length; i++){ - $(existingEntities)[i].remove(); - } - - //Do a final check to make sure there are no duplicate ids in the EML - var elementsWithIDs = $(eml).find("[id]"), - //Get an array of all the ids in this EML doc - allIDs = _.map(elementsWithIDs, function(el){ return $(el).attr("id") }); - - //If there is at least one id in the EML... - if(allIDs && allIDs.length){ - //Boil the array down to just the unique values - var uniqueIDs = _.uniq(allIDs); - - //If the unique array is shorter than the array of all ids, - // then there is a duplicate somewhere - if(uniqueIDs.length < allIDs.length){ - - //For each element in the EML that has an id, - _.each(elementsWithIDs, function(el){ - - //Get the id for this element - var id = $(el).attr("id"); - - //If there is more than one element in the EML with this id, - if( $(eml).find("[id='" + id + "']").length > 1 ){ - //And if it is not a unit node, which we don't want to change, - if( !$(el).is("unit") ) - //Then change the id attribute to a random uuid - $(el).attr("id", "urn-uuid-" + uuid.v4()); + //Get the existing taxon coverage nodes from the EML + var existingEntities = datasetNode.find( + "otherEntity, dataTable, spatialRaster, spatialVector, storedProcedure, view", + ); + + //Serialize the entities + _.each( + this.get("entities"), + function (entity, position) { + //Update the existing node if it exists + if (existingEntities.length - 1 >= position) { + //Remove the entity from the EML + $(existingEntities[position]).detach(); + //Insert it into the correct position + this.getEMLPosition(eml, entity.get("type").toLowerCase()).after( + entity.updateDOM(), + ); + } + //Or, append new nodes + else { + //Inser the entity into the correct position + this.getEMLPosition(eml, entity.get("type").toLowerCase()).after( + entity.updateDOM(), + ); } + }, + this, + ); + + //Remove extra entities that have been removed + var numExtraEntities = + existingEntities.length - this.get("entities").length; + for ( + var i = existingEntities.length - numExtraEntities; + i < existingEntities.length; + i++ + ) { + $(existingEntities)[i].remove(); + } + //Do a final check to make sure there are no duplicate ids in the EML + var elementsWithIDs = $(eml).find("[id]"), + //Get an array of all the ids in this EML doc + allIDs = _.map(elementsWithIDs, function (el) { + return $(el).attr("id"); }); + //If there is at least one id in the EML... + if (allIDs && allIDs.length) { + //Boil the array down to just the unique values + var uniqueIDs = _.uniq(allIDs); + + //If the unique array is shorter than the array of all ids, + // then there is a duplicate somewhere + if (uniqueIDs.length < allIDs.length) { + //For each element in the EML that has an id, + _.each(elementsWithIDs, function (el) { + //Get the id for this element + var id = $(el).attr("id"); + + //If there is more than one element in the EML with this id, + if ($(eml).find("[id='" + id + "']").length > 1) { + //And if it is not a unit node, which we don't want to change, + if (!$(el).is("unit")) + //Then change the id attribute to a random uuid + $(el).attr("id", "urn-uuid-" + uuid.v4()); + } + }); + } } - } - - //Camel-case the XML - var emlString = ""; - _.each(html, function(rootEMLNode){ emlString += this.formatXML(rootEMLNode); }, this); - - return emlString; - }, - - /* - * Given an EML DOM and party type, this function updated and/or adds the EMLParties to the EML - */ - serializeParties: function(eml, type){ - - //Remove the nodes from the EML for this party type - $(eml).children("dataset").children(type.toLowerCase()).remove(); - - //Serialize each party of this type - _.each(this.get(type), function(party, i){ - - //Get the last node of this type to insert after - var insertAfter = $(eml).children("dataset").children(type.toLowerCase()).last(); - - //If there isn't a node found, find the EML position to insert after - if( !insertAfter.length ) { - insertAfter = this.getEMLPosition(eml, type); - } - //Update the DOM of the EMLParty - var emlPartyDOM = party.updateDOM(); + //Camel-case the XML + var emlString = ""; + _.each( + html, + function (rootEMLNode) { + emlString += this.formatXML(rootEMLNode); + }, + this, + ); + + return emlString; + }, - //Make sure we don't insert empty EMLParty nodes into the EML - if( $(emlPartyDOM).children().length ){ - //Insert the party DOM at the insert position - if ( insertAfter && insertAfter.length ) - insertAfter.after(emlPartyDOM); - //If an insert position still hasn't been found, then just append to the dataset node - else - $(eml).find("dataset").append(emlPartyDOM); + /* + * Given an EML DOM and party type, this function updated and/or adds the EMLParties to the EML + */ + serializeParties: function (eml, type) { + //Remove the nodes from the EML for this party type + $(eml).children("dataset").children(type.toLowerCase()).remove(); + + //Serialize each party of this type + _.each( + this.get(type), + function (party, i) { + //Get the last node of this type to insert after + var insertAfter = $(eml) + .children("dataset") + .children(type.toLowerCase()) + .last(); + + //If there isn't a node found, find the EML position to insert after + if (!insertAfter.length) { + insertAfter = this.getEMLPosition(eml, type); } - }, this); - - //Create a certain parties from the current app user if none is given - if(type == "contact" && !this.get("contact").length){ - //Get the creators - var creators = this.get("creator"), - contacts = []; + //Update the DOM of the EMLParty + var emlPartyDOM = party.updateDOM(); - _.each(creators, function(creator){ - //Clone the creator model and add it to the contacts array - var newModel = new EMLParty({ parentModel: this }); - newModel.set(creator.toJSON()); - newModel.set("type", type); - - contacts.push(newModel); - }, this); - - this.set(type, contacts); + //Make sure we don't insert empty EMLParty nodes into the EML + if ($(emlPartyDOM).children().length) { + //Insert the party DOM at the insert position + if (insertAfter && insertAfter.length) + insertAfter.after(emlPartyDOM); + //If an insert position still hasn't been found, then just append to the dataset node + else $(eml).find("dataset").append(emlPartyDOM); + } + }, + this, + ); + + //Create a certain parties from the current app user if none is given + if (type == "contact" && !this.get("contact").length) { + //Get the creators + var creators = this.get("creator"), + contacts = []; + + _.each( + creators, + function (creator) { + //Clone the creator model and add it to the contacts array + var newModel = new EMLParty({ parentModel: this }); + newModel.set(creator.toJSON()); + newModel.set("type", type); + + contacts.push(newModel); + }, + this, + ); - //Call this function again to serialize the new models - this.serializeParties(eml, type); - } - }, + this.set(type, contacts); + //Call this function again to serialize the new models + this.serializeParties(eml, type); + } + }, - serializeKeywords: function(eml) { + serializeKeywords: function (eml) { // Remove all existing keywordSets before appending - $(eml).find('dataset').find('keywordset').remove(); + $(eml).find("dataset").find("keywordset").remove(); - if (this.get('keywordSets').length == 0) return; + if (this.get("keywordSets").length == 0) return; // Create the new keywordSets nodes - var nodes = _.map(this.get('keywordSets'), function(kwd) { + var nodes = _.map(this.get("keywordSets"), function (kwd) { return kwd.updateDOM(); }); - this.getEMLPosition(eml, "keywordset").after(nodes); + this.getEMLPosition(eml, "keywordset").after(nodes); }, /* * Remoes nodes from the EML that do not have an accompanying model * (Were probably removed from the EML by the user during editing) */ - removeExtraNodes: function(nodes, models){ + removeExtraNodes: function (nodes, models) { // Remove the extra nodes - var extraNodes = nodes.length - models.length; - if(extraNodes > 0){ - for(var i = models.length; i < nodes.length; i++){ - $(nodes[i]).remove(); - } - } + var extraNodes = nodes.length - models.length; + if (extraNodes > 0) { + for (var i = models.length; i < nodes.length; i++) { + $(nodes[i]).remove(); + } + } }, /* * Saves the EML document to the server using the DataONE API */ - save: function(attributes, options){ - + save: function (attributes, options) { //Validate before we try anything else - if(!this.isValid()){ + if (!this.isValid()) { this.trigger("invalid"); this.trigger("cancelSave"); return false; - } - else{ + } else { this.trigger("valid"); } @@ -1281,34 +1382,34 @@

Source: src/js/models/metadata/eml211/EML211.js

this.set("draftSaved", false); //Create the creator from the current user if none is provided - if(!this.get("creator").length){ - var party = new EMLParty({ parentModel: this, type: "creator" }); + if (!this.get("creator").length) { + var party = new EMLParty({ parentModel: this, type: "creator" }); - party.createFromUser(); + party.createFromUser(); - this.set("creator", [party]); + this.set("creator", [party]); } //Create the contact from the current user if none is provided - if(!this.get("contact").length){ - var party = new EMLParty({ parentModel: this, type: "contact" }); + if (!this.get("contact").length) { + var party = new EMLParty({ parentModel: this, type: "contact" }); - party.createFromUser(); + party.createFromUser(); - this.set("contact", [party]); + this.set("contact", [party]); } //If this is an existing object and there is no system metadata, retrieve it - if(!this.isNew() && !this.get("sysMetaXML")){ + if (!this.isNew() && !this.get("sysMetaXML")) { var model = this; //When the system metadata is fetched, try saving again var fetchOptions = { - success: function(response){ - model.set(DataONEObject.prototype.parse.call(model, response)); - model.save(attributes, options); - } - } + success: function (response) { + model.set(DataONEObject.prototype.parse.call(model, response)); + model.save(attributes, options); + }, + }; //Fetch the system metadata now this.fetchSystemMetadata(fetchOptions); @@ -1316,185 +1417,198 @@

Source: src/js/models/metadata/eml211/EML211.js

return; } - //Create a FormData object to send data with our XHR - var formData = new FormData(); - - try{ - - //Add the identifier to the XHR data - if(this.isNew()){ - formData.append("pid", this.get("id")); - } - else{ - //Create a new ID - this.updateID(); - - //Add the ids to the form data - formData.append("newPid", this.get("id")); - formData.append("pid", this.get("oldPid")); - } - - //Serialize the EML XML - var xml = this.serialize(); - var xmlBlob = new Blob([xml], {type : 'application/xml'}); - - //Get the size of the new EML XML - this.set("size", xmlBlob.size); - - //Get the new checksum of the EML XML - var checksum = md5(xml); - this.set("checksum", checksum); - this.set("checksumAlgorithm", "MD5"); - - //Create the system metadata XML - var sysMetaXML = this.serializeSysMeta(); - - //Send the system metadata as a Blob - var sysMetaXMLBlob = new Blob([sysMetaXML], {type : 'application/xml'}); - - //Add the object XML and System Metadata XML to the form data - //Append the system metadata first, so we can take advantage of Metacat's streaming multipart handler - formData.append("sysmeta", sysMetaXMLBlob, "sysmeta"); - formData.append("object", xmlBlob); - } - catch(error){ - //Reset the identifier since we didn't actually update the object - this.resetID(); - - this.set("uploadStatus", "e"); - this.trigger("error"); - this.trigger("cancelSave"); - return false; - } - - var model = this; - var saveOptions = options || {}; - _.extend(saveOptions, { - data : formData, - cache: false, - contentType: false, - dataType: "text", - processData: false, - parse: false, - //Use the URL function to determine the URL - url: this.isNew() ? this.url() : this.url({update: true}), - xhr: function(){ - var xhr = new window.XMLHttpRequest(); - - //Upload progress - xhr.upload.addEventListener("progress", function(evt){ - if (evt.lengthComputable) { - var percentComplete = evt.loaded / evt.total * 100; - - model.set("uploadProgress", percentComplete); - } - }, false); - - return xhr; - }, - success: function(model, response, xhr){ - - model.set("numSaveAttempts", 0); - model.set("uploadStatus", "c"); - model.set("sysMetaXML", model.serializeSysMeta()); - model.set("oldPid", null); - model.fetch({merge: true, systemMetadataOnly: true}); - model.trigger("successSaving", model); - - }, - error: function(model, response, xhr){ - - model.set("numSaveAttempts", model.get("numSaveAttempts") + 1); - var numSaveAttempts = model.get("numSaveAttempts"); + //Create a FormData object to send data with our XHR + var formData = new FormData(); - //Reset the identifier changes - model.resetID(); - - if( numSaveAttempts < 3 && (response.status == 408 || response.status == 0) ){ + try { + //Add the identifier to the XHR data + if (this.isNew()) { + formData.append("pid", this.get("id")); + } else { + //Create a new ID + this.updateID(); - //Try saving again in 10, 40, and 90 seconds - setTimeout(function(){ - model.save.call(model); - }, - (numSaveAttempts * numSaveAttempts) * 10000); + //Add the ids to the form data + formData.append("newPid", this.get("id")); + formData.append("pid", this.get("oldPid")); } - else{ - model.set("numSaveAttempts", 0); - - //Get the error error information - var errorDOM = $($.parseHTML(response.responseText)), - errorContainer = errorDOM.filter("error"), - msgContainer = errorContainer.length? errorContainer.find("description") : errorDOM.not("style, title"), - errorMsg = msgContainer.length? msgContainer.text() : errorDOM; - - //When there is no network connection (status == 0), there will be no response text - if(!errorMsg || (response.status == 408 || response.status == 0)) - errorMsg = "There was a network issue that prevented your metadata from uploading. " + - "Make sure you are connected to a reliable internet connection."; - - //Save the error message in the model - model.set("errorMessage", errorMsg); - - //Set the model status as e for error - model.set("uploadStatus", "e"); - //Save the EML as a plain text file, until drafts are a supported feature - var copy = model.createTextCopy(); + //Serialize the EML XML + var xml = this.serialize(); + var xmlBlob = new Blob([xml], { type: "application/xml" }); - //If the EML copy successfully saved, let the user know that there is a copy saved behind the scenes - model.listenToOnce(copy, "successSaving", function(){ + //Get the size of the new EML XML + this.set("size", xmlBlob.size); - model.set("draftSaved", true); + //Get the new checksum of the EML XML + var checksum = md5(xml); + this.set("checksum", checksum); + this.set("checksumAlgorithm", "MD5"); - //Trigger the errorSaving event so other parts of the app know that the model failed to save - //And send the error message with it - model.trigger("errorSaving", errorMsg); + //Create the system metadata XML + var sysMetaXML = this.serializeSysMeta(); - }); + //Send the system metadata as a Blob + var sysMetaXMLBlob = new Blob([sysMetaXML], { + type: "application/xml", + }); - //If the EML copy fails to save too, then just display the usual error message - model.listenToOnce(copy, "errorSaving", function(){ + //Add the object XML and System Metadata XML to the form data + //Append the system metadata first, so we can take advantage of Metacat's streaming multipart handler + formData.append("sysmeta", sysMetaXMLBlob, "sysmeta"); + formData.append("object", xmlBlob); + } catch (error) { + //Reset the identifier since we didn't actually update the object + this.resetID(); - //Trigger the errorSaving event so other parts of the app know that the model failed to save - //And send the error message with it - model.trigger("errorSaving", errorMsg); + this.set("uploadStatus", "e"); + this.trigger("error"); + this.trigger("cancelSave"); + return false; + } - }); + var model = this; + var saveOptions = options || {}; + _.extend( + saveOptions, + { + data: formData, + cache: false, + contentType: false, + dataType: "text", + processData: false, + parse: false, + //Use the URL function to determine the URL + url: this.isNew() ? this.url() : this.url({ update: true }), + xhr: function () { + var xhr = new window.XMLHttpRequest(); + + //Upload progress + xhr.upload.addEventListener( + "progress", + function (evt) { + if (evt.lengthComputable) { + var percentComplete = (evt.loaded / evt.total) * 100; + + model.set("uploadProgress", percentComplete); + } + }, + false, + ); - //Save the EML plain text copy - copy.save(); + return xhr; + }, + success: function (model, response, xhr) { + model.set("numSaveAttempts", 0); + model.set("uploadStatus", "c"); + model.set("sysMetaXML", model.serializeSysMeta()); + model.set("oldPid", null); + model.fetch({ merge: true, systemMetadataOnly: true }); + model.trigger("successSaving", model); + }, + error: function (model, response, xhr) { + model.set("numSaveAttempts", model.get("numSaveAttempts") + 1); + var numSaveAttempts = model.get("numSaveAttempts"); + + //Reset the identifier changes + model.resetID(); + + if ( + numSaveAttempts < 3 && + (response.status == 408 || response.status == 0) + ) { + //Try saving again in 10, 40, and 90 seconds + setTimeout( + function () { + model.save.call(model); + }, + numSaveAttempts * numSaveAttempts * 10000, + ); + } else { + model.set("numSaveAttempts", 0); + + //Get the error error information + var errorDOM = $($.parseHTML(response.responseText)), + errorContainer = errorDOM.filter("error"), + msgContainer = errorContainer.length + ? errorContainer.find("description") + : errorDOM.not("style, title"), + errorMsg = msgContainer.length + ? msgContainer.text() + : errorDOM; + + //When there is no network connection (status == 0), there will be no response text + if (!errorMsg || response.status == 408 || response.status == 0) + errorMsg = + "There was a network issue that prevented your metadata from uploading. " + + "Make sure you are connected to a reliable internet connection."; + + //Save the error message in the model + model.set("errorMessage", errorMsg); + + //Set the model status as e for error + model.set("uploadStatus", "e"); + + //Save the EML as a plain text file, until drafts are a supported feature + var copy = model.createTextCopy(); + + //If the EML copy successfully saved, let the user know that there is a copy saved behind the scenes + model.listenToOnce(copy, "successSaving", function () { + model.set("draftSaved", true); + + //Trigger the errorSaving event so other parts of the app know that the model failed to save + //And send the error message with it + model.trigger("errorSaving", errorMsg); + }); - // Track the error - MetacatUI.analytics?.trackException( - `EML save error: ${errorMsg}, EML draft: ${copy.get("id")}`, - model.get("id"), - true - ); - } - } - }, MetacatUI.appUserModel.createAjaxSettings()); + //If the EML copy fails to save too, then just display the usual error message + model.listenToOnce(copy, "errorSaving", function () { + //Trigger the errorSaving event so other parts of the app know that the model failed to save + //And send the error message with it + model.trigger("errorSaving", errorMsg); + }); - return Backbone.Model.prototype.save.call(this, attributes, saveOptions); - }, + //Save the EML plain text copy + copy.save(); + // Track the error + MetacatUI.analytics?.trackException( + `EML save error: ${errorMsg}, EML draft: ${copy.get("id")}`, + model.get("id"), + true, + ); + } + }, + }, + MetacatUI.appUserModel.createAjaxSettings(), + ); + + return Backbone.Model.prototype.save.call( + this, + attributes, + saveOptions, + ); + }, /* * Checks if this EML model has all the required values necessary to save to the server */ - validate: function() { + validate: function () { let errors = {}; //A title is always required by EML - if( !this.get("title").length || !this.get("title")[0] ){ + if (!this.get("title").length || !this.get("title")[0]) { errors.title = "A title is required"; } // Validate the publication date if (this.get("pubDate") != null) { if (!this.isValidYearDate(this.get("pubDate"))) { - errors["pubDate"] = ["The value entered for publication date, '" - + this.get("pubDate") + - "' is not a valid value for this field. Enter with a year (e.g. 2017) or a date in the format YYYY-MM-DD."] + errors["pubDate"] = [ + "The value entered for publication date, '" + + this.get("pubDate") + + "' is not a valid value for this field. Enter with a year (e.g. 2017) or a date in the format YYYY-MM-DD.", + ]; } } @@ -1502,103 +1616,114 @@

Source: src/js/models/metadata/eml211/EML211.js

errors.temporalCoverage = []; //If temporal coverage is required and there aren't any, return an error - if( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && - !this.get("temporalCoverage").length ){ - errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; + if ( + MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && + !this.get("temporalCoverage").length + ) { + errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; } //If temporal coverage is required and they are all empty, return an error - else if( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && - _.every(this.get("temporalCoverage"), function(tc){ - return tc.isEmpty(); - }) ){ - errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; + else if ( + MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && + _.every(this.get("temporalCoverage"), function (tc) { + return tc.isEmpty(); + }) + ) { + errors.temporalCoverage = [{ beginDate: "Provide a begin date." }]; } //If temporal coverage is not required, validate each one - else if( this.get("temporalCoverage").length || - ( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && - _.every(this.get("temporalCoverage"), function(tc){ - return tc.isEmpty(); - }) )) { + else if ( + this.get("temporalCoverage").length || + (MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage && + _.every(this.get("temporalCoverage"), function (tc) { + return tc.isEmpty(); + })) + ) { //Iterate over each temporal coverage and add it's validation errors - _.each(this.get("temporalCoverage"), function(temporalCoverage){ - if( !temporalCoverage.isValid() && !temporalCoverage.isEmpty() ){ + _.each(this.get("temporalCoverage"), function (temporalCoverage) { + if (!temporalCoverage.isValid() && !temporalCoverage.isEmpty()) { errors.temporalCoverage.push(temporalCoverage.validationError); } }); } //Remove the temporalCoverage attribute if no errors were found - if( errors.temporalCoverage.length == 0 ){ + if (errors.temporalCoverage.length == 0) { delete errors.temporalCoverage; } //Validate the EMLParty models - var partyTypes = ["associatedParty", "contact", "creator", "metadataProvider", "publisher"]; - _.each(partyTypes, function(type){ - - var people = this.get(type); - _.each(people, function(person, i){ - - if( !person.isValid() ){ - if( !errors[type] ) - errors[type] = [person.validationError]; - else - errors[type].push(person.validationError); - } - - }, this); - - }, this); + var partyTypes = [ + "associatedParty", + "contact", + "creator", + "metadataProvider", + "publisher", + ]; + _.each( + partyTypes, + function (type) { + var people = this.get(type); + _.each( + people, + function (person, i) { + if (!person.isValid()) { + if (!errors[type]) errors[type] = [person.validationError]; + else errors[type].push(person.validationError); + } + }, + this, + ); + }, + this, + ); //Validate the EMLGeoCoverage models - _.each(this.get("geoCoverage"), function(geoCoverageModel, i){ - - if( !geoCoverageModel.isValid() ){ - if( !errors.geoCoverage ) - errors.geoCoverage = [geoCoverageModel.validationError]; - else - errors.geoCoverage.push(geoCoverageModel.validationError); - } - - }, this); + _.each( + this.get("geoCoverage"), + function (geoCoverageModel, i) { + if (!geoCoverageModel.isValid()) { + if (!errors.geoCoverage) + errors.geoCoverage = [geoCoverageModel.validationError]; + else errors.geoCoverage.push(geoCoverageModel.validationError); + } + }, + this, + ); //Validate the EMLTaxonCoverage model var taxonModel = this.get("taxonCoverage")[0]; - if( !taxonModel.isEmpty() && !taxonModel.isValid() ){ + if (!taxonModel.isEmpty() && !taxonModel.isValid()) { errors = _.extend(errors, taxonModel.validationError); - } - else if( taxonModel.isEmpty() && + } else if ( + taxonModel.isEmpty() && this.get("taxonCoverage").length == 1 && - MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage ){ - + MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage + ) { taxonModel.isValid(); errors = _.extend(errors, taxonModel.validationError); - } //Validate each EMLEntity model - _.each( this.get("entities"), function(entityModel){ - - if( !entityModel.isValid() ){ - if( !errors.entities ) + _.each(this.get("entities"), function (entityModel) { + if (!entityModel.isValid()) { + if (!errors.entities) errors.entities = [entityModel.validationError]; - else - errors.entities.push(entityModel.validationError); + else errors.entities.push(entityModel.validationError); } - }); //Validate the EML Methods let emlMethods = this.get("methods"); - if( emlMethods ){ - if( !emlMethods.isValid() ){ + if (emlMethods) { + if (!emlMethods.isValid()) { errors.methods = emlMethods.validationError; } } // Validate each EMLAnnotation model - if( this.get("annotations") ){ + if (this.get("annotations")) { this.get("annotations").each(function (model) { if (model.isValid()) { return; @@ -1613,96 +1738,125 @@

Source: src/js/models/metadata/eml211/EML211.js

} //Check the required fields for this MetacatUI configuration - for([field, isRequired] of Object.entries(MetacatUI.appModel.get("emlEditorRequiredFields"))){ - + for ([field, isRequired] of Object.entries( + MetacatUI.appModel.get("emlEditorRequiredFields"), + )) { //If it's not required, then go to the next field - if(!isRequired) continue; - - if(field == "alternateIdentifier"){ - if( !this.get("alternateIdentifier").length || _.every(this.get("alternateIdentifier"), function(altId){ return altId.trim() == "" }) ) - errors.alternateIdentifier = "At least one alternate identifier is required." - } - else if(field == "generalTaxonomicCoverage"){ - if( !this.get("taxonCoverage").length || !this.get("taxonCoverage")[0].get("generalTaxonomicCoverage") ) - errors.generalTaxonomicCoverage = "Provide a description of the general taxonomic coverage of this data set."; - } - else if(field == "geoCoverage"){ - if(!this.get("geoCoverage").length) - errors.geoCoverage = "At least one location is required."; - } - else if(field == "intellectualRights"){ - if( !this.get("intellectualRights") ) - errors.intellectualRights = "Select usage rights for this data set."; - } - else if(field == "studyExtentDescription"){ - if( !this.get("methods") || !this.get("methods").get("studyExtentDescription") ) - errors.studyExtentDescription = "Provide a study extent description."; - } - else if(field == "samplingDescription"){ - if( !this.get("methods") || !this.get("methods").get("samplingDescription") ) - errors.samplingDescription = "Provide a sampling description."; - } - else if(field == "temporalCoverage"){ - if(!this.get("temporalCoverage").length) - errors.temporalCoverage = "Provide the date(s) for this data set."; - } - else if(field == "taxonCoverage"){ - if(!this.get("taxonCoverage").length) - errors.taxonCoverage = "At least one taxa rank and value is required."; - } - else if(field == "keywordSets"){ - if( !this.get("keywordSets").length ) - errors.keywordSets = "Provide at least one keyword."; - } - //The EMLMethods model will validate itself for required fields, but - // this is a rudimentary check to make sure the EMLMethods model was created - // in the first place - else if(field == "methods"){ - if(!this.get("methods")) - errors.methods = "At least one method step is required."; - } - else if(field == "funding"){ - // Note: Checks for either the funding or award element. award - // element is checked by the project's objectDOM for now until - // EMLProject fully supports the award element - if(!this.get("project") || - !(this.get("project").get("funding").length || - (this.get("project").get("objectDOM") && - this.get("project").get("objectDOM").querySelectorAll && - this.get("project").get("objectDOM").querySelectorAll("award").length > 0))) - errors.funding = "Provide at least one project funding number or name."; - } - else if(field == "abstract"){ - if(!this.get("abstract").length) - errors["abstract"] = "Provide an abstract."; - } - else if(field == "dataSensitivity"){ - if( !this.getDataSensitivity() ){ - errors["dataSensitivity"] = "Pick the category that best describes the level of sensitivity or restriction of the data."; - } - } + if (!isRequired) continue; + + if (field == "alternateIdentifier") { + if ( + !this.get("alternateIdentifier").length || + _.every(this.get("alternateIdentifier"), function (altId) { + return altId.trim() == ""; + }) + ) + errors.alternateIdentifier = + "At least one alternate identifier is required."; + } else if (field == "generalTaxonomicCoverage") { + if ( + !this.get("taxonCoverage").length || + !this.get("taxonCoverage")[0].get("generalTaxonomicCoverage") + ) + errors.generalTaxonomicCoverage = + "Provide a description of the general taxonomic coverage of this data set."; + } else if (field == "geoCoverage") { + if (!this.get("geoCoverage").length) + errors.geoCoverage = "At least one location is required."; + } else if (field == "intellectualRights") { + if (!this.get("intellectualRights")) + errors.intellectualRights = + "Select usage rights for this data set."; + } else if (field == "studyExtentDescription") { + if ( + !this.get("methods") || + !this.get("methods").get("studyExtentDescription") + ) + errors.studyExtentDescription = + "Provide a study extent description."; + } else if (field == "samplingDescription") { + if ( + !this.get("methods") || + !this.get("methods").get("samplingDescription") + ) + errors.samplingDescription = "Provide a sampling description."; + } else if (field == "temporalCoverage") { + if (!this.get("temporalCoverage").length) + errors.temporalCoverage = + "Provide the date(s) for this data set."; + } else if (field == "taxonCoverage") { + if (!this.get("taxonCoverage").length) + errors.taxonCoverage = + "At least one taxa rank and value is required."; + } else if (field == "keywordSets") { + if (!this.get("keywordSets").length) + errors.keywordSets = "Provide at least one keyword."; + } + //The EMLMethods model will validate itself for required fields, but + // this is a rudimentary check to make sure the EMLMethods model was created + // in the first place + else if (field == "methods") { + if (!this.get("methods")) + errors.methods = "At least one method step is required."; + } else if (field == "funding") { + // Note: Checks for either the funding or award element. award + // element is checked by the project's objectDOM for now until + // EMLProject fully supports the award element + if ( + !this.get("project") || + !( + this.get("project").get("funding").length || + (this.get("project").get("objectDOM") && + this.get("project").get("objectDOM").querySelectorAll && + this.get("project").get("objectDOM").querySelectorAll("award") + .length > 0) + ) + ) + errors.funding = + "Provide at least one project funding number or name."; + } else if (field == "abstract") { + if (!this.get("abstract").length) + errors["abstract"] = "Provide an abstract."; + } else if (field == "dataSensitivity") { + if (!this.getDataSensitivity()) { + errors["dataSensitivity"] = + "Pick the category that best describes the level of sensitivity or restriction of the data."; + } + } //If this is an EMLParty type, check that there is a party of this type in the model - else if( EMLParty.prototype.partyTypes.map(t=>t.dataCategory).includes(field) ){ + else if ( + EMLParty.prototype.partyTypes + .map((t) => t.dataCategory) + .includes(field) + ) { //If this is an associatedParty role - if( EMLParty.prototype.defaults().roleOptions?.includes(field) ){ - if(!this.get("associatedParty")?.map(p=>p.get("roles")).flat().includes(field)){ - errors[field] = "Provide information about the people or organization(s) in the role: " + - EMLParty.prototype.partyTypes.find(t=>t.dataCategory==field)?.label; + if (EMLParty.prototype.defaults().roleOptions?.includes(field)) { + if ( + !this.get("associatedParty") + ?.map((p) => p.get("roles")) + .flat() + .includes(field) + ) { + errors[field] = + "Provide information about the people or organization(s) in the role: " + + EMLParty.prototype.partyTypes.find( + (t) => t.dataCategory == field, + )?.label; } + } else if (!this.get(field)?.length) { + errors[field] = + "Provide information about the people or organization(s) in the role: " + + EMLParty.prototype.partyTypes.find( + (t) => t.dataCategory == field, + )?.label; } - else if( !this.get(field)?.length ){ - errors[field] = "Provide information about the people or organization(s) in the role: " + - EMLParty.prototype.partyTypes.find(t=>t.dataCategory==field)?.label; - } - } - else if( !this.get(field) || !this.get(field)?.length ){ + } else if (!this.get(field) || !this.get(field)?.length) { errors[field] = "Provide a " + field + "."; } } - if( Object.keys(errors).length ) - return errors; - else{ + if (Object.keys(errors).length) return errors; + else { return; } }, @@ -1713,38 +1867,46 @@

Source: src/js/models/metadata/eml211/EML211.js

Note that this method considers a zero-length String to be valid because the EML211.serialize() method will properly handle a null or zero-length String by serializing out the current year. */ - isValidYearDate: function(value) { - return (value === "" || /^\d{4}$/.test(value) || /^\d{4}-\d{2}-\d{2}$/.test(value)); + isValidYearDate: function (value) { + return ( + value === "" || + /^\d{4}$/.test(value) || + /^\d{4}-\d{2}-\d{2}$/.test(value) + ); }, /* * Sends an AJAX request to fetch the system metadata for this EML object. * Will not trigger a sync event since it does not use Backbone.Model.fetch */ - fetchSystemMetadata: function(options){ - - if(!options) var options = {}; + fetchSystemMetadata: function (options) { + if (!options) var options = {}; else options = _.clone(options); var model = this, - fetchOptions = _.extend({ - url: MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(this.get("id")), - dataType: "text", - success: function(response){ - model.set(DataONEObject.prototype.parse.call(model, response)); - - //Trigger a custom event that the sys meta was updated - model.trigger("sysMetaUpdated"); + fetchOptions = _.extend( + { + url: + MetacatUI.appModel.get("metaServiceUrl") + + encodeURIComponent(this.get("id")), + dataType: "text", + success: function (response) { + model.set(DataONEObject.prototype.parse.call(model, response)); + + //Trigger a custom event that the sys meta was updated + model.trigger("sysMetaUpdated"); + }, + error: function () { + model.trigger("error"); + }, }, - error: function(){ - model.trigger('error'); - } - }, options); + options, + ); - //Add the authorization header and other AJAX settings - _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); + //Add the authorization header and other AJAX settings + _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); - $.ajax(fetchOptions); + $.ajax(fetchOptions); }, /* * Returns the nofde in the given EML document that the given node type @@ -1753,7 +1915,7 @@

Source: src/js/models/metadata/eml211/EML211.js

* Returns false if either the node is not found in the and this should * be handled by the caller. */ - getEMLPosition: function(eml, nodeName) { + getEMLPosition: function (eml, nodeName) { var nodeOrder = this.get("nodeOrder"); var position = _.indexOf(nodeOrder, nodeName.toLowerCase()); @@ -1775,8 +1937,8 @@

Source: src/js/models/metadata/eml211/EML211.js

/* * Checks if this model has updates that need to be synced with the server. */ - hasUpdates: function(){ - if(this.constructor.__super__.hasUpdates.call(this)) return true; + hasUpdates: function () { + if (this.constructor.__super__.hasUpdates.call(this)) return true; //If nothing else has been changed, then this object hasn't had any updates return false; @@ -1785,15 +1947,14 @@

Source: src/js/models/metadata/eml211/EML211.js

/* Add an entity into the EML 2.1.1 object */ - addEntity: function(emlEntity, position) { + addEntity: function (emlEntity, position) { //Get the current list of entities var currentEntities = this.get("entities"); - if( typeof position == "undefined" || position == -1) + if (typeof position == "undefined" || position == -1) currentEntities.push(emlEntity); - else - //Add the entity model to the entity array - currentEntities.splice(position, 0, emlEntity); + //Add the entity model to the entity array + else currentEntities.splice(position, 0, emlEntity); this.trigger("change:entities"); @@ -1805,9 +1966,8 @@

Source: src/js/models/metadata/eml211/EML211.js

/* Remove an entity from the EML 2.1.1 object */ - removeEntity: function(emlEntity) { - if(!emlEntity || typeof emlEntity != "object") - return; + removeEntity: function (emlEntity) { + if (!emlEntity || typeof emlEntity != "object") return; //Get the current list of entities var entities = this.get("entities"); @@ -1820,89 +1980,108 @@

Source: src/js/models/metadata/eml211/EML211.js

/* * Find the entity model for a given DataONEObject */ - getEntity: function(dataONEObj){ - + getEntity: function (dataONEObj) { //If an EMLEntity model has been found for this object before, then return it - if( dataONEObj.get("metadataEntity") ){ + if (dataONEObj.get("metadataEntity")) { dataONEObj.get("metadataEntity").set("dataONEObject", dataONEObj); return dataONEObj.get("metadataEntity"); } - var entity = _.find(this.get("entities"), function(e){ - - //Matches of the checksum or identifier are definite matches - if( e.get("xmlID") == dataONEObj.getXMLSafeID() ) - return true; - else if( e.get("physicalMD5Checksum") && (e.get("physicalMD5Checksum") == dataONEObj.get("checksum") && dataONEObj.get("checksumAlgorithm").toUpperCase() == "MD5")) - return true; - else if(e.get("downloadID") && e.get("downloadID") == dataONEObj.get("id")) - return true; + var entity = _.find( + this.get("entities"), + function (e) { + //Matches of the checksum or identifier are definite matches + if (e.get("xmlID") == dataONEObj.getXMLSafeID()) return true; + else if ( + e.get("physicalMD5Checksum") && + e.get("physicalMD5Checksum") == dataONEObj.get("checksum") && + dataONEObj.get("checksumAlgorithm").toUpperCase() == "MD5" + ) + return true; + else if ( + e.get("downloadID") && + e.get("downloadID") == dataONEObj.get("id") + ) + return true; - // Get the file name from the EML for this entity - var fileNameFromEML = e.get("physicalObjectName") || e.get("entityName"); + // Get the file name from the EML for this entity + var fileNameFromEML = + e.get("physicalObjectName") || e.get("entityName"); - // If the EML file name matches the DataONEObject file name - if (fileNameFromEML && + // If the EML file name matches the DataONEObject file name + if ( + fileNameFromEML && dataONEObj.get("fileName") && - ((fileNameFromEML.toLowerCase() == dataONEObj.get("fileName").toLowerCase()) || - (fileNameFromEML.replace(/ /g, "_").toLowerCase() == dataONEObj.get("fileName").toLowerCase()))) { - - //Get an array of all the other entities in this EML - var otherEntities = _.without(this.get("entities"), e); + (fileNameFromEML.toLowerCase() == + dataONEObj.get("fileName").toLowerCase() || + fileNameFromEML.replace(/ /g, "_").toLowerCase() == + dataONEObj.get("fileName").toLowerCase()) + ) { + //Get an array of all the other entities in this EML + var otherEntities = _.without(this.get("entities"), e); // If this entity name matches the dataone object file name, AND no other dataone object file name // matches, then we can assume this is the entity element for this file. - var otherMatchingEntity = _.find(otherEntities, function(otherE){ - - // Get the file name from the EML for the other entities - var otherFileNameFromEML = otherE.get("physicalObjectName") || otherE.get("entityName"); - - // If the file names match, return true - if( (otherFileNameFromEML == dataONEObj.get("fileName")) || (otherFileNameFromEML.replace(/ /g, "_") == dataONEObj.get("fileName")) ) - return true; - }); - - // If this entity's file name didn't match any other file names in the EML, - // then this entity is a match for the given dataONEObject - if( !otherMatchingEntity ) - return true; - } - - }, this); + var otherMatchingEntity = _.find( + otherEntities, + function (otherE) { + // Get the file name from the EML for the other entities + var otherFileNameFromEML = + otherE.get("physicalObjectName") || + otherE.get("entityName"); + + // If the file names match, return true + if ( + otherFileNameFromEML == dataONEObj.get("fileName") || + otherFileNameFromEML.replace(/ /g, "_") == + dataONEObj.get("fileName") + ) + return true; + }, + ); + + // If this entity's file name didn't match any other file names in the EML, + // then this entity is a match for the given dataONEObject + if (!otherMatchingEntity) return true; + } + }, + this, + ); //If we found an entity, give it an ID and return it - if(entity){ - + if (entity) { //If this entity has been matched to another DataONEObject already, then don't match it again - if( entity.get("dataONEObject") == dataONEObj ){ + if (entity.get("dataONEObject") == dataONEObj) { return entity; } //If this entity has been matched to a different DataONEObject already, then don't match it again. //i.e. We will not override existing entity<->DataONEObject pairings - else if( entity.get("dataONEObject") ){ + else if (entity.get("dataONEObject")) { return; - } - else{ + } else { entity.set("dataONEObject", dataONEObj); } - //Create an XML-safe ID and set it on the Entity model - var entityID = this.getUniqueEntityId(dataONEObj); - entity.set("xmlID", entityID); + //Create an XML-safe ID and set it on the Entity model + var entityID = this.getUniqueEntityId(dataONEObj); + entity.set("xmlID", entityID); - //Save a reference to this entity so we don't have to refind it later - dataONEObj.set("metadataEntity", entity); + //Save a reference to this entity so we don't have to refind it later + dataONEObj.set("metadataEntity", entity); return entity; } //See if one data object is of this type in the package - var matchingTypes = _.filter(this.get("entities"), function(e){ - return (e.get("formatName") == (dataONEObj.get("formatId") || dataONEObj.get("mediaType"))); + var matchingTypes = _.filter(this.get("entities"), function (e) { + return ( + e.get("formatName") == + (dataONEObj.get("formatId") || dataONEObj.get("mediaType")) + ); }); - if(matchingTypes.length == 1){ - //Create an XML-safe ID and set it on the Entity model + if (matchingTypes.length == 1) { + //Create an XML-safe ID and set it on the Entity model matchingTypes[0].set("xmlID", dataONEObj.getXMLSafeID()); return matchingTypes[0]; @@ -1910,54 +2089,54 @@

Source: src/js/models/metadata/eml211/EML211.js

//If this EML is in a DataPackage with only one other DataONEObject, // and there is only one entity in the EML, then we can assume they are the same entity - if( this.get("entities").length == 1 ){ - - if( this.get("collections")[0] && this.get("collections")[0].type == "DataPackage" && - this.get("collections")[0].length == 2 && _.contains(this.get("collections")[0].models, dataONEObj)){ - return this.get("entities")[0]; + if (this.get("entities").length == 1) { + if ( + this.get("collections")[0] && + this.get("collections")[0].type == "DataPackage" && + this.get("collections")[0].length == 2 && + _.contains(this.get("collections")[0].models, dataONEObj) + ) { + return this.get("entities")[0]; } - } return false; - }, - createEntity: function(dataONEObject){ + createEntity: function (dataONEObject) { // Add or append an entity to the parent's entity list - var entityModel = new EMLOtherEntity({ - entityName : dataONEObject.get("fileName"), - entityType : dataONEObject.get("formatId") || - dataONEObject.get("mediaType") || - "application/octet-stream", - dataONEObject: dataONEObject, - parentModel: this, - xmlID: dataONEObject.getXMLSafeID() - }); + var entityModel = new EMLOtherEntity({ + entityName: dataONEObject.get("fileName"), + entityType: + dataONEObject.get("formatId") || + dataONEObject.get("mediaType") || + "application/octet-stream", + dataONEObject: dataONEObject, + parentModel: this, + xmlID: dataONEObject.getXMLSafeID(), + }); - this.addEntity(entityModel); + this.addEntity(entityModel); - //If this DataONEObject fails to upload, remove the EML entity - this.listenTo(dataONEObject, "errorSaving", function(){ - this.removeEntity(dataONEObject.get("metadataEntity")); + //If this DataONEObject fails to upload, remove the EML entity + this.listenTo(dataONEObject, "errorSaving", function () { + this.removeEntity(dataONEObject.get("metadataEntity")); - //Listen for a successful save so the entity can be added back - this.listenToOnce(dataONEObject, "successSaving", function(){ - this.addEntity(dataONEObject.get("metadataEntity")) - }); + //Listen for a successful save so the entity can be added back + this.listenToOnce(dataONEObject, "successSaving", function () { + this.addEntity(dataONEObject.get("metadataEntity")); }); - + }); }, /* - * Creates an XML-safe identifier that is unique to this EML document, - * based on the given DataONEObject model. It is intended for EML entity nodes in particular. - * - * @param {DataONEObject} - a DataONEObject model that this EML documents - * @return {string} - an identifier string unique to this EML document - */ - getUniqueEntityId: function(dataONEObject){ - + * Creates an XML-safe identifier that is unique to this EML document, + * based on the given DataONEObject model. It is intended for EML entity nodes in particular. + * + * @param {DataONEObject} - a DataONEObject model that this EML documents + * @return {string} - an identifier string unique to this EML document + */ + getUniqueEntityId: function (dataONEObject) { var uniqueId = ""; uniqueId = dataONEObject.getXMLSafeID(); @@ -1966,30 +2145,37 @@

Source: src/js/models/metadata/eml211/EML211.js

var emlString = this.get("objectXML"); //If this id already exists in the EML... - if(emlString && emlString.indexOf(' id="' + uniqueId + '"')){ + if (emlString && emlString.indexOf(' id="' + uniqueId + '"')) { //Create a random uuid to use instead uniqueId = "urn-uuid-" + uuid.v4(); } return uniqueId; - }, /* * removeParty - removes the given EMLParty model from this EML211 model's attributes */ - removeParty: function(partyModel){ + removeParty: function (partyModel) { //The list of attributes this EMLParty might be stored in - var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"]; + var possibleAttr = [ + "creator", + "contact", + "metadataProvider", + "publisher", + "associatedParty", + ]; // Iterate over each possible attribute - _.each(possibleAttr, function(attr){ - - if( _.contains(this.get(attr), partyModel) ){ - this.set( attr, _.without(this.get(attr), partyModel) ); - } - - }, this); + _.each( + possibleAttr, + function (attr) { + if (_.contains(this.get(attr), partyModel)) { + this.set(attr, _.without(this.get(attr), partyModel)); + } + }, + this, + ); }, /** @@ -1997,37 +2183,47 @@

Source: src/js/models/metadata/eml211/EML211.js

* * @param {EMLParty} partyModel: The EMLParty model we're moving */ - movePartyUp: function(partyModel) { - var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"]; + movePartyUp: function (partyModel) { + var possibleAttr = [ + "creator", + "contact", + "metadataProvider", + "publisher", + "associatedParty", + ]; // Iterate over each possible attribute - _.each(possibleAttr, function(attr){ - if (!_.contains(this.get(attr), partyModel)) { - return; - } - // Make a clone because we're going to use splice - var models = _.clone(this.get(attr)); + _.each( + possibleAttr, + function (attr) { + if (!_.contains(this.get(attr), partyModel)) { + return; + } + // Make a clone because we're going to use splice + var models = _.clone(this.get(attr)); - // Find the index of the model we're moving - var index = _.findIndex(models, function(m) { - return m === partyModel; - }); + // Find the index of the model we're moving + var index = _.findIndex(models, function (m) { + return m === partyModel; + }); - if (index === 0) { - // Already first - return; - } + if (index === 0) { + // Already first + return; + } - if (index === -1) { - // Couldn't find the model - return; - } + if (index === -1) { + // Couldn't find the model + return; + } - // Do the move using splice and update the model - models.splice(index - 1, 0, models.splice(index, 1)[0]) - this.set(attr, models); - this.trigger("change:" + attr); - }, this); + // Do the move using splice and update the model + models.splice(index - 1, 0, models.splice(index, 1)[0]); + this.set(attr, models); + this.trigger("change:" + attr); + }, + this, + ); }, /** @@ -2035,64 +2231,70 @@

Source: src/js/models/metadata/eml211/EML211.js

* * @param {EMLParty} partyModel: The EMLParty model we're moving */ - movePartyDown: function(partyModel) { - var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"]; + movePartyDown: function (partyModel) { + var possibleAttr = [ + "creator", + "contact", + "metadataProvider", + "publisher", + "associatedParty", + ]; // Iterate over each possible attribute - _.each(possibleAttr, function(attr){ - if (!_.contains(this.get(attr), partyModel)) { - return; - } - // Make a clone because we're going to use splice - var models = _.clone(this.get(attr)); - - // Find the index of the model we're moving - var index = _.findIndex(models, function(m) { - return m === partyModel; - }); + _.each( + possibleAttr, + function (attr) { + if (!_.contains(this.get(attr), partyModel)) { + return; + } + // Make a clone because we're going to use splice + var models = _.clone(this.get(attr)); - if (index === -1) { - // Couldn't find the model - return; - } + // Find the index of the model we're moving + var index = _.findIndex(models, function (m) { + return m === partyModel; + }); - // Figure out where to put the new model - // Leave it in the same place if the next index doesn't exist - // Move one forward if it does - var newIndex = (models.length <= index + 1) ? index : index + 1; + if (index === -1) { + // Couldn't find the model + return; + } - // Do the move using splice and update the model - models.splice(newIndex, 0, models.splice(index, 1)[0]) - this.set(attr, models); - this.trigger("change:" + attr); - }, this); + // Figure out where to put the new model + // Leave it in the same place if the next index doesn't exist + // Move one forward if it does + var newIndex = models.length <= index + 1 ? index : index + 1; + + // Do the move using splice and update the model + models.splice(newIndex, 0, models.splice(index, 1)[0]); + this.set(attr, models); + this.trigger("change:" + attr); + }, + this, + ); }, /* - * Adds the given EMLParty model to this EML211 model in the - * appropriate role array in the given position - * - * @param {EMLParty} - The EMLParty model to add - * @param {number} - The position in the role array in which to insert this EMLParty - * @return {boolean} - Returns true if the EMLParty was successfully added, false if it was cancelled - */ - addParty: function(partyModel, position){ - + * Adds the given EMLParty model to this EML211 model in the + * appropriate role array in the given position + * + * @param {EMLParty} - The EMLParty model to add + * @param {number} - The position in the role array in which to insert this EMLParty + * @return {boolean} - Returns true if the EMLParty was successfully added, false if it was cancelled + */ + addParty: function (partyModel, position) { //If the EMLParty model is empty, don't add it to the EML211 model - if(partyModel.isEmpty()) - return false; + if (partyModel.isEmpty()) return false; //Get the role of this EMLParty var role = partyModel.get("type") || "associatedParty"; //If this model already contains this EMLParty, then exit - if( _.contains(this.get(role), partyModel) ) - return false; + if (_.contains(this.get(role), partyModel)) return false; - if( typeof position == "undefined" ){ + if (typeof position == "undefined") { this.get(role).push(partyModel); - } - else { + } else { this.get(role).splice(position, 0, partyModel); } @@ -2106,62 +2308,72 @@

Source: src/js/models/metadata/eml211/EML211.js

* @param {string} partyType - A string that represents either the role or the party type. For example, "contact", "creator", "principalInvestigator", etc. * @since 2.15.0 */ - getPartiesByType: function(partyType){ - + getPartiesByType: function (partyType) { try { - if(!partyType){ - return false + if (!partyType) { + return false; } var associatedPartyTypes = new EMLParty().get("roleOptions"), - isAssociatedParty = associatedPartyTypes.includes(partyType), - parties = []; + isAssociatedParty = associatedPartyTypes.includes(partyType), + parties = []; // For "contact", "creator", "metadataProvider", "publisher", each party type has it's own // array in the EML model - if(!isAssociatedParty){ + if (!isAssociatedParty) { parties = this.get(partyType); - // For "custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator", etc., - // party members are listed in the EML model's associated parties array. Each associated party's - // party type is indicated in the role attribute. + // For "custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator", etc., + // party members are listed in the EML model's associated parties array. Each associated party's + // party type is indicated in the role attribute. } else { - parties = _.filter(this.get("associatedParty"), function (associatedParty) { - return associatedParty.get("roles").includes(partyType) } + parties = _.filter( + this.get("associatedParty"), + function (associatedParty) { + return associatedParty.get("roles").includes(partyType); + }, ); } return parties; - } catch (error) { - console.log("Error trying to find a list of party members in an EML model by type. Error details: " + error); + console.log( + "Error trying to find a list of party members in an EML model by type. Error details: " + + error, + ); } }, - createUnits: function(){ + createUnits: function () { this.units.fetch(); }, /* Initialize the object XML for brand spankin' new EML objects */ - createXML: function() { - - let emlSystem = MetacatUI.appModel.get("emlSystem"); - emlSystem = (!emlSystem || typeof emlSystem != "string") ? "knb" : emlSystem; - - var xml = "<eml:eml xmlns:eml=\"https://eml.ecoinformatics.org/eml-2.2.0\"></eml:eml>", - eml = $($.parseHTML(xml)); - - // Set base attributes - eml.attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - eml.attr("xmlns:stmml", "http://www.xml-cml.org/schema/stmml-1.1"); - eml.attr("xsi:schemaLocation", "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd"); - eml.attr("packageId", this.get("id")); - eml.attr("system", emlSystem); - - // Add the dataset - eml.append(document.createElement("dataset")); - eml.find("dataset").append(document.createElement("title")); - - var emlString = $(document.createElement("div")).append(eml.clone()).html(); - - return emlString; + createXML: function () { + let emlSystem = MetacatUI.appModel.get("emlSystem"); + emlSystem = + !emlSystem || typeof emlSystem != "string" ? "knb" : emlSystem; + + var xml = + '<eml:eml xmlns:eml="https://eml.ecoinformatics.org/eml-2.2.0"></eml:eml>', + eml = $($.parseHTML(xml)); + + // Set base attributes + eml.attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + eml.attr("xmlns:stmml", "http://www.xml-cml.org/schema/stmml-1.1"); + eml.attr( + "xsi:schemaLocation", + "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd", + ); + eml.attr("packageId", this.get("id")); + eml.attr("system", emlSystem); + + // Add the dataset + eml.append(document.createElement("dataset")); + eml.find("dataset").append(document.createElement("title")); + + var emlString = $(document.createElement("div")) + .append(eml.clone()) + .html(); + + return emlString; }, /* @@ -2170,69 +2382,77 @@

Source: src/js/models/metadata/eml211/EML211.js

@param xmlString The XML string to make the replacement in */ - cleanUpXML: function(xmlString){ + cleanUpXML: function (xmlString) { xmlString.replace("<source>", "<sourced>"); xmlString.replace("</source>", "</sourced>"); return xmlString; }, - createTextCopy: function(){ - var emlDraftText = "EML draft for " + this.get("id") + "(" + this.get("title") + ") by " + - MetacatUI.appUserModel.get("firstName") + " " + MetacatUI.appUserModel.get("lastName"); - - if(this.get("uploadStatus") == "e" && this.get("errorMessage")){ - emlDraftText += ". This EML had the following save error: `" + this.get("errorMessage") + "` "; - } - else { + createTextCopy: function () { + var emlDraftText = + "EML draft for " + + this.get("id") + + "(" + + this.get("title") + + ") by " + + MetacatUI.appUserModel.get("firstName") + + " " + + MetacatUI.appUserModel.get("lastName"); + + if (this.get("uploadStatus") == "e" && this.get("errorMessage")) { + emlDraftText += + ". This EML had the following save error: `" + + this.get("errorMessage") + + "` "; + } else { emlDraftText += ": "; } emlDraftText += this.serialize(); var plainTextEML = new DataONEObject({ - formatId: "text/plain", - fileName: "eml_draft_" + (MetacatUI.appUserModel.get("lastName") || "") + ".txt", - uploadFile: new Blob([emlDraftText], {type : 'plain/text'}), - synced: true - }); + formatId: "text/plain", + fileName: + "eml_draft_" + + (MetacatUI.appUserModel.get("lastName") || "") + + ".txt", + uploadFile: new Blob([emlDraftText], { type: "plain/text" }), + synced: true, + }); return plainTextEML; }, /* - * Cleans up the given text so that it is XML-valid by escaping reserved characters, trimming white space, etc. - * - * @param {string} textString - The string to clean up - * @return {string} - The cleaned up string - */ - cleanXMLText: function(textString){ - - if( typeof textString != "string" ) - return; + * Cleans up the given text so that it is XML-valid by escaping reserved characters, trimming white space, etc. + * + * @param {string} textString - The string to clean up + * @return {string} - The cleaned up string + */ + cleanXMLText: function (textString) { + if (typeof textString != "string") return; textString = textString.trim(); //Check for XML/HTML elements - _.each(textString.match(/<\s*[^>]*>/g), function(xmlNode){ - + _.each(textString.match(/<\s*[^>]*>/g), function (xmlNode) { //Encode <, >, and </ substrings var tagName = xmlNode.replace(/>/g, "&gt;"); tagName = tagName.replace(/</g, "&lt;"); //Replace the xmlNode in the full text string textString = textString.replace(xmlNode, tagName); - }); //Remove Unicode characters that are not valid XML characters //Create a regular expression that matches any character that is not a valid XML character // (see https://www.w3.org/TR/xml/#charsets) - var invalidCharsRegEx = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/g; + var invalidCharsRegEx = + /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/g; textString = textString.replace(invalidCharsRegEx, ""); return textString; - }, /* @@ -2241,53 +2461,55 @@

Source: src/js/models/metadata/eml211/EML211.js

@param xmlString The XML string with reference elements to transform */ - dereference: function(xmlString) { - var referencesList; // the array of references elements in the document - var referencedID; // The id of the referenced element - var referencesParentEl; // The parent of the given references element - var referencedEl; // The referenced DOM to be copied - - var xmlDOM = $.parseXML(xmlString); - referencesList = xmlDOM.getElementsByTagName("references"); - - if (referencesList.length) { - // Process each references elements - _.each(referencesList, function(referencesEl, index, referencesList) { - // Can't rely on the passed referencesEl since the list length changes - // because of the remove() below. Reuse referencesList[0] for every item: - // referencedID = $(referencesEl).text(); // doesn't work - referencesEl = referencesList[0]; - referencedID = $(referencesEl).text(); - referencesParentEl = ($(referencesEl).parent())[0]; - if (typeof referencedID !== "undefined" && referencedID != "") { - referencedEl = xmlDOM.getElementById(referencedID); - if (typeof referencedEl != "undefined") { - // Clone the referenced element and replace the references element - var referencedClone = ($(referencedEl).clone())[0]; - $(referencesParentEl) - .children(referencesEl.localName) - .replaceWith($(referencedClone).children()); - //$(referencesParentEl).append($(referencedClone).children()); - $(referencesParentEl).attr("id", DataONEObject.generateId()); - } - } - }, xmlDOM); - } - return (new XMLSerializer()).serializeToString(xmlDOM); + dereference: function (xmlString) { + var referencesList; // the array of references elements in the document + var referencedID; // The id of the referenced element + var referencesParentEl; // The parent of the given references element + var referencedEl; // The referenced DOM to be copied + + var xmlDOM = $.parseXML(xmlString); + referencesList = xmlDOM.getElementsByTagName("references"); + + if (referencesList.length) { + // Process each references elements + _.each( + referencesList, + function (referencesEl, index, referencesList) { + // Can't rely on the passed referencesEl since the list length changes + // because of the remove() below. Reuse referencesList[0] for every item: + // referencedID = $(referencesEl).text(); // doesn't work + referencesEl = referencesList[0]; + referencedID = $(referencesEl).text(); + referencesParentEl = $(referencesEl).parent()[0]; + if (typeof referencedID !== "undefined" && referencedID != "") { + referencedEl = xmlDOM.getElementById(referencedID); + if (typeof referencedEl != "undefined") { + // Clone the referenced element and replace the references element + var referencedClone = $(referencedEl).clone()[0]; + $(referencesParentEl) + .children(referencesEl.localName) + .replaceWith($(referencedClone).children()); + //$(referencesParentEl).append($(referencedClone).children()); + $(referencesParentEl).attr("id", DataONEObject.generateId()); + } + } + }, + xmlDOM, + ); + } + return new XMLSerializer().serializeToString(xmlDOM); }, /* - * Uses the EML `title` to set the `fileName` attribute on this model. - */ - setFileName: function(){ - + * Uses the EML `title` to set the `fileName` attribute on this model. + */ + setFileName: function () { var title = ""; // Get the title from the metadata - if( Array.isArray(this.get("title")) ){ + if (Array.isArray(this.get("title"))) { title = this.get("title")[0]; - } - else if( typeof this.get("title") == "string" ){ + } else if (typeof this.get("title") == "string") { title = this.get("title"); } @@ -2298,20 +2520,28 @@

Source: src/js/models/metadata/eml211/EML211.js

var trimmedTitle = title.trim().substr(0, maxLength); //re-trim if we are in the middle of a word - if( trimmedTitle.indexOf(" ") > -1 ){ - trimmedTitle = trimmedTitle.substr(0, Math.min(trimmedTitle.length, trimmedTitle.lastIndexOf(" "))); + if (trimmedTitle.indexOf(" ") > -1) { + trimmedTitle = trimmedTitle.substr( + 0, + Math.min(trimmedTitle.length, trimmedTitle.lastIndexOf(" ")), + ); } //Replace all non alphanumeric characters with underscores // and make sure there isn't more than one underscore in a row - trimmedTitle = trimmedTitle.replace(/[^a-zA-Z0-9]/g, "_").replace(/_{2,}/g, "_"); + trimmedTitle = trimmedTitle + .replace(/[^a-zA-Z0-9]/g, "_") + .replace(/_{2,}/g, "_"); //Set the fileName on the model this.set("fileName", trimmedTitle + ".xml"); }, - trickleUpChange: function(){ - if( !MetacatUI.rootDataPackage || !MetacatUI.rootDataPackage.packageModel ) + trickleUpChange: function () { + if ( + !MetacatUI.rootDataPackage || + !MetacatUI.rootDataPackage.packageModel + ) return; //Mark the package as changed @@ -2325,14 +2555,14 @@

Source: src/js/models/metadata/eml211/EML211.js

* @param {Element} eml: The root eml:eml element to modify * @return {Element} The element, possibly modified */ - setSchemaLocation: function(eml) { + setSchemaLocation: function (eml) { if (!MetacatUI || !MetacatUI.appModel) { return eml; } var current = $(eml).attr("xsi:schemaLocation"), - format = MetacatUI.appModel.get("editorSerializationFormat"), - location = MetacatUI.appModel.get("editorSchemaLocation"); + format = MetacatUI.appModel.get("editorSerializationFormat"), + location = MetacatUI.appModel.get("editorSchemaLocation"); // Return now if we can't do anything anyway if (!format || !location) { @@ -2356,7 +2586,7 @@

Source: src/js/models/metadata/eml211/EML211.js

return eml; }, - createID: function() { + createID: function () { this.set("xmlID", uuid.v4()); }, @@ -2370,19 +2600,17 @@

Source: src/js/models/metadata/eml211/EML211.js

* @property {Boolean} [annotationData.allowDuplicates] If false, this annotation will replace all annotations already set with the same propertyURI. * By default, more than one annotation with a given propertyURI can be added (defaults to true) */ - addAnnotation: function(annotationData){ - - try{ - if( !annotationData || typeof annotationData != "object" ){ + addAnnotation: function (annotationData) { + try { + if (!annotationData || typeof annotationData != "object") { return; } //If no element name is provided, default to the dataset element. let elementName = ""; - if( !annotationData.elementName ){ - elementName = "dataset" - } - else{ + if (!annotationData.elementName) { + elementName = "dataset"; + } else { elementName = annotationData.elementName; } //Remove the elementName property so it isn't set on the EMLAnnotation model later. @@ -2396,71 +2624,63 @@

Source: src/js/models/metadata/eml211/EML211.js

let annotation = new EMLAnnotation(annotationData); //Update annotations set on the dataset element - if( elementName == "dataset" ){ + if (elementName == "dataset") { let annotations = this.get("annotations"); //If the current annotations set on the EML model are not in Array form, change it to an array - if( !annotations ){ + if (!annotations) { annotations = new EMLAnnotations(); } - if( allowDuplicates === false ){ + if (allowDuplicates === false) { //Add the EMLAnnotation to the collection, making sure to remove duplicates first annotations.replaceDuplicateWith(annotation); - } - else{ + } else { annotations.add(annotation); } //Set the annotations and force the change to be recognized by the model - this.set("annotations", annotations, {silent: true}); + this.set("annotations", annotations, { silent: true }); this.handleChange(this, { force: true }); - - } - else{ + } else { /** @todo Add annotation support for other EML Elements */ } - - } - catch(e){ + } catch (e) { console.error("Could not add Annotation to the EML: ", e); } - }, /** - * Finds annotations that are of the `data sensitivity` property from the NCEAS SENSO ontology. - * Returns undefined if none are found. This function returns EMLAnnotation models because the data - * sensitivity is stored in the EML Model as EMLAnnotations and added to EML as semantic annotations. - * @returns {EMLAnnotation[]|undefined} - */ - getDataSensitivity: function(){ - try{ + * Finds annotations that are of the `data sensitivity` property from the NCEAS SENSO ontology. + * Returns undefined if none are found. This function returns EMLAnnotation models because the data + * sensitivity is stored in the EML Model as EMLAnnotations and added to EML as semantic annotations. + * @returns {EMLAnnotation[]|undefined} + */ + getDataSensitivity: function () { + try { let annotations = this.get("annotations"); - if(annotations){ - let found = annotations.where({ propertyURI: this.get("dataSensitivityPropertyURI") }); - if( !found || !found.length ){ + if (annotations) { + let found = annotations.where({ + propertyURI: this.get("dataSensitivityPropertyURI"), + }); + if (!found || !found.length) { return; - } - else{ + } else { return found; } - } - else{ + } else { return; } - } - catch(e){ + } catch (e) { console.error("Failed to get Data Sensitivity from EML model: ", e); return; } - } - - }); + }, + }, + ); - return EML211; - } -); + return EML211; +});
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLAnnotation.js.html b/docs/docs/src_js_models_metadata_eml211_EMLAnnotation.js.html index 792d51c7d..204f79118 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLAnnotation.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLAnnotation.js.html @@ -44,169 +44,190 @@

Source: src/js/models/metadata/eml211/EMLAnnotation.js
-
define(["jquery", "underscore", "backbone"],
-  function ($, _, Backbone) {
-
-    /**
-     * @class EMLAnnotation
-     * @classdesc Stores EML SemanticAnnotation elements.
-     * @classcategory Models/Metadata/EML211
-     * @see https://eml.ecoinformatics.org/eml-2.2.0/eml-semantics.xsd
-     * @extends Backbone.Model
-     */
-    var EMLAnnotation = Backbone.Model.extend(
-          /** @lends EMLAnnotation.prototype */{
-
-        type: "EMLAnnotation",
-
-        defaults: function () {
-          return {
-            isNew: true,
-            propertyLabel: null,
-            propertyURI: null,
-            valueLabel: null,
-            valueURI: null,
-            objectDOM: null,
-            objectXML: null
-          }
-        },
-
-        initialize: function (attributes, opions) {
-          this.on("change", this.trickleUpChange);
-        },
-
-        parse: function (attributes, options) {
-          // If parsing, this is an existing annotation so it's not isNew
-          attributes.isNew = false;
-
-          var propertyURI = $(attributes.objectDOM).find("propertyuri");
-          var valueURI = $(attributes.objectDOM).find("valueuri");
-
-          if (propertyURI.length !== 1 || valueURI.length !== 1) {
-            return;
-          }
-
-          attributes.propertyURI = $(propertyURI).text().trim();
-
-          attributes.valueURI = $(valueURI).text().trim();
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @class EMLAnnotation
+   * @classdesc Stores EML SemanticAnnotation elements.
+   * @classcategory Models/Metadata/EML211
+   * @see https://eml.ecoinformatics.org/eml-2.2.0/eml-semantics.xsd
+   * @extends Backbone.Model
+   */
+  var EMLAnnotation = Backbone.Model.extend(
+    /** @lends EMLAnnotation.prototype */ {
+      type: "EMLAnnotation",
+
+      defaults: function () {
+        return {
+          isNew: true,
+          propertyLabel: null,
+          propertyURI: null,
+          valueLabel: null,
+          valueURI: null,
+          objectDOM: null,
+          objectXML: null,
+        };
+      },
+
+      initialize: function (attributes, opions) {
+        this.on("change", this.trickleUpChange);
+      },
+
+      parse: function (attributes, options) {
+        // If parsing, this is an existing annotation so it's not isNew
+        attributes.isNew = false;
+
+        var propertyURI = $(attributes.objectDOM).find("propertyuri");
+        var valueURI = $(attributes.objectDOM).find("valueuri");
+
+        if (propertyURI.length !== 1 || valueURI.length !== 1) {
+          return;
+        }
 
-          var propertyLabel = $(propertyURI).attr("label");
-          var valueLabel = $(valueURI).attr("label");
+        attributes.propertyURI = $(propertyURI).text().trim();
 
-          if (!propertyLabel || !valueLabel) {
-            return;
-          }
+        attributes.valueURI = $(valueURI).text().trim();
 
-          attributes.propertyLabel = propertyLabel.trim();
-          attributes.valueLabel = valueLabel.trim();
+        var propertyLabel = $(propertyURI).attr("label");
+        var valueLabel = $(valueURI).attr("label");
 
-          return attributes;
-        },
+        if (!propertyLabel || !valueLabel) {
+          return;
+        }
 
-        validate: function () {
-          var errors = [];
+        attributes.propertyLabel = propertyLabel.trim();
+        attributes.valueLabel = valueLabel.trim();
 
-          if (this.isEmpty()) {
-            this.trigger("valid");
+        return attributes;
+      },
 
-            return;
-          }
+      validate: function () {
+        var errors = [];
 
-          var propertyURI = this.get("propertyURI");
+        if (this.isEmpty()) {
+          this.trigger("valid");
 
-          if (!propertyURI || propertyURI.length <= 0) {
-            errors.push({ category: "propertyURI", message: "Property URI must be set." });
-          } else if (propertyURI.match(/http[s]?:\/\/.+/) === null) {
-            errors.push({ category: "propertyURI", message: "Property URI should be an HTTP(S) URI." });
-          }
-
-          var propertyLabel = this.get("propertyLabel");
+          return;
+        }
 
-          if (!propertyLabel || propertyLabel.length <= 0) {
-            errors.push({ category: "propertyLabel", message: "Property Label must be set." });
-          }
+        var propertyURI = this.get("propertyURI");
+
+        if (!propertyURI || propertyURI.length <= 0) {
+          errors.push({
+            category: "propertyURI",
+            message: "Property URI must be set.",
+          });
+        } else if (propertyURI.match(/http[s]?:\/\/.+/) === null) {
+          errors.push({
+            category: "propertyURI",
+            message: "Property URI should be an HTTP(S) URI.",
+          });
+        }
 
-          var valueURI = this.get("valueURI");
+        var propertyLabel = this.get("propertyLabel");
 
-          if (!valueURI || valueURI.length <= 0) {
-            errors.push({ category: "valueURI", message: "Value URI must be set." });
-          } else if (valueURI.match(/http[s]?:\/\/.+/) === null) {
-            errors.push({ category: "valueURI", message: "Value URI should be an HTTP(S) URI." });
-          }
+        if (!propertyLabel || propertyLabel.length <= 0) {
+          errors.push({
+            category: "propertyLabel",
+            message: "Property Label must be set.",
+          });
+        }
 
-          var valueLabel = this.get("valueLabel");
+        var valueURI = this.get("valueURI");
+
+        if (!valueURI || valueURI.length <= 0) {
+          errors.push({
+            category: "valueURI",
+            message: "Value URI must be set.",
+          });
+        } else if (valueURI.match(/http[s]?:\/\/.+/) === null) {
+          errors.push({
+            category: "valueURI",
+            message: "Value URI should be an HTTP(S) URI.",
+          });
+        }
 
-          if (!valueLabel || valueLabel.length <= 0) {
-            errors.push({ category: "valueLabel", message: "Value Label must be set." });
-          }
+        var valueLabel = this.get("valueLabel");
 
-          if (errors.length === 0) {
-            this.trigger("valid");
+        if (!valueLabel || valueLabel.length <= 0) {
+          errors.push({
+            category: "valueLabel",
+            message: "Value Label must be set.",
+          });
+        }
 
-            return;
-          }
+        if (errors.length === 0) {
+          this.trigger("valid");
 
-          return errors;
-        },
+          return;
+        }
 
-        updateDOM: function (objectDOM) {
-          objectDOM = document.createElement("annotation");
+        return errors;
+      },
 
-          if (this.get("propertyURI")) {
-            var propertyURIEl = document.createElement("propertyuri");
-            $(propertyURIEl).html(this.get("propertyURI"));
+      updateDOM: function (objectDOM) {
+        objectDOM = document.createElement("annotation");
 
-            if (this.get("propertyLabel")) {
-              $(propertyURIEl).attr("label", this.get("propertyLabel"));
-            }
+        if (this.get("propertyURI")) {
+          var propertyURIEl = document.createElement("propertyuri");
+          $(propertyURIEl).html(this.get("propertyURI"));
 
-            $(objectDOM).append(propertyURIEl);
+          if (this.get("propertyLabel")) {
+            $(propertyURIEl).attr("label", this.get("propertyLabel"));
           }
 
-          if (this.get("valueURI")) {
-            var valueURIEl = document.createElement("valueuri");
-            $(valueURIEl).html(this.get("valueURI"));
+          $(objectDOM).append(propertyURIEl);
+        }
 
-            if (this.get("valueLabel")) {
-              $(valueURIEl).attr("label", this.get("valueLabel"));
-            }
+        if (this.get("valueURI")) {
+          var valueURIEl = document.createElement("valueuri");
+          $(valueURIEl).html(this.get("valueURI"));
 
-            $(objectDOM).append(valueURIEl);
+          if (this.get("valueLabel")) {
+            $(valueURIEl).attr("label", this.get("valueLabel"));
           }
 
-          return objectDOM;
-        },
-
-        formatXML: function (xmlString) {
-          return DataONEObject.prototype.formatXML.call(this, xmlString);
-        },
-
-        /**
-         * isEmpty
-         *
-         * Check whether the model's properties are all empty for the purpose
-         * of skipping the model during serialization to avoid invalid EML
-         * documents.
-         *
-         * @return {boolean} - Returns true if all child elements have no
-         * content
-         */
-        isEmpty: function () {
-          return (typeof this.get("propertyLabel") !== "string" || this.get("propertyLabel").length <= 0) &&
-            (typeof this.get("propertyURI") !== "string" || this.get("propertyURI").length <= 0) &&
-            (typeof this.get("valueLabel") !== "string" || this.get("valueLabel").length <= 0) &&
-            (typeof this.get("valueURI") !== "string" || this.get("valueURI").length <= 0)
-        },
-
-        /* Let the top level package know of attribute changes from this object */
-        trickleUpChange: function () {
-          MetacatUI.rootDataPackage.packageModel.set("changed", true);
+          $(objectDOM).append(valueURIEl);
         }
-      });
 
-    return EMLAnnotation;
-  }
-);
+        return objectDOM;
+      },
+
+      formatXML: function (xmlString) {
+        return DataONEObject.prototype.formatXML.call(this, xmlString);
+      },
+
+      /**
+       * isEmpty
+       *
+       * Check whether the model's properties are all empty for the purpose
+       * of skipping the model during serialization to avoid invalid EML
+       * documents.
+       *
+       * @return {boolean} - Returns true if all child elements have no
+       * content
+       */
+      isEmpty: function () {
+        return (
+          (typeof this.get("propertyLabel") !== "string" ||
+            this.get("propertyLabel").length <= 0) &&
+          (typeof this.get("propertyURI") !== "string" ||
+            this.get("propertyURI").length <= 0) &&
+          (typeof this.get("valueLabel") !== "string" ||
+            this.get("valueLabel").length <= 0) &&
+          (typeof this.get("valueURI") !== "string" ||
+            this.get("valueURI").length <= 0)
+        );
+      },
+
+      /* Let the top level package know of attribute changes from this object */
+      trickleUpChange: function () {
+        MetacatUI.rootDataPackage.packageModel.set("changed", true);
+      },
+    },
+  );
+
+  return EMLAnnotation;
+});
 

diff --git a/docs/docs/src_js_models_metadata_eml211_EMLAttribute.js.html b/docs/docs/src_js_models_metadata_eml211_EMLAttribute.js.html index 8cb0f8402..2413cc46a 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLAttribute.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLAttribute.js.html @@ -44,566 +44,622 @@

Source: src/js/models/metadata/eml211/EMLAttribute.js

-
define(["jquery", "underscore", "backbone", "uuid",
-        "models/metadata/eml211/EMLMeasurementScale", "models/metadata/eml211/EMLAnnotation",
-        "collections/metadata/eml/EMLMissingValueCodes",
-        "models/DataONEObject"],
-    function ($, _, Backbone, uuid, EMLMeasurementScale, EMLAnnotation,
-        EMLMissingValueCodes,
-        DataONEObject) {
-
-        /**
-         * @class EMLAttribute
-         * @classdesc EMLAttribute represents a data attribute within an entity, such as
-         * a column variable in a data table, or a feature attribute in a shapefile.
-         * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html
-         * @classcategory Models/Metadata/EML211
-         */
-        var EMLAttribute = Backbone.Model.extend(
-          /** @lends EMLAttribute.prototype */{
-
-            /* Attributes of an EML attribute object */
-            defaults: function(){
-            	return {
-	                /* Attributes from EML */
-	                xmlID: null, // The XML id of the attribute
-	                attributeName: null,
-	                attributeLabel: [], // Zero or more human readable labels
-	                attributeDefinition: null,
-	                storageType: [], // Zero or more storage types
-	                typeSystem: [], // Zero or more system types for storage type
-	                measurementScale: null, // An EML{Non}NumericDomain or EMLDateTimeDomain object
-	                missingValueCodes: new EMLMissingValueCodes(), // An EMLMissingValueCodes collection
-	                accuracy: null, // An EMLAccuracy object
-	                coverage: null, // an EMLCoverage object
-	                methods: [], // Zero or more EMLMethods objects
-                    references: null, // A reference to another EMLAttribute by id (needs work)
-                    annotation: [], // Zero or more EMLAnnotation objects
-
-	                /* Attributes not from EML */
-	                type: "attribute", // The element type in the DOM
-	                parentModel: null, // The parent model this attribute belongs to
-	                objectXML: null, // The serialized XML of this EML attribute
-	                objectDOM: null,  // The DOM of this EML attribute
-	                nodeOrder: [ // The order of the top level XML element nodes
-	                    "attributeName",
-	                    "attributeLabel",
-	                    "attributeDefinition",
-	                    "storageType",
-	                    "measurementScale",
-	                    "missingValueCode",
-	                    "accuracy",
-	                    "coverage",
-	                    "methods",
-	                    "annotation"
-	                ]
-            	}
-            },
-
-            /*
-             * The map of lower case to camel case node names
-             * needed to deal with parsing issues with $.parseHTML().
-             * Use this until we can figure out issues with $.parseXML().
-             */
-            nodeNameMap: {
-                "attributename": "attributeName",
-                "attributelabel": "attributeLabel",
-                "attributedefinition": "attributeDefinition",
-                "sourced" : "source",
-                "storagetype": "storageType",
-                "typesystem": "typeSystem",
-                "measurementscale": "measurementScale",
-                "missingvaluecode": "missingValueCode",
-                "propertyuri": "propertyURI",
-                "valueuri" : "valueURI"
-            },
-
-            /* Initialize an EMLAttribute object */
-            initialize: function(attributes, options) {
-                
-                if (!attributes) {
-                    var attributes = {};
-                }
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "uuid",
+  "models/metadata/eml211/EMLMeasurementScale",
+  "models/metadata/eml211/EMLAnnotation",
+  "collections/metadata/eml/EMLMissingValueCodes",
+  "models/DataONEObject",
+], function (
+  $,
+  _,
+  Backbone,
+  uuid,
+  EMLMeasurementScale,
+  EMLAnnotation,
+  EMLMissingValueCodes,
+  DataONEObject,
+) {
+  /**
+   * @class EMLAttribute
+   * @classdesc EMLAttribute represents a data attribute within an entity, such as
+   * a column variable in a data table, or a feature attribute in a shapefile.
+   * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html
+   * @classcategory Models/Metadata/EML211
+   */
+  var EMLAttribute = Backbone.Model.extend(
+    /** @lends EMLAttribute.prototype */ {
+      /* Attributes of an EML attribute object */
+      defaults: function () {
+        return {
+          /* Attributes from EML */
+          xmlID: null, // The XML id of the attribute
+          attributeName: null,
+          attributeLabel: [], // Zero or more human readable labels
+          attributeDefinition: null,
+          storageType: [], // Zero or more storage types
+          typeSystem: [], // Zero or more system types for storage type
+          measurementScale: null, // An EML{Non}NumericDomain or EMLDateTimeDomain object
+          missingValueCodes: new EMLMissingValueCodes(), // An EMLMissingValueCodes collection
+          accuracy: null, // An EMLAccuracy object
+          coverage: null, // an EMLCoverage object
+          methods: [], // Zero or more EMLMethods objects
+          references: null, // A reference to another EMLAttribute by id (needs work)
+          annotation: [], // Zero or more EMLAnnotation objects
+
+          /* Attributes not from EML */
+          type: "attribute", // The element type in the DOM
+          parentModel: null, // The parent model this attribute belongs to
+          objectXML: null, // The serialized XML of this EML attribute
+          objectDOM: null, // The DOM of this EML attribute
+          nodeOrder: [
+            // The order of the top level XML element nodes
+            "attributeName",
+            "attributeLabel",
+            "attributeDefinition",
+            "storageType",
+            "measurementScale",
+            "missingValueCode",
+            "accuracy",
+            "coverage",
+            "methods",
+            "annotation",
+          ],
+        };
+      },
+
+      /*
+       * The map of lower case to camel case node names
+       * needed to deal with parsing issues with $.parseHTML().
+       * Use this until we can figure out issues with $.parseXML().
+       */
+      nodeNameMap: {
+        attributename: "attributeName",
+        attributelabel: "attributeLabel",
+        attributedefinition: "attributeDefinition",
+        sourced: "source",
+        storagetype: "storageType",
+        typesystem: "typeSystem",
+        measurementscale: "measurementScale",
+        missingvaluecode: "missingValueCode",
+        propertyuri: "propertyURI",
+        valueuri: "valueURI",
+      },
+
+      /* Initialize an EMLAttribute object */
+      initialize: function (attributes, options) {
+        if (!attributes) {
+          var attributes = {};
+        }
+
+        // If initialized with missingValueCode as an array, convert it to a collection
+        if (
+          attributes.missingValueCodes &&
+          attributes.missingValueCodes instanceof Array
+        ) {
+          this.missingValueCodes = new EMLMissingValueCodes(
+            attributes.missingValueCode,
+          );
+        }
+
+        this.stopListening(this.get("missingValueCodes"));
+        this.listenTo(
+          this.get("missingValueCodes"),
+          "update",
+          this.trickleUpChange,
+        );
+        this.on(
+          "change:attributeName " +
+            "change:attributeLabel " +
+            "change:attributeDefinition " +
+            "change:storageType " +
+            "change:measurementScale " +
+            "change:missingValueCodes " +
+            "change:accuracy " +
+            "change:coverage " +
+            "change:methods " +
+            "change:references " +
+            "change:annotation",
+          this.trickleUpChange,
+        );
+      },
+
+      /*
+       * Parse the incoming attribute's XML elements
+       */
+      parse: function (attributes, options) {
+        var $objectDOM;
+
+        if (attributes.objectDOM) {
+          $objectDOM = $(attributes.objectDOM);
+        } else if (attributes.objectXML) {
+          $objectDOM = $(attributes.objectXML);
+        } else {
+          return {};
+        }
+
+        // Add the XML id
+        if (typeof $objectDOM.attr("id") !== "undefined") {
+          attributes.xmlID = $objectDOM.attr("id");
+        }
+
+        // Add the attributeName
+        attributes.attributeName = $objectDOM.children("attributename").text();
+
+        // Add the attributeLabel
+        attributes.attributeLabel = [];
+        var attributeLabels = $objectDOM.children("attributelabel");
+        _.each(attributeLabels, function (attributeLabel) {
+          attributes.attributeLabel.push(attributeLabel.textContent);
+        });
+
+        // Add the attributeDefinition
+        attributes.attributeDefinition = $objectDOM
+          .children("attributedefinition")
+          .text();
+
+        // Add the storageType
+        attributes.storageType = [];
+        attributes.typeSystem = [];
+        var storageTypes = $objectDOM.children("storagetype");
+        _.each(storageTypes, function (storageType) {
+          attributes.storageType.push(storageType.textContent);
+          var type = $(storageType).attr("typesystem");
+          attributes.typeSystem.push(type || null);
+        });
 
-                // If initialized with missingValueCode as an array, convert it to a collection
+        var measurementScale = $objectDOM.find("measurementscale")[0];
+        if (measurementScale) {
+          attributes.measurementScale = EMLMeasurementScale.getInstance(
+            measurementScale.outerHTML,
+          );
+          attributes.measurementScale.set("parentModel", this);
+        }
+
+        // Add annotations
+        var annotations = $objectDOM.children("annotation");
+        attributes.annotation = [];
+
+        _.each(
+          annotations,
+          function (anno) {
+            annotation = new EMLAnnotation(
+              {
+                objectDOM: anno,
+                objectXML: anno.outerHTML,
+              },
+              { parse: true },
+            );
+
+            attributes.annotation.push(annotation);
+          },
+          this,
+        );
+
+        // Add the missingValueCodes as a collection
+        attributes.missingValueCodes = new EMLMissingValueCodes();
+        attributes.missingValueCodes.parse(
+          $objectDOM.children("missingvaluecode"),
+        );
+
+        attributes.objectDOM = $objectDOM[0];
+
+        return attributes;
+      },
+
+      serialize: function () {
+        var objectDOM = this.updateDOM(),
+          xmlString = objectDOM.outerHTML;
+
+        //Camel-case the XML
+        xmlString = this.formatXML(xmlString);
+
+        return xmlString;
+      },
+
+      /* Copy the original XML and update fields in a DOM object */
+      updateDOM: function (objectDOM) {
+        var nodeToInsertAfter;
+        var type = this.get("type") || "attribute";
+        if (!objectDOM) {
+          objectDOM = this.get("objectDOM");
+        }
+        var objectXML = this.get("objectXML");
+
+        // If present, use the cached DOM
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+
+          // otherwise, use the cached XML
+        } else if (objectXML) {
+          objectDOM = $(objectXML)[0].cloneNode(true);
+
+          // This is new, create it
+        } else {
+          objectDOM = document.createElement(type);
+        }
+
+        // update the id attribute
+        var xmlID = this.get("xmlID");
+        if (xmlID) {
+          $(objectDOM).attr("id", xmlID);
+        }
+
+        // Update the attributeName
+        if (
+          typeof this.get("attributeName") == "string" &&
+          this.get("attributeName").trim().length
+        ) {
+          if ($(objectDOM).find("attributename").length) {
+            $(objectDOM).find("attributename").text(this.get("attributeName"));
+          } else {
+            nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeName");
+
+            if (!nodeToInsertAfter) {
+              $(objectDOM).append(
+                $(document.createElement("attributename")).text(
+                  this.get("attributeName"),
+                )[0],
+              );
+            } else {
+              $(nodeToInsertAfter).after(
+                $(document.createElement("attributename")).text(
+                  this.get("attributeName"),
+                )[0],
+              );
+            }
+          }
+        }
+        //If there is no attribute name, return an empty string because it
+        // is invalid
+        else {
+          return "";
+        }
+
+        // Update the attributeLabels
+        nodeToInsertAfter = undefined;
+        var attributeLabels = this.get("attributeLabel");
+        if (attributeLabels) {
+          if (attributeLabels.length) {
+            // Copy and reverse the array for inserting
+            attributeLabels = Array.from(attributeLabels).reverse();
+            // Remove all current attributeLabels
+            $(objectDOM).find("attributelabel").remove();
+            nodeToInsertAfter = this.getEMLPosition(
+              objectDOM,
+              "attributeLabel",
+            );
+
+            if (!nodeToInsertAfter) {
+              // Add the new list back in
+              _.each(attributeLabels, function (attributeLabel) {
+                //If there is an empty string or falsey value in the label, don't add it to the XML
+                // We check purposefuly for falsey types (instead of just doing !attributeLabel) because
+                // it's ok to serialize labels that are the number 0.
                 if (
-                    attributes.missingValueCodes &&
-                    attributes.missingValueCodes instanceof Array
+                  (typeof attributeLabel == "string" &&
+                    !attributeLabel.trim().length) ||
+                  attributeLabel === false ||
+                  attributeLabel === null ||
+                  typeof attributeLabel == "undefined"
                 ) {
-                    this.missingValueCodes =
-                        new EMLMissingValueCodes(attributes.missingValueCode);
-                }   
-
-                this.stopListening(this.get("missingValueCodes"));
-                this.listenTo(
-                    this.get("missingValueCodes"),
-                    "update",
-                    this.trickleUpChange
-                )
-                this.on(
-                    "change:attributeName " +
-                    "change:attributeLabel " +
-                    "change:attributeDefinition " +
-                    "change:storageType " +
-                    "change:measurementScale " +
-                    "change:missingValueCodes " +
-                    "change:accuracy " +
-                    "change:coverage " +
-                    "change:methods " +
-                    "change:references " +
-                    "change:annotation",
-                    this.trickleUpChange);
-            },
-
-            /*
-             * Parse the incoming attribute's XML elements
-             */
-            parse: function(attributes, options) {
-                var $objectDOM;
-
-                if ( attributes.objectDOM ) {
-                    $objectDOM = $(attributes.objectDOM);
-                } else if ( attributes.objectXML ) {
-                    $objectDOM = $(attributes.objectXML);
-                } else {
-                    return {};
-                }
-
-                // Add the XML id
-                if ( typeof $objectDOM.attr("id") !== "undefined" ) {
-                    attributes.xmlID = $objectDOM.attr("id");
-                }
-
-                // Add the attributeName
-                attributes.attributeName = $objectDOM.children("attributename").text();
-
-                // Add the attributeLabel
-                attributes.attributeLabel = [];
-                var attributeLabels = $objectDOM.children("attributelabel");
-                _.each(attributeLabels, function(attributeLabel) {
-                    attributes.attributeLabel.push(attributeLabel.textContent);
-                });
-
-                // Add the attributeDefinition
-                attributes.attributeDefinition = $objectDOM.children("attributedefinition").text();
-
-                // Add the storageType
-                attributes.storageType = [];
-                attributes.typeSystem = [];
-                var storageTypes = $objectDOM.children("storagetype");
-                _.each(storageTypes, function(storageType) {
-                    attributes.storageType.push(storageType.textContent);
-                    var type = $(storageType).attr("typesystem");
-                    attributes.typeSystem.push(type || null);
-                });
-
-                var measurementScale = $objectDOM.find("measurementscale")[0];
-                if ( measurementScale ) {
-                    attributes.measurementScale =
-                        EMLMeasurementScale.getInstance(measurementScale.outerHTML);
-                    attributes.measurementScale.set("parentModel", this);
+                  return;
                 }
 
-                // Add annotations
-                var annotations = $objectDOM.children("annotation");
-                attributes.annotation = [];
-
-                _.each(annotations, function(anno) {
-                    annotation = new EMLAnnotation({
-                            objectDOM: anno,
-                            objectXML: anno.outerHTML
-                    }, { parse: true });
-
-                    attributes.annotation.push(annotation);
-                }, this);
-
-                // Add the missingValueCodes as a collection
-                attributes.missingValueCodes = new EMLMissingValueCodes();
-                attributes.missingValueCodes.parse(
-                    $objectDOM.children("missingvaluecode")
+                $(objectDOM).append(
+                  $(document.createElement("attributelabel")).text(
+                    attributeLabel,
+                  )[0],
                 );
-
-                attributes.objectDOM = $objectDOM[0];
-
-                return attributes;
-            },
-
-            serialize: function(){
-            	var objectDOM = this.updateDOM(),
-					xmlString = objectDOM.outerHTML;
-
-				//Camel-case the XML
-		    	xmlString = this.formatXML(xmlString);
-
-		    	return xmlString;
-            },
-
-            /* Copy the original XML and update fields in a DOM object */
-            updateDOM: function(objectDOM){
-
-                var nodeToInsertAfter;
-                var type = this.get("type") || "attribute";
-                if ( ! objectDOM ) {
-                    objectDOM = this.get("objectDOM");
-                }
-                var objectXML = this.get("objectXML");
-
-                // If present, use the cached DOM
-                if ( objectDOM ) {
-                    objectDOM = objectDOM.cloneNode(true);
-
-                // otherwise, use the cached XML
-                } else if ( objectXML ){
-                    objectDOM = $(objectXML)[0].cloneNode(true);
-
-                // This is new, create it
-                } else {
-                    objectDOM = document.createElement(type);
-                }
-
-                // update the id attribute
-                var xmlID = this.get("xmlID");
-                if ( xmlID ) {
-                    $(objectDOM).attr("id", xmlID);
-                }
-
-                // Update the attributeName
-                if ( typeof this.get("attributeName") == "string" && this.get("attributeName").trim().length ) {
-                    if ( $(objectDOM).find("attributename").length ) {
-                        $(objectDOM).find("attributename").text(this.get("attributeName"));
-                    } else {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeName");
-
-                        if( ! nodeToInsertAfter ) {
-                            $(objectDOM).append($(document.createElement("attributename"))
-                                .text(this.get("attributeName"))[0]);
-                        } else {
-                            $(nodeToInsertAfter).after(
-                                $(document.createElement("attributename")).text(this.get("attributeName"))[0]
-                            );
-                        }
-                    }
-                }
-                //If there is no attribute name, return an empty string because it
-                // is invalid
-                else{
-                  return "";
-                }
-
-                // Update the attributeLabels
-                nodeToInsertAfter = undefined;
-                var attributeLabels = this.get("attributeLabel");
-                if ( attributeLabels ) {
-                    if ( attributeLabels.length ) {
-                        // Copy and reverse the array for inserting
-                        attributeLabels = Array.from(attributeLabels).reverse();
-                        // Remove all current attributeLabels
-                        $(objectDOM).find("attributelabel").remove();
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeLabel");
-
-                        if( ! nodeToInsertAfter ) {
-                            // Add the new list back in
-                            _.each(attributeLabels, function(attributeLabel) {
-
-                              //If there is an empty string or falsey value in the label, don't add it to the XML
-                              // We check purposefuly for falsey types (instead of just doing !attributeLabel) because
-                              // it's ok to serialize labels that are the number 0.
-                              if( (typeof attributeLabel == "string" && !attributeLabel.trim().length) ||
-                                  attributeLabel === false || attributeLabel === null || typeof attributeLabel == "undefined"){
-                                    return;
-                              }
-
-                              $(objectDOM).append(
-                                  $(document.createElement("attributelabel"))
-                                      .text(attributeLabel)[0]);
-                            });
-                        } else {
-                            // Add the new list back in after its previous sibling
-                            _.each(attributeLabels, function(attributeLabel) {
-
-                                //If there is an empty string or falsey value in the label, don't add it to the XML
-                                // We check purposefuly for falsey types (instead of just doing !attributeLabel) because
-                                // it's ok to serialize labels that are the number 0.
-                                if( (typeof attributeLabel == "string" && !attributeLabel.trim().length) ||
-                                    attributeLabel === false || attributeLabel === null || typeof attributeLabel == "undefined"){
-                                      return;
-                                }
-
-                                $(nodeToInsertAfter).after(
-                                    $(document.createElement("attributelabel"))
-                                        .text(attributeLabel)[0]);
-                            });
-                        }
-                    }
-                    //If the label array is empty, remove all the labels from the DOM
-                    else{
-                      $(objectDOM).find("attributelabel").remove();
-                    }
-                }
-                //If there is no attribute label, remove them from the DOM
-                else{
-                  $(objectDOM).find("attributelabel").remove();
-                }
-
-                // Update the attributeDefinition
-                nodeToInsertAfter = undefined;
-                if ( this.get("attributeDefinition") ) {
-                    if ( $(objectDOM).find("attributedefinition").length ) {
-                        $(objectDOM).find("attributedefinition").text(this.get("attributeDefinition"));
-                    } else {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeDefinition");
-
-                        if( ! nodeToInsertAfter ) {
-                            $(objectDOM).append($(document.createElement("attributedefinition"))
-                                .text(this.get("attributeDefinition"))[0]);
-                        } else {
-                            $(nodeToInsertAfter).after($(document.createElement("attributedefinition"))
-                                .text(this.get("attributeDefinition"))[0]);
-                        }
-                    }
-                }
-                // If there is no attribute definition, then return an empty String
-                // because it is invalid
-                else{
-                  return "";
+              });
+            } else {
+              // Add the new list back in after its previous sibling
+              _.each(attributeLabels, function (attributeLabel) {
+                //If there is an empty string or falsey value in the label, don't add it to the XML
+                // We check purposefuly for falsey types (instead of just doing !attributeLabel) because
+                // it's ok to serialize labels that are the number 0.
+                if (
+                  (typeof attributeLabel == "string" &&
+                    !attributeLabel.trim().length) ||
+                  attributeLabel === false ||
+                  attributeLabel === null ||
+                  typeof attributeLabel == "undefined"
+                ) {
+                  return;
                 }
 
-                // Update the storageTypes
-                nodeToInsertAfter = undefined;
-                var storageTypes = this.get("storageTypes");
-                if ( storageTypes ) {
-                    if ( storageTypes.length ) {
-                        // Copy and reverse the array for inserting
-                        storageTypes = Array.from(storageTypes).reverse();
-                        // Remove all current attributeLabels
-                        $(objectDOM).find("storagetype").remove();
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "storageType");
-
-                        if( ! nodeToInsertAfter ) {
-                            // Add the new list back in
-                            _.each(storageTypes, function(storageType) {
-
-                              if(!storageType)
-                                return;
-
-                              $(objectDOM).append(
-                                  $(document.createElement("storagetype"))
-                                      .text(storageType)[0]);
-                            });
-                        } else {
-                            // Add the new list back in after its previous sibling
-                            _.each(storageTypes, function(storageType) {
-
-                              if(!storageType)
-                                return;
-
-                              $(nodeToInsertAfter).after(
-                                  $(document.createElement("storagetype"))
-                                      .text(storageType)[0]);
-                            });
-                        }
-                    }
-                }
-                /*If there are no storage types, remove them all from the DOM.
+                $(nodeToInsertAfter).after(
+                  $(document.createElement("attributelabel")).text(
+                    attributeLabel,
+                  )[0],
+                );
+              });
+            }
+          }
+          //If the label array is empty, remove all the labels from the DOM
+          else {
+            $(objectDOM).find("attributelabel").remove();
+          }
+        }
+        //If there is no attribute label, remove them from the DOM
+        else {
+          $(objectDOM).find("attributelabel").remove();
+        }
+
+        // Update the attributeDefinition
+        nodeToInsertAfter = undefined;
+        if (this.get("attributeDefinition")) {
+          if ($(objectDOM).find("attributedefinition").length) {
+            $(objectDOM)
+              .find("attributedefinition")
+              .text(this.get("attributeDefinition"));
+          } else {
+            nodeToInsertAfter = this.getEMLPosition(
+              objectDOM,
+              "attributeDefinition",
+            );
+
+            if (!nodeToInsertAfter) {
+              $(objectDOM).append(
+                $(document.createElement("attributedefinition")).text(
+                  this.get("attributeDefinition"),
+                )[0],
+              );
+            } else {
+              $(nodeToInsertAfter).after(
+                $(document.createElement("attributedefinition")).text(
+                  this.get("attributeDefinition"),
+                )[0],
+              );
+            }
+          }
+        }
+        // If there is no attribute definition, then return an empty String
+        // because it is invalid
+        else {
+          return "";
+        }
+
+        // Update the storageTypes
+        nodeToInsertAfter = undefined;
+        var storageTypes = this.get("storageTypes");
+        if (storageTypes) {
+          if (storageTypes.length) {
+            // Copy and reverse the array for inserting
+            storageTypes = Array.from(storageTypes).reverse();
+            // Remove all current attributeLabels
+            $(objectDOM).find("storagetype").remove();
+            nodeToInsertAfter = this.getEMLPosition(objectDOM, "storageType");
+
+            if (!nodeToInsertAfter) {
+              // Add the new list back in
+              _.each(storageTypes, function (storageType) {
+                if (!storageType) return;
+
+                $(objectDOM).append(
+                  $(document.createElement("storagetype")).text(storageType)[0],
+                );
+              });
+            } else {
+              // Add the new list back in after its previous sibling
+              _.each(storageTypes, function (storageType) {
+                if (!storageType) return;
+
+                $(nodeToInsertAfter).after(
+                  $(document.createElement("storagetype")).text(storageType)[0],
+                );
+              });
+            }
+          }
+        }
+        /*If there are no storage types, remove them all from the DOM.
                 TODO: Uncomment this out when storage type is supported in editor
                 else{
                   $(objectDOM).find("storagetype").remove();
                 }
                 */
 
-                // Update the measurementScale
-                nodeToInsertAfter = undefined;
-                var measurementScale = this.get("measurementScale");
-                var measurementScaleNodes;
-                var measurementScaleNode;
-                var domainNode;
-                if ( typeof measurementScale !== "undefined" && measurementScale) {
-
-                    // Find the measurementScale child or create a new one
-                    measurementScaleNodes = $(objectDOM).children("measurementscale");
-                    if ( measurementScaleNodes.length ) {
-                        measurementScaleNode = measurementScaleNodes[0];
-
-                    } else {
-                        measurementScaleNode = document.createElement("measurementscale");
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "measurementScale");
-
-                        if ( typeof nodeToInsertAfter === "undefined" ) {
-                            $(objectDOM).append(measurementScaleNode);
-                        } else {
-                            $(nodeToInsertAfter).after(measurementScaleNode);
-                        }
-                    }
-
-                    // Append the measurementScale domain content
-                    domainNode = measurementScale.updateDOM();
-                    if (typeof domainNode !== "undefined" ) {
-                        $(measurementScaleNode).children().remove();
-                        $(measurementScaleNode).append(domainNode);
-                    }
-
-                } else {
-                    console.log("No measurementScale object has been defined.");
-                }
-
-                // Update annotations
-                var annotation = this.get("annotation");
-
-                // Always remove all annotations to start with
-                $(objectDOM).children("annotation").remove();
-
-                _.each(annotation, function(anno) {
-                    if (anno.isEmpty()) {
-                        return;
-                    }
-
-                    var after = this.getEMLPosition(objectDOM, "annotation");
-                    $(after).after(anno.updateDOM());
-                }, this);
-
-                // Update the missingValueCodes
-                nodeToInsertAfter = undefined;
-                var missingValueCodes = this.get("missingValueCodes");
-                $(objectDOM).children("missingvaluecode").remove();
-                if (missingValueCodes) {
-                    var missingValueCodeNodes = missingValueCodes.updateDOM();
-                    if (missingValueCodeNodes) {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "missingValueCode");
-                        if (typeof nodeToInsertAfter === "undefined") {
-                            $(objectDOM).append(missingValueCodeNodes);
-                        } else {
-                            $(nodeToInsertAfter).after(missingValueCodeNodes);
-                        }
-                    }
-                }
-
-                return objectDOM;
-            },
-
-            /*
-             * Get the DOM node preceding the given nodeName
-             * to find what position in the EML document
-             * the named node should be appended
-             */
-            getEMLPosition: function(objectDOM, nodeName) {
-                var nodeOrder = this.get("nodeOrder");
-
-                var position = _.indexOf(nodeOrder, nodeName);
-
-                // Append to the bottom if not found
-                if ( position == -1 ) {
-                    return $(objectDOM).children().last()[0];
-                }
-
-                // Otherwise, go through each node in the node list and find the
-                // position where this node will be inserted after
-                for ( var i = position - 1; i >= 0; i-- ) {
-                    if ( $(objectDOM).find(nodeOrder[i].toLowerCase()).length ) {
-                        return $(objectDOM).find(nodeOrder[i].toLowerCase()).last()[0];
-                    }
-                }
-            },
-
-            formatXML: function(xmlString){
-            	return DataONEObject.prototype.formatXML.call(this, xmlString);
-            },
-
-            validate: function(){
-            	var errors = {};
-
-            	//If there is no attribute name, add that error message
-            	if(!this.get("attributeName"))
-            		errors.attributeName = "Provide a name for this attribute.";
-
-            	//If there is no attribute definition, add that error message
-            	if(!this.get("attributeDefinition"))
-            		errors.attributeDefinition = "Provide a definition for this attribute.";
-
-            	//Get the EML measurement scale model
-            	var measurementScaleModel = this.get("measurementScale");
-
-            	// If there is no measurement scale model, then add that error message
-            	if( !measurementScaleModel ){
-            		errors.measurementScale = "Choose a measurement scale category for this attribute.";
-            	}
-            	else{
-                    if( !measurementScaleModel.isValid() ){
-                		errors.measurementScale = "More information is needed.";
-                	}
-                }
-                
-                // Validate the missing value codes
-                var missingValueCodesErrors = this.get("missingValueCodes")?.validate();
-                if (missingValueCodesErrors) {
-                    // Just display the first error message
-                    errors.missingValueCodes = Object.values(missingValueCodesErrors)[0]
-                }
+        // Update the measurementScale
+        nodeToInsertAfter = undefined;
+        var measurementScale = this.get("measurementScale");
+        var measurementScaleNodes;
+        var measurementScaleNode;
+        var domainNode;
+        if (typeof measurementScale !== "undefined" && measurementScale) {
+          // Find the measurementScale child or create a new one
+          measurementScaleNodes = $(objectDOM).children("measurementscale");
+          if (measurementScaleNodes.length) {
+            measurementScaleNode = measurementScaleNodes[0];
+          } else {
+            measurementScaleNode = document.createElement("measurementscale");
+            nodeToInsertAfter = this.getEMLPosition(
+              objectDOM,
+              "measurementScale",
+            );
+
+            if (typeof nodeToInsertAfter === "undefined") {
+              $(objectDOM).append(measurementScaleNode);
+            } else {
+              $(nodeToInsertAfter).after(measurementScaleNode);
+            }
+          }
+
+          // Append the measurementScale domain content
+          domainNode = measurementScale.updateDOM();
+          if (typeof domainNode !== "undefined") {
+            $(measurementScaleNode).children().remove();
+            $(measurementScaleNode).append(domainNode);
+          }
+        } else {
+          console.log("No measurementScale object has been defined.");
+        }
+
+        // Update annotations
+        var annotation = this.get("annotation");
+
+        // Always remove all annotations to start with
+        $(objectDOM).children("annotation").remove();
+
+        _.each(
+          annotation,
+          function (anno) {
+            if (anno.isEmpty()) {
+              return;
+            }
 
-                // If there is a measurement scale model and it is valid and there are no other
-                // errors, then trigger this model as valid and exit.
-                if (!Object.keys(errors).length) {
-                    this.trigger("valid", this);
-                    return;
-                } else {
-                    //If there is at least one error, then return the errors object
-                    return errors;
-                }
-            },
-
-            /*
-            * Validates each of the EMLAnnotation models on this model
-            *
-            * @return {Array} - Returns an array of error messages for all the EMLAnnotation models
-            */
-           validateAnnotations: function(){
-             var errors = [];
-
-             //Validate each of the EMLAttributes
-             _.each(this.get("annotation"), function (anno) {
-               if (anno.isValid()) {
-                 return;
-               }
-
-               errors.push(anno.validationError);
-             });
-
-             return errors;
-           },
-
-            /*
-            * Climbs up the model heirarchy until it finds the EML model
-            *
-            * @return {EML211 or false} - Returns the EML 211 Model or false if not found
-            */
-            getParentEML: function(){
-              var emlModel = this.get("parentModel"),
-                  tries = 0;
-
-              while (emlModel.type !== "EML" && tries < 6){
-                emlModel = emlModel.get("parentModel");
-                tries++;
-              }
-
-              if( emlModel && emlModel.type == "EML")
-                return emlModel;
-              else
-                return false;
-
-            },
-
-            /* Let the top level package know of attribute changes from this object */
-            trickleUpChange: function(){
-                MetacatUI.rootDataPackage.packageModel.set("changed", true);
-            },
-
-            createID: function() {
-                this.set("xmlID", uuid.v4());
+            var after = this.getEMLPosition(objectDOM, "annotation");
+            $(after).after(anno.updateDOM());
+          },
+          this,
+        );
+
+        // Update the missingValueCodes
+        nodeToInsertAfter = undefined;
+        var missingValueCodes = this.get("missingValueCodes");
+        $(objectDOM).children("missingvaluecode").remove();
+        if (missingValueCodes) {
+          var missingValueCodeNodes = missingValueCodes.updateDOM();
+          if (missingValueCodeNodes) {
+            nodeToInsertAfter = this.getEMLPosition(
+              objectDOM,
+              "missingValueCode",
+            );
+            if (typeof nodeToInsertAfter === "undefined") {
+              $(objectDOM).append(missingValueCodeNodes);
+            } else {
+              $(nodeToInsertAfter).after(missingValueCodeNodes);
             }
+          }
+        }
+
+        return objectDOM;
+      },
+
+      /*
+       * Get the DOM node preceding the given nodeName
+       * to find what position in the EML document
+       * the named node should be appended
+       */
+      getEMLPosition: function (objectDOM, nodeName) {
+        var nodeOrder = this.get("nodeOrder");
+
+        var position = _.indexOf(nodeOrder, nodeName);
+
+        // Append to the bottom if not found
+        if (position == -1) {
+          return $(objectDOM).children().last()[0];
+        }
+
+        // Otherwise, go through each node in the node list and find the
+        // position where this node will be inserted after
+        for (var i = position - 1; i >= 0; i--) {
+          if ($(objectDOM).find(nodeOrder[i].toLowerCase()).length) {
+            return $(objectDOM).find(nodeOrder[i].toLowerCase()).last()[0];
+          }
+        }
+      },
+
+      formatXML: function (xmlString) {
+        return DataONEObject.prototype.formatXML.call(this, xmlString);
+      },
+
+      validate: function () {
+        var errors = {};
+
+        //If there is no attribute name, add that error message
+        if (!this.get("attributeName"))
+          errors.attributeName = "Provide a name for this attribute.";
+
+        //If there is no attribute definition, add that error message
+        if (!this.get("attributeDefinition"))
+          errors.attributeDefinition =
+            "Provide a definition for this attribute.";
+
+        //Get the EML measurement scale model
+        var measurementScaleModel = this.get("measurementScale");
+
+        // If there is no measurement scale model, then add that error message
+        if (!measurementScaleModel) {
+          errors.measurementScale =
+            "Choose a measurement scale category for this attribute.";
+        } else {
+          if (!measurementScaleModel.isValid()) {
+            errors.measurementScale = "More information is needed.";
+          }
+        }
+
+        // Validate the missing value codes
+        var missingValueCodesErrors = this.get("missingValueCodes")?.validate();
+        if (missingValueCodesErrors) {
+          // Just display the first error message
+          errors.missingValueCodes = Object.values(missingValueCodesErrors)[0];
+        }
+
+        // If there is a measurement scale model and it is valid and there are no other
+        // errors, then trigger this model as valid and exit.
+        if (!Object.keys(errors).length) {
+          this.trigger("valid", this);
+          return;
+        } else {
+          //If there is at least one error, then return the errors object
+          return errors;
+        }
+      },
+
+      /*
+       * Validates each of the EMLAnnotation models on this model
+       *
+       * @return {Array} - Returns an array of error messages for all the EMLAnnotation models
+       */
+      validateAnnotations: function () {
+        var errors = [];
+
+        //Validate each of the EMLAttributes
+        _.each(this.get("annotation"), function (anno) {
+          if (anno.isValid()) {
+            return;
+          }
+
+          errors.push(anno.validationError);
         });
 
-        return EMLAttribute;
-    }
-);
+        return errors;
+      },
+
+      /*
+       * Climbs up the model heirarchy until it finds the EML model
+       *
+       * @return {EML211 or false} - Returns the EML 211 Model or false if not found
+       */
+      getParentEML: function () {
+        var emlModel = this.get("parentModel"),
+          tries = 0;
+
+        while (emlModel.type !== "EML" && tries < 6) {
+          emlModel = emlModel.get("parentModel");
+          tries++;
+        }
+
+        if (emlModel && emlModel.type == "EML") return emlModel;
+        else return false;
+      },
+
+      /* Let the top level package know of attribute changes from this object */
+      trickleUpChange: function () {
+        MetacatUI.rootDataPackage.packageModel.set("changed", true);
+      },
+
+      createID: function () {
+        this.set("xmlID", uuid.v4());
+      },
+    },
+  );
+
+  return EMLAttribute;
+});
 
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLDataTable.js.html b/docs/docs/src_js_models_metadata_eml211_EMLDataTable.js.html index 58c66c338..5ea187728 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLDataTable.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLDataTable.js.html @@ -44,241 +44,259 @@

Source: src/js/models/metadata/eml211/EMLDataTable.js

-
define(["jquery", "underscore", "backbone", "models/metadata/eml211/EMLEntity"],
-    function($, _, Backbone, EMLEntity) {
-
-        /**
-        * @class EMLDataTable
-         * @classdesc EMLDataTable represents a tabular data entity, corresponding
-         * with the EML dataTable module.
-         * @classcategory Models/Metadata/EML211
-         * @see https://eml.ecoinformatics.org/schema/eml-datatable_xsd
-         * @extends EMLEntity
-         */
-        var EMLDataTable = EMLEntity.extend(
-          /** @lends EMLDataTable.prototype */{
-
-            //The class name for this model
-            type: "EMLDataTable",
-
-            /* Attributes of any entity */
-            defaults: function(){
-                return    _.extend({
-
-                        /* Attributes from EML */
-                        caseSensitive: null, // The case sensitivity of the table records
-                        numberOfRecords: null, // the number of records in the table
-                        type: "dataTable",
-
-                        /* Attributes not from EML */
-                        nodeOrder: [ // The order of the top level XML element nodes
-                            "caseSensitive",
-                            "numberOfRecords",
-                            "references"
-                        ],
-
-                    }, EMLEntity.prototype.defaults());
-            },
-
-            /*
-             * The map of lower case to camel case node names
-             * needed to deal with parsing issues with $.parseHTML().
-             * Use this until we can figure out issues with $.parseXML().
-             */
-            nodeNameMap: _.extend({
-                "casesensitive" : "caseSensitive",
-                "numberofrecords": "numberOfRecords"
-
-            }, EMLEntity.prototype.nodeNameMap),
-
-            /* Initialize an EMLDataTable object */
-            initialize: function(attributes) {
-
-                // if options.parse = true, Backbone will call parse()
-
-                // Call super() first
-                this.constructor.__super__.initialize.apply(this, [attributes]);
-
-                // EMLDataTable-specific work
-                this.set("type", "dataTable", {silent: true});
-
-                // Register change events
-                this.on( "change:caseSensitive change:numberOfRecords", EMLEntity.trickleUpChange);
-
-            },
-
-            /*
-             * Parse the incoming other entity's XML elements
-             */
-            parse: function(attributes, options) {
-
-                var attributes = attributes || {};
-
-                // Call super() first
-                attributes = this.constructor.__super__.parse.apply(this, [attributes, options]);
-
-                // EMLDataTable-specific work
-                var objectXML  = attributes.objectXML; // The dataTable XML fragment
-                var objectDOM; // The W3C DOM of the object XML fragment
-                var $objectDOM; // The JQuery object of the XML fragment
-
-                // Use the updated objectDOM if we have it
-                if ( attributes.objectDOM ) {
-                    $objectDOM = $(attributes.objectDOM);
-                } else {
-                    // Hmm, oddly not there, start from scratch =/
-                    $objectDOM = $(objectXML);
-                }
-
-                // Add the caseSensitive
-                attributes.caseSensitive = $objectDOM.children("caseSensitive").text();
-
-                // Add the numberOfRecords
-                attributes.numberOfRecords = $objectDOM.children("numberOfRecords").text();
-
-                // Add the references value
-                attributes.references = $objectDOM.children("references").text();
-
-                return attributes;
-            },
-
-            /* Copy the original XML and update fields in a DOM object */
-            updateDOM: function(objectDOM) {
-                var nodeToInsertAfter;
-                var type = this.get("type") || "dataTable";
-                if ( ! objectDOM ) {
-                    objectDOM = this.get("objectDOM");
-                }
-                var objectXML = this.get("objectXML");
-
-                // If present, use the cached DOM
-                if ( objectDOM ) {
-                    objectDOM = objectDOM.cloneNode(true);
-
-                // otherwise, use the cached XML
-                } else if ( objectXML ){
-                    objectDOM = $(objectXML)[0].cloneNode(true);
-
-                // This is new, create it
-                } else {
-                    objectDOM = document.createElement(type);
-
-                }
-
-                // Now call the superclass
-                objectDOM = this.constructor.__super__.updateDOM.apply(this, [objectDOM]);
-
-                // And then update the EMLDataTable-specific fields
-                // Update the caseSensitive field
-                if ( this.get("caseSensitive") ) {
-                    if ( $(objectDOM).find("caseSensitive").length ) {
-                        $(objectDOM).find("caseSensitive").text(this.get("caseSensitive"));
-
-                    } else {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "caseSensitive");
-
-                        if ( ! nodeToInsertAfter ) {
-                            $(objectDOM).append($(document.createElement("casesensitive"))
-                                .text(this.get("caseSensitive"))[0]);
-                        } else {
-                            $(nodeToInsertAfter).after($(document.createElement("casesensitive"))
-                                .text(this.get("caseSensitive"))[0]);
-                        }
-                    }
-                }
-
-                // Update the numberOfRecords field
-                if ( this.get("numberOfRecords") ) {
-                    if ( $(objectDOM).find("numberOfRecords").length ) {
-                        $(objectDOM).find("numberOfRecords").text(this.get("numberOfRecords"));
-
-                    } else {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "numberOfRecords");
-
-                        if ( ! nodeToInsertAfter ) {
-                            $(objectDOM).append($(document.createElement("numberofrecords"))
-                                .text(this.get("numberOfRecords"))[0]);
-                        } else {
-                            $(nodeToInsertAfter).after($(document.createElement("numberofrecords"))
-                                .text(this.get("numberOfRecords"))[0]);
-                        }
-                    }
-                }
-
-                return objectDOM;
-            },
-
-            /* Serialize the EML DOM to XML */
-            serialize: function() {
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/metadata/eml211/EMLEntity",
+], function ($, _, Backbone, EMLEntity) {
+  /**
+   * @class EMLDataTable
+   * @classdesc EMLDataTable represents a tabular data entity, corresponding
+   * with the EML dataTable module.
+   * @classcategory Models/Metadata/EML211
+   * @see https://eml.ecoinformatics.org/schema/eml-datatable_xsd
+   * @extends EMLEntity
+   */
+  var EMLDataTable = EMLEntity.extend(
+    /** @lends EMLDataTable.prototype */ {
+      //The class name for this model
+      type: "EMLDataTable",
+
+      /* Attributes of any entity */
+      defaults: function () {
+        return _.extend(
+          {
+            /* Attributes from EML */
+            caseSensitive: null, // The case sensitivity of the table records
+            numberOfRecords: null, // the number of records in the table
+            type: "dataTable",
+
+            /* Attributes not from EML */
+            nodeOrder: [
+              // The order of the top level XML element nodes
+              "caseSensitive",
+              "numberOfRecords",
+              "references",
+            ],
+          },
+          EMLEntity.prototype.defaults(),
+        );
+      },
+
+      /*
+       * The map of lower case to camel case node names
+       * needed to deal with parsing issues with $.parseHTML().
+       * Use this until we can figure out issues with $.parseXML().
+       */
+      nodeNameMap: _.extend(
+        {
+          casesensitive: "caseSensitive",
+          numberofrecords: "numberOfRecords",
+        },
+        EMLEntity.prototype.nodeNameMap,
+      ),
+
+      /* Initialize an EMLDataTable object */
+      initialize: function (attributes) {
+        // if options.parse = true, Backbone will call parse()
+
+        // Call super() first
+        this.constructor.__super__.initialize.apply(this, [attributes]);
+
+        // EMLDataTable-specific work
+        this.set("type", "dataTable", { silent: true });
+
+        // Register change events
+        this.on(
+          "change:caseSensitive change:numberOfRecords",
+          EMLEntity.trickleUpChange,
+        );
+      },
+
+      /*
+       * Parse the incoming other entity's XML elements
+       */
+      parse: function (attributes, options) {
+        var attributes = attributes || {};
+
+        // Call super() first
+        attributes = this.constructor.__super__.parse.apply(this, [
+          attributes,
+          options,
+        ]);
+
+        // EMLDataTable-specific work
+        var objectXML = attributes.objectXML; // The dataTable XML fragment
+        var objectDOM; // The W3C DOM of the object XML fragment
+        var $objectDOM; // The JQuery object of the XML fragment
+
+        // Use the updated objectDOM if we have it
+        if (attributes.objectDOM) {
+          $objectDOM = $(attributes.objectDOM);
+        } else {
+          // Hmm, oddly not there, start from scratch =/
+          $objectDOM = $(objectXML);
+        }
+
+        // Add the caseSensitive
+        attributes.caseSensitive = $objectDOM.children("caseSensitive").text();
+
+        // Add the numberOfRecords
+        attributes.numberOfRecords = $objectDOM
+          .children("numberOfRecords")
+          .text();
+
+        // Add the references value
+        attributes.references = $objectDOM.children("references").text();
+
+        return attributes;
+      },
+
+      /* Copy the original XML and update fields in a DOM object */
+      updateDOM: function (objectDOM) {
+        var nodeToInsertAfter;
+        var type = this.get("type") || "dataTable";
+        if (!objectDOM) {
+          objectDOM = this.get("objectDOM");
+        }
+        var objectXML = this.get("objectXML");
+
+        // If present, use the cached DOM
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+
+          // otherwise, use the cached XML
+        } else if (objectXML) {
+          objectDOM = $(objectXML)[0].cloneNode(true);
+
+          // This is new, create it
+        } else {
+          objectDOM = document.createElement(type);
+        }
+
+        // Now call the superclass
+        objectDOM = this.constructor.__super__.updateDOM.apply(this, [
+          objectDOM,
+        ]);
+
+        // And then update the EMLDataTable-specific fields
+        // Update the caseSensitive field
+        if (this.get("caseSensitive")) {
+          if ($(objectDOM).find("caseSensitive").length) {
+            $(objectDOM).find("caseSensitive").text(this.get("caseSensitive"));
+          } else {
+            nodeToInsertAfter = this.getEMLPosition(objectDOM, "caseSensitive");
+
+            if (!nodeToInsertAfter) {
+              $(objectDOM).append(
+                $(document.createElement("casesensitive")).text(
+                  this.get("caseSensitive"),
+                )[0],
+              );
+            } else {
+              $(nodeToInsertAfter).after(
+                $(document.createElement("casesensitive")).text(
+                  this.get("caseSensitive"),
+                )[0],
+              );
+            }
+          }
+        }
+
+        // Update the numberOfRecords field
+        if (this.get("numberOfRecords")) {
+          if ($(objectDOM).find("numberOfRecords").length) {
+            $(objectDOM)
+              .find("numberOfRecords")
+              .text(this.get("numberOfRecords"));
+          } else {
+            nodeToInsertAfter = this.getEMLPosition(
+              objectDOM,
+              "numberOfRecords",
+            );
+
+            if (!nodeToInsertAfter) {
+              $(objectDOM).append(
+                $(document.createElement("numberofrecords")).text(
+                  this.get("numberOfRecords"),
+                )[0],
+              );
+            } else {
+              $(nodeToInsertAfter).after(
+                $(document.createElement("numberofrecords")).text(
+                  this.get("numberOfRecords"),
+                )[0],
+              );
+            }
+          }
+        }
 
-                var xmlString = "";
+        return objectDOM;
+      },
 
-                // Update the superclass fields in the objectDOM first
-                var objectDOM = this.constructor.__super__.updateDOM.apply(this, []);
-
-                // Then update the subclass fields in the objectDOM
-                // TODO
+      /* Serialize the EML DOM to XML */
+      serialize: function () {
+        var xmlString = "";
 
+        // Update the superclass fields in the objectDOM first
+        var objectDOM = this.constructor.__super__.updateDOM.apply(this, []);
 
-                this.set("objectXML", xmlString);
+        // Then update the subclass fields in the objectDOM
+        // TODO
 
-                return xmlString;
-            },
+        this.set("objectXML", xmlString);
 
-            /* Validate the datable's required fields */
-            validate: function(){
+        return xmlString;
+      },
 
-              var errors = {};
+      /* Validate the datable's required fields */
+      validate: function () {
+        var errors = {};
 
-              // Require the entity name
-              if( !this.get("entityName") ) {
-                  errors.entityName = "Please specify an data table name.";
-              }
+        // Require the entity name
+        if (!this.get("entityName")) {
+          errors.entityName = "Please specify an data table name.";
+        }
 
-              //Validate the attributes
-              var attributeErrors = this.validateAttributes();
-              if(attributeErrors.length)
-                errors.attributeList = errors;
+        //Validate the attributes
+        var attributeErrors = this.validateAttributes();
+        if (attributeErrors.length) errors.attributeList = errors;
 
-              // Require the attribute list
-              /*if( !this.get("attributeList").length ) {
+        // Require the attribute list
+        /*if( !this.get("attributeList").length ) {
                   errors.attributeList = "Please describe the table attributes (columns).";
               }*/
 
-              if( Object.keys(errors).length ){
-                return errors;
-              }
-              else{
-                return false;
-              }
-            },
-
-            /*
-            * Climbs up the model heirarchy until it finds the EML model
-            *
-            * @return {EML211 or false} - Returns the EML 211 Model or false if not found
-            */
-            getParentEML: function(){
-              var emlModel = this.get("parentModel"),
-                  tries = 0;
-
-              while (emlModel.type !== "EML" && tries < 6){
-                emlModel = emlModel.get("parentModel");
-                tries++;
-              }
-
-              if( emlModel && emlModel.type == "EML")
-                return emlModel;
-              else
-                return false;
-
-            }
-
-        });
-
-        return EMLDataTable;
-    }
-);
+        if (Object.keys(errors).length) {
+          return errors;
+        } else {
+          return false;
+        }
+      },
+
+      /*
+       * Climbs up the model heirarchy until it finds the EML model
+       *
+       * @return {EML211 or false} - Returns the EML 211 Model or false if not found
+       */
+      getParentEML: function () {
+        var emlModel = this.get("parentModel"),
+          tries = 0;
+
+        while (emlModel.type !== "EML" && tries < 6) {
+          emlModel = emlModel.get("parentModel");
+          tries++;
+        }
+
+        if (emlModel && emlModel.type == "EML") return emlModel;
+        else return false;
+      },
+    },
+  );
+
+  return EMLDataTable;
+});
 
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLDistribution.js.html b/docs/docs/src_js_models_metadata_eml211_EMLDistribution.js.html index 5c84fb830..e935268a5 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLDistribution.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLDistribution.js.html @@ -44,12 +44,11 @@

Source: src/js/models/metadata/eml211/EMLDistribution.js<
-
/* global define */
-define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
   $,
   _,
   Backbone,
-  DataONEObject
+  DataONEObject,
 ) {
   /**
    * @class EMLDistribution
@@ -62,7 +61,7 @@ 

Source: src/js/models/metadata/eml211/EMLDistribution.js< * @constructor */ var EMLDistribution = Backbone.Model.extend( - /** @lends EMLDistribution.prototype */{ + /** @lends EMLDistribution.prototype */ { /** * Default values for an EML 211 Distribution model. This is essentially a * flattened version of the EML 2.1.1 DistributionType, including nodes and @@ -119,7 +118,12 @@

Source: src/js/models/metadata/eml211/EMLDistribution.js< * @type {string[]} * @since 2.26.0 */ - offlineNodes: ["mediumname", "mediumvolume", "mediumformat", "mediumnote"], + offlineNodes: [ + "mediumname", + "mediumvolume", + "mediumformat", + "mediumnote", + ], /** * lower-case EML node names that belong within the <online> node. These @@ -146,7 +150,7 @@

Source: src/js/models/metadata/eml211/EMLDistribution.js< this.listenTo( this, "change:" + nodeAttr.join(" change:"), - this.trickleUpChange + this.trickleUpChange, ); }, @@ -296,7 +300,7 @@

Source: src/js/models/metadata/eml211/EMLDistribution.js< // Add the urlFunction attribute if one is set in the model. Remove it if // it's not set. - const url = $objectDOM.find("url") + const url = $objectDOM.find("url"); if (url) { const urlFunction = this.get("urlFunction"); if (urlFunction) { @@ -306,7 +310,6 @@

Source: src/js/models/metadata/eml211/EMLDistribution.js< } } - return objectDOM; }, @@ -370,7 +373,8 @@

Source: src/js/models/metadata/eml211/EMLDistribution.js< formatXML: function (xmlString) { return DataONEObject.prototype.formatXML.call(this, xmlString); }, - }); + }, + ); return EMLDistribution; }); diff --git a/docs/docs/src_js_models_metadata_eml211_EMLEntity.js.html b/docs/docs/src_js_models_metadata_eml211_EMLEntity.js.html index 62e519dc8..2ebe81cb5 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLEntity.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLEntity.js.html @@ -44,538 +44,567 @@

Source: src/js/models/metadata/eml211/EMLEntity.js

-
define(["jquery", "underscore", "backbone", "uuid", "models/DataONEObject",
-        "models/metadata/eml211/EMLAttribute"],
-    function($, _, Backbone, uuid, DataONEObject, EMLAttribute) {
-
-        /**
-         * @class EMLEntity
-         * @classdesc EMLEntity represents an abstract data entity, corresponding
-         * with the EML EntityGroup and other elements common to all
-         * entity types, including otherEntity, dataTable, spatialVector,
-         * spatialRaster, and storedProcedure
-         * @classcategory Models/Metadata/EML211
-         * @see https://eml.ecoinformatics.org/schema/eml-entity_xsd
-         * @extends Backbone.Model
-         */
-        var EMLEntity = Backbone.Model.extend(
-          /** @lends EMLEntity.prototype */{
-
-        	//The class name for this model
-        	type: "EMLEntity",
-
-            /* Attributes of any entity */
-            defaults: function(){
-            	return {
-	                /* Attributes from EML */
-	                xmlID: null, // The XML id of the entity
-	                alternateIdentifier: [], // Zero or more alt ids
-	                entityName: null, // Required, the name of the entity
-	                entityDescription: null, // Description of the entity
-	                physical: [], // Zero to many EMLPhysical objects
-	                physicalMD5Checksum: null,
-	                physicalSize: null,
-	                physicalObjectName: null,
-	                coverage: [], // Zero to many EML{Geo|Taxon|Temporal}Coverage objects
-	                methods: null, // Zero or one EMLMethod object
-	                additionalInfo: [], // Zero to many EMLText objects
-	                attributeList: [], // Zero to many EMLAttribute objects
-	                constraint: [], // Zero to many EMLConstraint objects
-	                references: null, // A reference to another EMLEntity by id (needs work)
-
-	                //Temporary attribute until we implement the eml-physical module
-	                downloadID: null,
-	                formatName: null,
-
-	                /* Attributes not from EML */
-	                nodeOrder: [ // The order of the top level XML element nodes
-	                    "alternateIdentifier",
-	                    "entityName",
-	                    "entityDescription",
-	                    "physical",
-	                    "coverage",
-	                    "methods",
-	                    "additionalInfo",
-	                    "annotation",
-	                    "attributeList",
-	                    "constraint"
-	                ],
-	                parentModel: null, // The parent model this entity belongs to
-	                dataONEObject: null, //Reference to the DataONEObject this EMLEntity describes
-	                objectXML: null, // The serialized XML of this EML entity
-	                objectDOM: null,  // The DOM of this EML entity
-                  type: "otherentity"
-            	}
-            },
-
-            /*
-             * The map of lower case to camel case node names
-             * needed to deal with parsing issues with $.parseHTML().
-             * Use this until we can figure out issues with $.parseXML().
-             */
-            nodeNameMap: {
-                "alternateidentifier": "alternateIdentifier",
-                "entityname": "entityName",
-                "entitydescription": "entityDescription",
-                "additionalinfo": "additionalInfo",
-                "attributelist": "attributeList"
-            },
-
-            /* Initialize an EMLEntity object */
-            initialize: function(attributes, options) {
-
-                // if options.parse = true, Backbone will call parse()
-
-                // Register change events
-                this.on(
-                    "change:alternateIdentifier " +
-                    "change:entityName " +
-                    "change:entityDescription " +
-                    "change:physical " +
-                    "change:coverage " +
-                    "change:methods " +
-                    "change:additionalInfo " +
-                    "change:attributeList " +
-                    "change:constraint " +
-                    "change:references",
-                    EMLEntity.trickleUpChange);
-
-                //Listen to changes on the DataONEObject file name
-                if(this.get("dataONEObject")){
-                  this.listenTo(this.get("dataONEObject"), "change:fileName", this.updateFileName);
-                }
-
-                //Listen to changes on the DataONEObject to reset the listener
-                this.on("change:dataONEObject", function(entity, dataONEObj){
-
-                  //Stop listening to the old DataONEObject
-                  if(this.previous("dataONEObject")){
-                    this.stopListening(this.previous("dataONEObject"), "change:fileName");
-                  }
-
-                  //Listen to changes on the file name
-                  this.listenTo(dataONEObj, "change:fileName", this.updateFileName);
-                });
-
-            },
-
-            /*
-             * Parse the incoming entity's common XML elements
-             * Content example:
-             * <otherEntity>
-             *     <alternateIdentifier>file-alt.1.1.txt</alternateIdentifier>
-             *     <alternateIdentifier>file-again.1.1.txt</alternateIdentifier>
-             *     <entityName>file.1.1.txt</entityName>
-             *     <entityDescription>A file summary</entityDescription>
-             * </otherEntity>
-             */
-            parse: function(attributes, options) {
-                var $objectDOM;
-                var objectDOM = attributes.objectDOM;
-                var objectXML = attributes.objectXML;
-
-                // Use the cached object if we have it
-                if ( objectDOM ) {
-                    $objectDOM = $(objectDOM);
-                } else if ( objectXML ) {
-                    $objectDOM = $(objectXML);
-                }
-
-                // Add the XML id
-                attributes.xmlID = $objectDOM.attr("id");
-
-                // Add the alternateIdentifiers
-                attributes.alternateIdentifier = [];
-                var alternateIds = $objectDOM.children("alternateidentifier");
-                _.each(alternateIds, function(alternateId) {
-                    attributes.alternateIdentifier.push(alternateId.textContent);
-                });
-
-                // Add the entityName
-                attributes.entityName = $objectDOM.children("entityname").text();
-
-                // Add the entityDescription
-                attributes.entityDescription = $objectDOM.children("entitydescription").text();
-
-                //Get some physical attributes from the EMLPhysical module
-                var physical = $objectDOM.find("physical");
-                if(physical){
-                	attributes.physicalSize = physical.find("size").text();
-                	attributes.physicalObjectName = physical.find("objectname").text();
-
-                	var checksumType = physical.find("authentication").attr("method");
-                	if(checksumType == "MD5")
-                		attributes.physicalMD5Checksum = physical.find("authentication").text();
-                }
-
-                attributes.objectXML = objectXML;
-                attributes.objectDOM = $objectDOM[0];
-
-                //Find the id from the download distribution URL
-                var urlNode = $objectDOM.find("url");
-                if(urlNode.length){
-                	var downloadURL = urlNode.text(),
-                		downloadID  = "";
-
-                	if( downloadURL.indexOf("/resolve/") > -1 )
-                		downloadID = downloadURL.substring( downloadURL.indexOf("/resolve/") + 9 );
-                	else if( downloadURL.indexOf("/object/") > -1 )
-                		downloadID = downloadURL.substring( downloadURL.indexOf("/object/") + 8 );
-                	else if( downloadURL.indexOf("ecogrid") > -1 ){
-                		var withoutEcoGridPrefix = downloadURL.substring( downloadURL.indexOf("ecogrid://") + 10 ),
-							downloadID = withoutEcoGridPrefix.substring( withoutEcoGridPrefix.indexOf("/")+1 );
-                	}
-
-
-                	if(downloadID.length)
-                        attributes.downloadID = downloadID;
-                }
-
-                //Find the format name
-                var formatNode = $objectDOM.find("formatName");
-                if(formatNode.length){
-                	attributes.formatName = formatNode.text();
-                }
-
-                // Add the attributeList
-                var attributeList = $objectDOM.find("attributelist");
-                var attribute; // An individual EML attribute
-                var options = {parse: true};
-                attributes.attributeList = [];
-                if ( attributeList.length ) {
-                    _.each(attributeList[0].children, function(attr) {
-                        attribute = new EMLAttribute(
-                            {
-                                objectDOM: attr,
-                                objectXML: attr.outerHTML,
-                                parentModel: this
-                            }, options);
-                        // Can't use this.addAttribute() here (no this yet)
-                        attributes.attributeList.push(attribute);
-                    }, this);
-
-                }
-                return attributes;
-            },
-
-            /*
-             * Add an attribute to the attributeList, inserting it
-             * at the zero-based index
-             */
-            addAttribute: function(attribute, index) {
-                if ( typeof index == "undefined" ) {
-                    this.get("attributeList").push(attribute);
-                } else {
-                    this.get("attributeList").splice(index, attribute);
-                }
-
-                this.trigger("change:attributeList");
-            },
-
-            /*
-             * Remove an EMLAttribute model from the attributeList array
-             *
-             * @param {EMLAttribute} - The EMLAttribute model to remove from this model's attributeList
-             */
-            removeAttribute: function(attribute) {
-
-              //Get the index of the EMLAttribute in the array
-            	var attrIndex = this.get("attributeList").indexOf(attribute);
-
-              //If this attribute model does not exist in the attribute list, don't do anything
-              if( attrIndex == -1 ){
-                return;
-              }
-
-              //Remove that index from the array
-            	this.get("attributeList").splice(attrIndex, 1);
-
-              //Trickle the change up the model chain
-              this.trickleUpChange();
-            },
-
-            /* Validate the top level EMLEntity fields */
-            validate: function() {
-                var errors = {};
-
-                // will be run by calls to isValid()
-                if ( ! this.get("entityName") ) {
-                    errors.entityName = "An entity name is required.";
-                }
-
-                //Validate the attributes
-                var attributeErrors = this.validateAttributes();
-                if(attributeErrors.length)
-                  errors.attributeList = attributeErrors;
-
-                if( Object.keys(errors).length )
-                  return errors;
-                else{
-                  this.trigger("valid");
-                  return false;
-                }
-
-            },
-
-            /*
-            * Validates each of the EMLAttribute models in the attributeList
-            *
-            * @return {Array} - Returns an array of error messages for all the EMlAttribute models
-            */
-            validateAttributes: function(){
-              var errors = [];
-
-              //Validate each of the EMLAttributes
-              _.each( this.get("attributeList"), function(attribute){
-
-                if( !attribute.isValid() ){
-                  errors.push(attribute.validationError);
-                }
-
-              });
-
-              return errors;
-            },
-
-            /* Copy the original XML and update fields in a DOM object */
-            updateDOM: function(objectDOM) {
-                var nodeToInsertAfter;
-                var type = this.get("type") || "otherEntity";
-                if ( ! objectDOM ) {
-                    objectDOM = this.get("objectDOM");
-                }
-                var objectXML = this.get("objectXML");
-
-                // If present, use the cached DOM
-                if ( objectDOM ) {
-                    objectDOM = objectDOM.cloneNode(true);
-
-                // otherwise, use the cached XML
-                } else if ( objectXML ){
-                    objectDOM = $(objectXML)[0].cloneNode(true);
-
-                // This is new, create it
-                } else {
-                    objectDOM = document.createElement(type);
-                }
-
-                //Update the id attribute on this XML node
-                // update the id attribute
-               if( this.get("dataONEObject") ){
-                 //Ideally, the EMLEntity will use the object's id in it's id attribute, so we wil switch them
-                 var xmlID = this.get("dataONEObject").getXMLSafeID();
-
-                 //Set the xml-safe id on the model and use it as the id attribute
-                 $(objectDOM).attr("id", xmlID);
-                 this.set("xmlID", xmlID);
-               }
-               //If there isn't a matching DataONEObject but there is an id set on this model, use that id
-               else if(this.get("xmlID")){
-                $(objectDOM).attr("id", this.get("xmlID"));
-               }
-
-                // Update the alternateIdentifiers
-                var altIDs = this.get("alternateIdentifier");
-                if ( altIDs ) {
-                    if ( altIDs.length ) {
-                        // Copy and reverse the array for prepending
-                        altIDs = Array.from(altIDs).reverse();
-                        // Remove all current alternateIdentifiers
-                        $(objectDOM).find("alternateIdentifier").remove();
-                        // Add the new list back in
-                        _.each(altIDs, function(altID) {
-                            $(objectDOM).prepend(
-                                $(document.createElement("alternateIdentifier"))
-                                    .text(altID));
-                        });
-                    }
-                }
-                else{
-
-                  // Remove all current alternateIdentifiers
-                  $(objectDOM).find("alternateIdentifier").remove();
-
-                }
-
-                // Update the entityName
-                if ( this.get("entityName") ) {
-                    if ( $(objectDOM).find("entityName").length ) {
-                        $(objectDOM).find("entityName").text(this.get("entityName"));
-
-                    } else {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "entityName");
-                        if ( ! nodeToInsertAfter ) {
-                            $(objectDOM).append($(document.createElement("entityName"))
-                                .text(this.get("entityName"))[0]);
-                        } else {
-                            $(nodeToInsertAfter).after($(document.createElement("entityName"))
-                                .text(this.get("entityName"))[0]);
-                        }
-                    }
-                }
-
-                // Update the entityDescription
-                if ( this.get("entityDescription") ) {
-                    if ( $(objectDOM).find("entityDescription").length ) {
-                        $(objectDOM).find("entityDescription").text(this.get("entityDescription"));
-
-                    } else {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "entityDescription");
-                        if ( ! nodeToInsertAfter ) {
-                            $(objectDOM).append($(document.createElement("entityDescription"))
-                                .text(this.get("entityDescription"))[0]);
-                        } else {
-                            $(nodeToInsertAfter).after($(document.createElement("entityDescription"))
-                                .text(this.get("entityDescription"))[0]);
-                        }
-                    }
-                }
-                //If there is no entity description
-                else{
-
-                  //If there is an entity description node in the XML, remove it
-                  $(objectDOM).find("entityDescription").remove();
-
-                }
-
-                // TODO: Update the physical section
-
-                // TODO: Update the coverage section
-
-                // TODO: Update the methods section
-
-
-                // Update the additionalInfo
-                var addInfos = this.get("additionalInfo");
-                if ( addInfos ) {
-                    if ( addInfos.length ) {
-                        // Copy and reverse the array for prepending
-                        addInfos = Array.from(addInfos).reverse();
-                        // Remove all current alternateIdentifiers
-                        $(objectDOM).find("additionalInfo").remove();
-                        // Add the new list back in
-                        _.each(addInfos, function(additionalInfo) {
-                            $(objectDOM).prepend(
-                                document.createElement("additionalInfo")
-                                    .text(additionalInfo));
-                        });
-                    }
-                }
-
-                // Update the attributeList section
-                let attributeList = this.get("attributeList");
-                let attributeListInDOM = $(objectDOM).children("attributelist");
-                let attributeListNode;
-                if ( attributeListInDOM.length ) {
-                    attributeListNode = attributeListInDOM[0];
-                    $(attributeListNode).children().remove(); // Each attr will be replaced
-                } else {
-                    attributeListNode = document.createElement("attributeList");
-                    nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeList");
-                    if( ! nodeToInsertAfter ) {
-                        $(objectDOM).append(attributeListNode);
-                    } else {
-                        $(nodeToInsertAfter).after(attributeListNode);
-                    }
-                }
-
-                var updatedAttrDOM;
-                if ( attributeList.length ) {
-                    // Add each attribute
-                    _.each(attributeList, function(attribute) {
-                            updatedAttrDOM = attribute.updateDOM();
-                            $(attributeListNode).append(updatedAttrDOM);
-                    }, this);
-                } else {
-                    // Attributes are not defined, remove them from the DOM
-                    attributeListNode.remove();
-                }
-
-                // TODO: Update the constraint section
-
-                return objectDOM;
-            },
-
-            /**
-            * Update the file name in the EML
-            */
-            updateFileName: function(){
-
-              var dataONEObj = this.get("dataONEObject");
-
-              //Get the DataONEObject model associated with this EML Entity
-              if(dataONEObj){
-                //If the last file name matched the EML entity name, then update it
-                if( dataONEObj.previous("fileName") == this.get("entityName") ){
-                  this.set("entityName", dataONEObj.get("fileName"));
-                }
-                //If the DataONEObject doesn't have an old file name or entity name, then update it
-                else if( !dataONEObj.previous("fileName") || !this.get("entityName") ){
-                  this.set("entityName", dataONEObj.get("fileName"));
-                }
-              }
-
-            },
-
-            /*
-             * Get the DOM node preceding the given nodeName
-             * to find what position in the EML document
-             * the named node should be appended
-             */
-            getEMLPosition: function(objectDOM, nodeName) {
-                var nodeOrder = this.get("nodeOrder");
-
-                var position = _.indexOf(nodeOrder, nodeName);
-
-                // Append to the bottom if not found
-                if ( position == -1 ) {
-                    return $(objectDOM).children().last()[0];
-                }
-
-                // Otherwise, go through each node in the node list and find the
-                // position where this node will be inserted after
-                for ( var i = position - 1; i >= 0; i-- ) {
-                    if ( $(objectDOM).find( nodeOrder[i].toLowerCase() ).length ) {
-                        return $(objectDOM).find(nodeOrder[i].toLowerCase()).last()[0];
-                    }
-                }
-            },
-
-            /*
-            * Climbs up the model heirarchy until it finds the EML model
-            *
-            * @return {EML211 or false} - Returns the EML 211 Model or false if not found
-            */
-            getParentEML: function(){
-              var emlModel = this.get("parentModel"),
-                  tries = 0;
-
-              while (emlModel.type !== "EML" && tries < 6){
-                emlModel = emlModel.get("parentModel");
-                tries++;
-              }
-
-              if( emlModel && emlModel.type == "EML")
-                return emlModel;
-              else
-                return false;
-
-            },
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "uuid",
+  "models/DataONEObject",
+  "models/metadata/eml211/EMLAttribute",
+], function ($, _, Backbone, uuid, DataONEObject, EMLAttribute) {
+  /**
+   * @class EMLEntity
+   * @classdesc EMLEntity represents an abstract data entity, corresponding
+   * with the EML EntityGroup and other elements common to all
+   * entity types, including otherEntity, dataTable, spatialVector,
+   * spatialRaster, and storedProcedure
+   * @classcategory Models/Metadata/EML211
+   * @see https://eml.ecoinformatics.org/schema/eml-entity_xsd
+   * @extends Backbone.Model
+   */
+  var EMLEntity = Backbone.Model.extend(
+    /** @lends EMLEntity.prototype */ {
+      //The class name for this model
+      type: "EMLEntity",
+
+      /* Attributes of any entity */
+      defaults: function () {
+        return {
+          /* Attributes from EML */
+          xmlID: null, // The XML id of the entity
+          alternateIdentifier: [], // Zero or more alt ids
+          entityName: null, // Required, the name of the entity
+          entityDescription: null, // Description of the entity
+          physical: [], // Zero to many EMLPhysical objects
+          physicalMD5Checksum: null,
+          physicalSize: null,
+          physicalObjectName: null,
+          coverage: [], // Zero to many EML{Geo|Taxon|Temporal}Coverage objects
+          methods: null, // Zero or one EMLMethod object
+          additionalInfo: [], // Zero to many EMLText objects
+          attributeList: [], // Zero to many EMLAttribute objects
+          constraint: [], // Zero to many EMLConstraint objects
+          references: null, // A reference to another EMLEntity by id (needs work)
+
+          //Temporary attribute until we implement the eml-physical module
+          downloadID: null,
+          formatName: null,
+
+          /* Attributes not from EML */
+          nodeOrder: [
+            // The order of the top level XML element nodes
+            "alternateIdentifier",
+            "entityName",
+            "entityDescription",
+            "physical",
+            "coverage",
+            "methods",
+            "additionalInfo",
+            "annotation",
+            "attributeList",
+            "constraint",
+          ],
+          parentModel: null, // The parent model this entity belongs to
+          dataONEObject: null, //Reference to the DataONEObject this EMLEntity describes
+          objectXML: null, // The serialized XML of this EML entity
+          objectDOM: null, // The DOM of this EML entity
+          type: "otherentity",
+        };
+      },
+
+      /*
+       * The map of lower case to camel case node names
+       * needed to deal with parsing issues with $.parseHTML().
+       * Use this until we can figure out issues with $.parseXML().
+       */
+      nodeNameMap: {
+        alternateidentifier: "alternateIdentifier",
+        entityname: "entityName",
+        entitydescription: "entityDescription",
+        additionalinfo: "additionalInfo",
+        attributelist: "attributeList",
+      },
+
+      /* Initialize an EMLEntity object */
+      initialize: function (attributes, options) {
+        // if options.parse = true, Backbone will call parse()
+
+        // Register change events
+        this.on(
+          "change:alternateIdentifier " +
+            "change:entityName " +
+            "change:entityDescription " +
+            "change:physical " +
+            "change:coverage " +
+            "change:methods " +
+            "change:additionalInfo " +
+            "change:attributeList " +
+            "change:constraint " +
+            "change:references",
+          EMLEntity.trickleUpChange,
+        );
+
+        //Listen to changes on the DataONEObject file name
+        if (this.get("dataONEObject")) {
+          this.listenTo(
+            this.get("dataONEObject"),
+            "change:fileName",
+            this.updateFileName,
+          );
+        }
+
+        //Listen to changes on the DataONEObject to reset the listener
+        this.on("change:dataONEObject", function (entity, dataONEObj) {
+          //Stop listening to the old DataONEObject
+          if (this.previous("dataONEObject")) {
+            this.stopListening(
+              this.previous("dataONEObject"),
+              "change:fileName",
+            );
+          }
+
+          //Listen to changes on the file name
+          this.listenTo(dataONEObj, "change:fileName", this.updateFileName);
+        });
+      },
+
+      /*
+       * Parse the incoming entity's common XML elements
+       * Content example:
+       * <otherEntity>
+       *     <alternateIdentifier>file-alt.1.1.txt</alternateIdentifier>
+       *     <alternateIdentifier>file-again.1.1.txt</alternateIdentifier>
+       *     <entityName>file.1.1.txt</entityName>
+       *     <entityDescription>A file summary</entityDescription>
+       * </otherEntity>
+       */
+      parse: function (attributes, options) {
+        var $objectDOM;
+        var objectDOM = attributes.objectDOM;
+        var objectXML = attributes.objectXML;
+
+        // Use the cached object if we have it
+        if (objectDOM) {
+          $objectDOM = $(objectDOM);
+        } else if (objectXML) {
+          $objectDOM = $(objectXML);
+        }
+
+        // Add the XML id
+        attributes.xmlID = $objectDOM.attr("id");
+
+        // Add the alternateIdentifiers
+        attributes.alternateIdentifier = [];
+        var alternateIds = $objectDOM.children("alternateidentifier");
+        _.each(alternateIds, function (alternateId) {
+          attributes.alternateIdentifier.push(alternateId.textContent);
+        });
 
-            /*Format the EML XML for entities*/
-            formatXML: function(xmlString){
-                return DataONEObject.prototype.formatXML.call(this, xmlString);
+        // Add the entityName
+        attributes.entityName = $objectDOM.children("entityname").text();
+
+        // Add the entityDescription
+        attributes.entityDescription = $objectDOM
+          .children("entitydescription")
+          .text();
+
+        //Get some physical attributes from the EMLPhysical module
+        var physical = $objectDOM.find("physical");
+        if (physical) {
+          attributes.physicalSize = physical.find("size").text();
+          attributes.physicalObjectName = physical.find("objectname").text();
+
+          var checksumType = physical.find("authentication").attr("method");
+          if (checksumType == "MD5")
+            attributes.physicalMD5Checksum = physical
+              .find("authentication")
+              .text();
+        }
+
+        attributes.objectXML = objectXML;
+        attributes.objectDOM = $objectDOM[0];
+
+        //Find the id from the download distribution URL
+        var urlNode = $objectDOM.find("url");
+        if (urlNode.length) {
+          var downloadURL = urlNode.text(),
+            downloadID = "";
+
+          if (downloadURL.indexOf("/resolve/") > -1)
+            downloadID = downloadURL.substring(
+              downloadURL.indexOf("/resolve/") + 9,
+            );
+          else if (downloadURL.indexOf("/object/") > -1)
+            downloadID = downloadURL.substring(
+              downloadURL.indexOf("/object/") + 8,
+            );
+          else if (downloadURL.indexOf("ecogrid") > -1) {
+            var withoutEcoGridPrefix = downloadURL.substring(
+                downloadURL.indexOf("ecogrid://") + 10,
+              ),
+              downloadID = withoutEcoGridPrefix.substring(
+                withoutEcoGridPrefix.indexOf("/") + 1,
+              );
+          }
+
+          if (downloadID.length) attributes.downloadID = downloadID;
+        }
+
+        //Find the format name
+        var formatNode = $objectDOM.find("formatName");
+        if (formatNode.length) {
+          attributes.formatName = formatNode.text();
+        }
+
+        // Add the attributeList
+        var attributeList = $objectDOM.find("attributelist");
+        var attribute; // An individual EML attribute
+        var options = { parse: true };
+        attributes.attributeList = [];
+        if (attributeList.length) {
+          _.each(
+            attributeList[0].children,
+            function (attr) {
+              attribute = new EMLAttribute(
+                {
+                  objectDOM: attr,
+                  objectXML: attr.outerHTML,
+                  parentModel: this,
+                },
+                options,
+              );
+              // Can't use this.addAttribute() here (no this yet)
+              attributes.attributeList.push(attribute);
             },
-
-	        /* Let the top level package know of attribute changes from this object */
-	        trickleUpChange: function(){
-	            MetacatUI.rootDataPackage.packageModel.set("changed", true);
-	        }
+            this,
+          );
+        }
+        return attributes;
+      },
+
+      /*
+       * Add an attribute to the attributeList, inserting it
+       * at the zero-based index
+       */
+      addAttribute: function (attribute, index) {
+        if (typeof index == "undefined") {
+          this.get("attributeList").push(attribute);
+        } else {
+          this.get("attributeList").splice(index, attribute);
+        }
+
+        this.trigger("change:attributeList");
+      },
+
+      /*
+       * Remove an EMLAttribute model from the attributeList array
+       *
+       * @param {EMLAttribute} - The EMLAttribute model to remove from this model's attributeList
+       */
+      removeAttribute: function (attribute) {
+        //Get the index of the EMLAttribute in the array
+        var attrIndex = this.get("attributeList").indexOf(attribute);
+
+        //If this attribute model does not exist in the attribute list, don't do anything
+        if (attrIndex == -1) {
+          return;
+        }
+
+        //Remove that index from the array
+        this.get("attributeList").splice(attrIndex, 1);
+
+        //Trickle the change up the model chain
+        this.trickleUpChange();
+      },
+
+      /* Validate the top level EMLEntity fields */
+      validate: function () {
+        var errors = {};
+
+        // will be run by calls to isValid()
+        if (!this.get("entityName")) {
+          errors.entityName = "An entity name is required.";
+        }
+
+        //Validate the attributes
+        var attributeErrors = this.validateAttributes();
+        if (attributeErrors.length) errors.attributeList = attributeErrors;
+
+        if (Object.keys(errors).length) return errors;
+        else {
+          this.trigger("valid");
+          return false;
+        }
+      },
+
+      /*
+       * Validates each of the EMLAttribute models in the attributeList
+       *
+       * @return {Array} - Returns an array of error messages for all the EMlAttribute models
+       */
+      validateAttributes: function () {
+        var errors = [];
+
+        //Validate each of the EMLAttributes
+        _.each(this.get("attributeList"), function (attribute) {
+          if (!attribute.isValid()) {
+            errors.push(attribute.validationError);
+          }
         });
 
-        return EMLEntity;
-    }
-);
+        return errors;
+      },
+
+      /* Copy the original XML and update fields in a DOM object */
+      updateDOM: function (objectDOM) {
+        var nodeToInsertAfter;
+        var type = this.get("type") || "otherEntity";
+        if (!objectDOM) {
+          objectDOM = this.get("objectDOM");
+        }
+        var objectXML = this.get("objectXML");
+
+        // If present, use the cached DOM
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+
+          // otherwise, use the cached XML
+        } else if (objectXML) {
+          objectDOM = $(objectXML)[0].cloneNode(true);
+
+          // This is new, create it
+        } else {
+          objectDOM = document.createElement(type);
+        }
+
+        //Update the id attribute on this XML node
+        // update the id attribute
+        if (this.get("dataONEObject")) {
+          //Ideally, the EMLEntity will use the object's id in it's id attribute, so we wil switch them
+          var xmlID = this.get("dataONEObject").getXMLSafeID();
+
+          //Set the xml-safe id on the model and use it as the id attribute
+          $(objectDOM).attr("id", xmlID);
+          this.set("xmlID", xmlID);
+        }
+        //If there isn't a matching DataONEObject but there is an id set on this model, use that id
+        else if (this.get("xmlID")) {
+          $(objectDOM).attr("id", this.get("xmlID"));
+        }
+
+        // Update the alternateIdentifiers
+        var altIDs = this.get("alternateIdentifier");
+        if (altIDs) {
+          if (altIDs.length) {
+            // Copy and reverse the array for prepending
+            altIDs = Array.from(altIDs).reverse();
+            // Remove all current alternateIdentifiers
+            $(objectDOM).find("alternateIdentifier").remove();
+            // Add the new list back in
+            _.each(altIDs, function (altID) {
+              $(objectDOM).prepend(
+                $(document.createElement("alternateIdentifier")).text(altID),
+              );
+            });
+          }
+        } else {
+          // Remove all current alternateIdentifiers
+          $(objectDOM).find("alternateIdentifier").remove();
+        }
+
+        // Update the entityName
+        if (this.get("entityName")) {
+          if ($(objectDOM).find("entityName").length) {
+            $(objectDOM).find("entityName").text(this.get("entityName"));
+          } else {
+            nodeToInsertAfter = this.getEMLPosition(objectDOM, "entityName");
+            if (!nodeToInsertAfter) {
+              $(objectDOM).append(
+                $(document.createElement("entityName")).text(
+                  this.get("entityName"),
+                )[0],
+              );
+            } else {
+              $(nodeToInsertAfter).after(
+                $(document.createElement("entityName")).text(
+                  this.get("entityName"),
+                )[0],
+              );
+            }
+          }
+        }
+
+        // Update the entityDescription
+        if (this.get("entityDescription")) {
+          if ($(objectDOM).find("entityDescription").length) {
+            $(objectDOM)
+              .find("entityDescription")
+              .text(this.get("entityDescription"));
+          } else {
+            nodeToInsertAfter = this.getEMLPosition(
+              objectDOM,
+              "entityDescription",
+            );
+            if (!nodeToInsertAfter) {
+              $(objectDOM).append(
+                $(document.createElement("entityDescription")).text(
+                  this.get("entityDescription"),
+                )[0],
+              );
+            } else {
+              $(nodeToInsertAfter).after(
+                $(document.createElement("entityDescription")).text(
+                  this.get("entityDescription"),
+                )[0],
+              );
+            }
+          }
+        }
+        //If there is no entity description
+        else {
+          //If there is an entity description node in the XML, remove it
+          $(objectDOM).find("entityDescription").remove();
+        }
+
+        // TODO: Update the physical section
+
+        // TODO: Update the coverage section
+
+        // TODO: Update the methods section
+
+        // Update the additionalInfo
+        var addInfos = this.get("additionalInfo");
+        if (addInfos) {
+          if (addInfos.length) {
+            // Copy and reverse the array for prepending
+            addInfos = Array.from(addInfos).reverse();
+            // Remove all current alternateIdentifiers
+            $(objectDOM).find("additionalInfo").remove();
+            // Add the new list back in
+            _.each(addInfos, function (additionalInfo) {
+              $(objectDOM).prepend(
+                document.createElement("additionalInfo").text(additionalInfo),
+              );
+            });
+          }
+        }
+
+        // Update the attributeList section
+        let attributeList = this.get("attributeList");
+        let attributeListInDOM = $(objectDOM).children("attributelist");
+        let attributeListNode;
+        if (attributeListInDOM.length) {
+          attributeListNode = attributeListInDOM[0];
+          $(attributeListNode).children().remove(); // Each attr will be replaced
+        } else {
+          attributeListNode = document.createElement("attributeList");
+          nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeList");
+          if (!nodeToInsertAfter) {
+            $(objectDOM).append(attributeListNode);
+          } else {
+            $(nodeToInsertAfter).after(attributeListNode);
+          }
+        }
+
+        var updatedAttrDOM;
+        if (attributeList.length) {
+          // Add each attribute
+          _.each(
+            attributeList,
+            function (attribute) {
+              updatedAttrDOM = attribute.updateDOM();
+              $(attributeListNode).append(updatedAttrDOM);
+            },
+            this,
+          );
+        } else {
+          // Attributes are not defined, remove them from the DOM
+          attributeListNode.remove();
+        }
+
+        // TODO: Update the constraint section
+
+        return objectDOM;
+      },
+
+      /**
+       * Update the file name in the EML
+       */
+      updateFileName: function () {
+        var dataONEObj = this.get("dataONEObject");
+
+        //Get the DataONEObject model associated with this EML Entity
+        if (dataONEObj) {
+          //If the last file name matched the EML entity name, then update it
+          if (dataONEObj.previous("fileName") == this.get("entityName")) {
+            this.set("entityName", dataONEObj.get("fileName"));
+          }
+          //If the DataONEObject doesn't have an old file name or entity name, then update it
+          else if (
+            !dataONEObj.previous("fileName") ||
+            !this.get("entityName")
+          ) {
+            this.set("entityName", dataONEObj.get("fileName"));
+          }
+        }
+      },
+
+      /*
+       * Get the DOM node preceding the given nodeName
+       * to find what position in the EML document
+       * the named node should be appended
+       */
+      getEMLPosition: function (objectDOM, nodeName) {
+        var nodeOrder = this.get("nodeOrder");
+
+        var position = _.indexOf(nodeOrder, nodeName);
+
+        // Append to the bottom if not found
+        if (position == -1) {
+          return $(objectDOM).children().last()[0];
+        }
+
+        // Otherwise, go through each node in the node list and find the
+        // position where this node will be inserted after
+        for (var i = position - 1; i >= 0; i--) {
+          if ($(objectDOM).find(nodeOrder[i].toLowerCase()).length) {
+            return $(objectDOM).find(nodeOrder[i].toLowerCase()).last()[0];
+          }
+        }
+      },
+
+      /*
+       * Climbs up the model heirarchy until it finds the EML model
+       *
+       * @return {EML211 or false} - Returns the EML 211 Model or false if not found
+       */
+      getParentEML: function () {
+        var emlModel = this.get("parentModel"),
+          tries = 0;
+
+        while (emlModel.type !== "EML" && tries < 6) {
+          emlModel = emlModel.get("parentModel");
+          tries++;
+        }
+
+        if (emlModel && emlModel.type == "EML") return emlModel;
+        else return false;
+      },
+
+      /*Format the EML XML for entities*/
+      formatXML: function (xmlString) {
+        return DataONEObject.prototype.formatXML.call(this, xmlString);
+      },
+
+      /* Let the top level package know of attribute changes from this object */
+      trickleUpChange: function () {
+        MetacatUI.rootDataPackage.packageModel.set("changed", true);
+      },
+    },
+  );
+
+  return EMLEntity;
+});
 
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLGeoCoverage.js.html b/docs/docs/src_js_models_metadata_eml211_EMLGeoCoverage.js.html index fbcab1390..4ac4a8dd0 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLGeoCoverage.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLGeoCoverage.js.html @@ -44,12 +44,11 @@

Source: src/js/models/metadata/eml211/EMLGeoCoverage.js
-
/* global define */
-define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
   $,
   _,
   Backbone,
-  DataONEObject
+  DataONEObject,
 ) {
   /**
    * @class EMLGeoCoverage
@@ -83,7 +82,7 @@ 

Source: src/js/models/metadata/eml211/EMLGeoCoverage.jsSource: src/js/models/metadata/eml211/EMLGeoCoverage.js-90 and <90. Please correct the latitude.", - "east": "Southeast longitude out of range (-180 to 180). Please adjust the longitude.", - "south": "Southeast latitude out of range, must be >-90 and <90. Please correct the latitude.", - "west": "Northwest longitude out of range (-180 to 180). Check and correct the longitude.", - "missing": "Latitude and longitude are required for each coordinate. Please complete all fields.", - "description": "Missing location description. Please add a brief description.", - "needPair": "Location requires at least one coordinate pair. Please add coordinates.", - "northSouthReversed": "North latitude should be greater than South. Please swap the values.", - "crossesAntiMeridian": "Bounding box crosses the anti-meridian. Please use multiple boxes that meet at the anti-meridian instead.", + default: "Please correct the geographic coverage.", + north: + "Northwest latitude out of range, must be >-90 and <90. Please correct the latitude.", + east: "Southeast longitude out of range (-180 to 180). Please adjust the longitude.", + south: + "Southeast latitude out of range, must be >-90 and <90. Please correct the latitude.", + west: "Northwest longitude out of range (-180 to 180). Check and correct the longitude.", + missing: + "Latitude and longitude are required for each coordinate. Please complete all fields.", + description: + "Missing location description. Please add a brief description.", + needPair: + "Location requires at least one coordinate pair. Please add coordinates.", + northSouthReversed: + "North latitude should be greater than South. Please swap the values.", + crossesAntiMeridian: + "Bounding box crosses the anti-meridian. Please use multiple boxes that meet at the anti-meridian instead.", }, /** @@ -254,8 +260,8 @@

Source: src/js/models/metadata/eml211/EMLGeoCoverage.jsSource: src/js/models/metadata/eml211/EMLGeoCoverage.jsSource: src/js/models/metadata/eml211/EMLGeoCoverage.jsSource: src/js/models/metadata/eml211/EMLMeasurementScale
-
define(["jquery", "underscore", "backbone",
-    "models/metadata/eml211/EMLNonNumericDomain",
-    "models/metadata/eml211/EMLNumericDomain",
-        "models/metadata/eml211/EMLDateTimeDomain"],
-    function($, _, Backbone, EMLNonNumericDomain, EMLNumericDomain, EMLDateTimeDomain) {
-
-        /**
-        * @class EMLMeasurementScale
-         * @classdesc EMLMeasurementScale is a measurement scale factory that returns
-         * an EMLMeasurementScale subclass of either EMLNonNumericDomain,
-         * EMLNumericDomain, or EMLDateTimeDomain, depending on the
-         * domain name found in the given measurementScaleXML
-         * @classcategory Models/Metadata/EML211
-         * @extends Backbone.Model
-         */
-        var EMLMeasurementScale = Backbone.Model.extend({},
-          /** @lends EMLMeasurementScale.prototype */
-        {
-            /*
-             * Get an instance of an EMLMeasurementScale subclass
-             * given the measurementScaleXML fragment
-             */
-            getInstance: function(measurementScaleXML) {
-                var instance = {};
-
-                if(measurementScaleXML && measurementScaleXML.indexOf("<") > -1){
-                    var objectDOM = $(measurementScaleXML)[0];
-                	var domainName = $(objectDOM).children()[0].localName;
-                	var options = {parse: true};
-                }
-                //If it's not an XML string, then it must be the domainName itself
-                else if(measurementScaleXML && measurementScaleXML.indexOf("<") == -1){
-                	var domainName = measurementScaleXML;
-                	var options = {};
-                	measurementScaleXML = null;
-                }
-
-                // Return the appropriate sub class of EMLMeasurementScale
-                switch ( domainName ) {
-                    case "nominal":
-                        instance = new EMLNonNumericDomain({
-                            "measurementScale": domainName,
-                            "objectDOM": $(measurementScaleXML)[0]
-                        }, options);
-                        break;
-                    case "ordinal":
-                        instance = new EMLNonNumericDomain({
-                            "measurementScale": domainName,
-                            "objectDOM": $(measurementScaleXML)[0]
-                        }, options);
-                        break;
-                    case "interval":
-                        instance = new EMLNumericDomain({
-                            "measurementScale": domainName,
-                            "objectDOM": $(measurementScaleXML)[0]
-                        }, options);
-                        break;
-                    case "ratio":
-                        instance = new EMLNumericDomain({
-                            "measurementScale": domainName,
-                            "objectDOM": $(measurementScaleXML)[0]
-                        }, options);
-                        break;
-                    case "datetime":
-                        instance = new EMLDateTimeDomain({
-                            "measurementScale": domainName,
-                            "objectDOM": $(measurementScaleXML)[0]
-                        }, options);
-                        break;
-                    default:
-                        instance = new EMLNonNumericDomain({
-                            "measurementScale": domainName,
-                            "objectDOM": $(measurementScaleXML)[0]
-                        }, options);
-                }
-
-                return instance;
-            }
-        });
-
-        return EMLMeasurementScale;
-    }
-);
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/metadata/eml211/EMLNonNumericDomain",
+  "models/metadata/eml211/EMLNumericDomain",
+  "models/metadata/eml211/EMLDateTimeDomain",
+], function (
+  $,
+  _,
+  Backbone,
+  EMLNonNumericDomain,
+  EMLNumericDomain,
+  EMLDateTimeDomain,
+) {
+  /**
+   * @class EMLMeasurementScale
+   * @classdesc EMLMeasurementScale is a measurement scale factory that returns
+   * an EMLMeasurementScale subclass of either EMLNonNumericDomain,
+   * EMLNumericDomain, or EMLDateTimeDomain, depending on the
+   * domain name found in the given measurementScaleXML
+   * @classcategory Models/Metadata/EML211
+   * @extends Backbone.Model
+   */
+  var EMLMeasurementScale = Backbone.Model.extend(
+    {},
+    /** @lends EMLMeasurementScale.prototype */
+    {
+      /*
+       * Get an instance of an EMLMeasurementScale subclass
+       * given the measurementScaleXML fragment
+       */
+      getInstance: function (measurementScaleXML) {
+        var instance = {};
+
+        if (measurementScaleXML && measurementScaleXML.indexOf("<") > -1) {
+          var objectDOM = $(measurementScaleXML)[0];
+          var domainName = $(objectDOM).children()[0].localName;
+          var options = { parse: true };
+        }
+        //If it's not an XML string, then it must be the domainName itself
+        else if (
+          measurementScaleXML &&
+          measurementScaleXML.indexOf("<") == -1
+        ) {
+          var domainName = measurementScaleXML;
+          var options = {};
+          measurementScaleXML = null;
+        }
+
+        // Return the appropriate sub class of EMLMeasurementScale
+        switch (domainName) {
+          case "nominal":
+            instance = new EMLNonNumericDomain(
+              {
+                measurementScale: domainName,
+                objectDOM: $(measurementScaleXML)[0],
+              },
+              options,
+            );
+            break;
+          case "ordinal":
+            instance = new EMLNonNumericDomain(
+              {
+                measurementScale: domainName,
+                objectDOM: $(measurementScaleXML)[0],
+              },
+              options,
+            );
+            break;
+          case "interval":
+            instance = new EMLNumericDomain(
+              {
+                measurementScale: domainName,
+                objectDOM: $(measurementScaleXML)[0],
+              },
+              options,
+            );
+            break;
+          case "ratio":
+            instance = new EMLNumericDomain(
+              {
+                measurementScale: domainName,
+                objectDOM: $(measurementScaleXML)[0],
+              },
+              options,
+            );
+            break;
+          case "datetime":
+            instance = new EMLDateTimeDomain(
+              {
+                measurementScale: domainName,
+                objectDOM: $(measurementScaleXML)[0],
+              },
+              options,
+            );
+            break;
+          default:
+            instance = new EMLNonNumericDomain(
+              {
+                measurementScale: domainName,
+                objectDOM: $(measurementScaleXML)[0],
+              },
+              options,
+            );
+        }
+
+        return instance;
+      },
+    },
+  );
+
+  return EMLMeasurementScale;
+});
 
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLMethods.js.html b/docs/docs/src_js_models_metadata_eml211_EMLMethods.js.html index 919bdb2cd..c9e4d5016 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLMethods.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLMethods.js.html @@ -44,15 +44,14 @@

Source: src/js/models/metadata/eml211/EMLMethods.js

-
/* global define */
-define(['jquery',
-    'underscore',
-    'backbone',
-    'models/DataONEObject',
-    'models/metadata/eml/EMLMethodStep',
-    'models/metadata/eml211/EMLText'],
-    function($, _, Backbone, DataONEObject, EMLMethodStep, EMLText) {
-
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/DataONEObject",
+  "models/metadata/eml/EMLMethodStep",
+  "models/metadata/eml211/EMLText",
+], function ($, _, Backbone, DataONEObject, EMLMethodStep, EMLText) {
   /**
   * @class EMLMethods
   * @classdesc Represents the EML Methods module. The methods field documents scientific methods
@@ -63,9 +62,8 @@ 

Source: src/js/models/metadata/eml211/EMLMethods.js

* @extends Backbone.Model */ var EMLMethods = Backbone.Model.extend( - /** @lends EMLMethods.prototype */{ - - /** + /** @lends EMLMethods.prototype */ { + /** * The default values of this model that are get() or set() * @returns {object} * @property {string} objectXML The original XML snippet string from the EML XML @@ -78,508 +76,571 @@

Source: src/js/models/metadata/eml211/EMLMethods.js

text-based/human readable description of the sampling procedures used in the research project. */ - defaults: function(){ - return { - objectXML: null, - objectDOM: null, - methodSteps: [], - studyExtentDescription: null, - samplingDescription: null - } - }, - - initialize: function(attributes){ - attributes = attributes || {}; - - if(attributes.objectDOM){ - this.set(this.parse(attributes.objectDOM)) - } - else if(attributes.objectXML){ - let objectDOM = $.parseHTML(attributes.objectXML)[0]; - this.set("objectDOM", objectDOM); - this.set(this.parse(objectDOM)) - } - else{ - //Create the custom method steps and add to the step list - let customMethodSteps = this.createCustomMethodSteps(); - this.set("methodSteps", customMethodSteps); - } - - //specific attributes to listen to - this.on("change:methodStepDescription change:studyExtentDescription change:samplingDescription", - this.trickleUpChange); - - }, + defaults: function () { + return { + objectXML: null, + objectDOM: null, + methodSteps: [], + studyExtentDescription: null, + samplingDescription: null, + }; + }, + + initialize: function (attributes) { + attributes = attributes || {}; + + if (attributes.objectDOM) { + this.set(this.parse(attributes.objectDOM)); + } else if (attributes.objectXML) { + let objectDOM = $.parseHTML(attributes.objectXML)[0]; + this.set("objectDOM", objectDOM); + this.set(this.parse(objectDOM)); + } else { + //Create the custom method steps and add to the step list + let customMethodSteps = this.createCustomMethodSteps(); + this.set("methodSteps", customMethodSteps); + } - /** - * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML). - * Used during parse() and serialize() - * @returns {object} - */ - nodeNameMap: function(){ - return _.extend(EMLMethodStep.prototype.nodeNameMap(), { - "methodstep" : "methodStep", - "datasource" : "dataSource", - "studyextent" : "studyExtent", - "spatialsamplingunits" : "spatialSamplingUnits", - "referencedentityid" : "referencedEntityId", - "qualitycontrol" : "qualityControl" - }) - }, + //specific attributes to listen to + this.on( + "change:methodStepDescription change:studyExtentDescription change:samplingDescription", + this.trickleUpChange, + ); + }, + + /** + * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML). + * Used during parse() and serialize() + * @returns {object} + */ + nodeNameMap: function () { + return _.extend(EMLMethodStep.prototype.nodeNameMap(), { + methodstep: "methodStep", + datasource: "dataSource", + studyextent: "studyExtent", + spatialsamplingunits: "spatialSamplingUnits", + referencedentityid: "referencedEntityId", + qualitycontrol: "qualityControl", + }); + }, - parse: function(objectDOM) { - var modelJSON = {}; + parse: function (objectDOM) { + var modelJSON = {}; - if (!objectDOM) var objectDOM = this.get("objectDOM"); + if (!objectDOM) var objectDOM = this.get("objectDOM"); - var model = this; + var model = this; - //Create the custom method steps - let customMethodSteps = this.createCustomMethodSteps(); + //Create the custom method steps + let customMethodSteps = this.createCustomMethodSteps(); - //Create new EMLMethodStep models for the method steps - let allMethodSteps = _.map($(objectDOM).find('methodstep'), function(el, i) { - return new EMLMethodStep({ - objectDOM: el - }); - }), + //Create new EMLMethodStep models for the method steps + let allMethodSteps = _.map( + $(objectDOM).find("methodstep"), + function (el, i) { + return new EMLMethodStep({ + objectDOM: el, + }); + }, + ), //Get the custom IDs for each method step, if there any - allMethodStepIDs = _.compact(allMethodSteps.map(step => { return step.get("customMethodID") })); + allMethodStepIDs = _.compact( + allMethodSteps.map((step) => { + return step.get("customMethodID"); + }), + ); + + //Filter out any custom method steps that we already created from the DOM + customMethodSteps = customMethodSteps.filter((step) => { + return !allMethodStepIDs.includes(step.get("customMethodID")); + }); - //Filter out any custom method steps that we already created from the DOM - customMethodSteps = customMethodSteps.filter(step => { return !allMethodStepIDs.includes(step.get("customMethodID")) }); + //Combine the parsed method steps and the default custom method steps + allMethodSteps = allMethodSteps.concat(customMethodSteps); - //Combine the parsed method steps and the default custom method steps - allMethodSteps = allMethodSteps.concat(customMethodSteps); + //Save the method steps to this model + modelJSON.methodSteps = allMethodSteps; - //Save the method steps to this model - modelJSON.methodSteps = allMethodSteps; + if ($(objectDOM).find("sampling studyextent description").length > 0) { + modelJSON.studyExtentDescription = new EMLText({ + objectDOM: $(objectDOM) + .find("sampling studyextent description") + .get(0), + type: "description", + parentModel: model, + }); + } - if ($(objectDOM).find('sampling studyextent description').length > 0) { - modelJSON.studyExtentDescription = new EMLText({ - objectDOM: $(objectDOM).find('sampling studyextent description').get(0), - type: 'description', - parentModel: model - }); - } + if ($(objectDOM).find("sampling samplingdescription").length > 0) { + modelJSON.samplingDescription = new EMLText({ + objectDOM: $(objectDOM).find("sampling samplingdescription").get(0), + type: "samplingDescription", + parentModel: model, + }); + } - if ($(objectDOM).find('sampling samplingdescription').length > 0) { - modelJSON.samplingDescription = new EMLText({ - objectDOM: $(objectDOM).find('sampling samplingdescription').get(0), - type: 'samplingDescription', - parentModel: model - }); - } + return modelJSON; + }, - return modelJSON; - }, + serialize: function () { + var objectDOM = this.updateDOM(); - serialize: function(){ - var objectDOM = this.updateDOM(); + if (!objectDOM) return ""; - if(!objectDOM) - return ""; + var xmlString = objectDOM.outerHTML; - var xmlString = objectDOM.outerHTML; + //Camel-case the XML + xmlString = this.formatXML(xmlString); - //Camel-case the XML - xmlString = this.formatXML(xmlString); + return xmlString; + }, - return xmlString; - }, - - /** - * Makes a copy of the original XML DOM and updates it with the new values from the model. - */ - updateDOM: function(){ - var objectDOM; + /** + * Makes a copy of the original XML DOM and updates it with the new values from the model. + */ + updateDOM: function () { + var objectDOM; - if (this.get("objectDOM")) { - objectDOM = this.get("objectDOM").cloneNode(true); - } else { - objectDOM = $(document.createElement("methods")); - } + if (this.get("objectDOM")) { + objectDOM = this.get("objectDOM").cloneNode(true); + } else { + objectDOM = $(document.createElement("methods")); + } - objectDOM = $(objectDOM); + objectDOM = $(objectDOM); - try{ - var methodStepsFromModel = this.get('methodSteps'), + try { + var methodStepsFromModel = this.get("methodSteps"), regularMethodSteps = this.getNonCustomSteps(), - customMethodSteps = _.difference(methodStepsFromModel, regularMethodSteps), + customMethodSteps = _.difference( + methodStepsFromModel, + regularMethodSteps, + ), sortedCustomMethodSteps = [], - methodStepsFromDOM = $(objectDOM).find("methodstep"); - - //Detach the existing method steps from the DOM first - methodStepsFromDOM.detach(); - - try{ + methodStepsFromDOM = $(objectDOM).find("methodstep"); + + //Detach the existing method steps from the DOM first + methodStepsFromDOM.detach(); + + try { + //Sort the custom method steps to match the app config order + let configCustomMethods = _.clone( + MetacatUI.appModel.get("customEMLMethods") || [], + ); + if (configCustomMethods.length) { + configCustomMethods.forEach((customOptions) => { + let matchingStep = customMethodSteps.find((step) => { + return customOptions.titleOptions.includes( + step.get("description").get("title"), + ); + }); + if (matchingStep) { + sortedCustomMethodSteps.push(matchingStep); + } + }); + } + } catch (e) { + console.error( + "Could not sort the custom methods during serialization. Will proceed without sorting the custom method steps: ", + e, + ); + sortedCustomMethodSteps = customMethodSteps; + } - //Sort the custom method steps to match the app config order - let configCustomMethods = _.clone(MetacatUI.appModel.get("customEMLMethods") || []); - if( configCustomMethods.length ){ - configCustomMethods.forEach( customOptions => { - let matchingStep = customMethodSteps.find( step => { return customOptions.titleOptions.includes(step.get("description").get("title")) }); - if( matchingStep ){ - sortedCustomMethodSteps.push(matchingStep); - } + //Update each method step and prepend to the top of the methods (reverse arrays first to keep the right order) + regularMethodSteps + .reverse() + .concat(sortedCustomMethodSteps.reverse()) + .forEach((step) => { + objectDOM.prepend(step.updateDOM()); }); - } - } - catch(e){ - console.error("Could not sort the custom methods during serialization. Will proceed without sorting the custom method steps: ", e); - sortedCustomMethodSteps = customMethodSteps; + } catch (e) { + console.error( + "Failed to serialize the method steps. Proceeding without updating. ", + e, + ); } - //Update each method step and prepend to the top of the methods (reverse arrays first to keep the right order) - regularMethodSteps.reverse().concat(sortedCustomMethodSteps.reverse()).forEach(step => { - objectDOM.prepend(step.updateDOM()); - }); - } - catch(e){ - console.error("Failed to serialize the method steps. Proceeding without updating. ", e); - } - - try{ - // Update the sampling metadata - if (this.get('samplingDescription') || this.get('studyExtentDescription')) { - - var samplingEl = $(document.createElement('sampling')), - studyExtentEl = $(document.createElement('studyExtent')), + try { + // Update the sampling metadata + if ( + this.get("samplingDescription") || + this.get("studyExtentDescription") + ) { + var samplingEl = $(document.createElement("sampling")), + studyExtentEl = $(document.createElement("studyExtent")), missingStudyExtent = false, missingDescription = false; - //If there is a study extent description, then create a DOM element for it and append it to the parent node - if (this.get('studyExtentDescription') && !this.get('studyExtentDescription').isEmpty()) { - $(studyExtentEl).append(this.get('studyExtentDescription').updateDOM()); - - //If the text matches the default "filler" text, then mark it as missing - if( this.get('studyExtentDescription').get("text")[0] == "No study extent description provided."){ + //If there is a study extent description, then create a DOM element for it and append it to the parent node + if ( + this.get("studyExtentDescription") && + !this.get("studyExtentDescription").isEmpty() + ) { + $(studyExtentEl).append( + this.get("studyExtentDescription").updateDOM(), + ); + + //If the text matches the default "filler" text, then mark it as missing + if ( + this.get("studyExtentDescription").get("text")[0] == + "No study extent description provided." + ) { + missingStudyExtent = true; + } + } + //If there isn't a study extent description, then mark it as missing and append the default "filler" text + else { missingStudyExtent = true; + $(studyExtentEl).append( + $(document.createElement("description")).html( + "<para>No study extent description provided.</para>", + ), + ); } - } - //If there isn't a study extent description, then mark it as missing and append the default "filler" text - else { - missingStudyExtent = true; - $(studyExtentEl).append($(document.createElement('description')).html("<para>No study extent description provided.</para>")); - } - - //Add the study extent element to the sampling element - $(samplingEl).append(studyExtentEl); - - //If there is a sampling description, then create a DOM element for it and append it to the parent node - if (this.get('samplingDescription') && !this.get('samplingDescription').isEmpty()) { - $(samplingEl).append(this.get('samplingDescription').updateDOM()); - - //If the text matches the default "filler" text, then mark it as missing - if( this.get('samplingDescription').get("text")[0] == "No sampling description provided."){ + //Add the study extent element to the sampling element + $(samplingEl).append(studyExtentEl); + + //If there is a sampling description, then create a DOM element for it and append it to the parent node + if ( + this.get("samplingDescription") && + !this.get("samplingDescription").isEmpty() + ) { + $(samplingEl).append(this.get("samplingDescription").updateDOM()); + + //If the text matches the default "filler" text, then mark it as missing + if ( + this.get("samplingDescription").get("text")[0] == + "No sampling description provided." + ) { + missingDescription = true; + } + } + //If there isn't a study extent description, then mark it as missing and append the default "filler" text + else { missingDescription = true; + $(samplingEl).append( + $(document.createElement("samplingDescription")).html( + "<para>No sampling description provided.</para>", + ), + ); } - } - //If there isn't a study extent description, then mark it as missing and append the default "filler" text - else { - missingDescription = true; - $(samplingEl).append($(document.createElement('samplingDescription')).html("<para>No sampling description provided.</para>")); - } - - //Find the existing <sampling> element - var existingSampling = objectDOM.find("sampling"); + //Find the existing <sampling> element + var existingSampling = objectDOM.find("sampling"); - //Remove all the sampling nodes if there is no study extent and no description - if(missingStudyExtent && missingDescription){ - existingSampling.remove(); - } - //Replace the existing sampling element, if it exists - else if( existingSampling.length > 0 ){ - existingSampling.replaceWith(samplingEl); - } - //Or append a new one - else{ - objectDOM.append(samplingEl); + //Remove all the sampling nodes if there is no study extent and no description + if (missingStudyExtent && missingDescription) { + existingSampling.remove(); + } + //Replace the existing sampling element, if it exists + else if (existingSampling.length > 0) { + existingSampling.replaceWith(samplingEl); + } + //Or append a new one + else { + objectDOM.append(samplingEl); + } } + } catch (e) { + console.error( + "Error while serializing the study extent and sampling. Won't update. ", + e, + ); } - } - catch(e){ - console.error("Error while serializing the study extent and sampling. Won't update. ", e); - } - - // Remove empty (zero-length or whitespace-only) nodes - objectDOM.find("*").filter(function() { return $.trim(this.innerHTML) === ""; } ).remove(); - - //Check if all the content is filler content. This means there are no method steps, no sampling description, and - // no study extent description. - if( objectDOM.find("samplingdescription").length == 1 && - objectDOM.find("samplingdescription para").text() == "No sampling description provided." && + // Remove empty (zero-length or whitespace-only) nodes + objectDOM + .find("*") + .filter(function () { + return $.trim(this.innerHTML) === ""; + }) + .remove(); + + //Check if all the content is filler content. This means there are no method steps, no sampling description, and + // no study extent description. + if ( + objectDOM.find("samplingdescription").length == 1 && + objectDOM.find("samplingdescription para").text() == + "No sampling description provided." && objectDOM.find("studyextent").length == 1 && - objectDOM.find("studyextent description para").text() == "No study extent description provided." ){ - + objectDOM.find("studyextent description para").text() == + "No study extent description provided." + ) { //If it is all empty / filler content, then totally remove the methods return ""; + } - } - - //If there are sampling nodes listed before methodStep nodes, then reorder them - if( objectDOM.children().index(objectDOM.find("methodstep").last()) > - objectDOM.children().index(objectDOM.find("sampling").last()) ){ - - //Detach all the sampling nodes and append them to the parent node - objectDOM.append( objectDOM.children("sampling").detach() ); - - } - - //If there are sampling nodes but no method nodes, make method nodes - if( objectDOM.find("samplingdescription").length > 0 && - objectDOM.find("studyextent").length > 0){ - //Make a filler method node - if(!objectDOM.find("methodstep").length){ - objectDOM.prepend("<methodstep><description><para>No method step description provided.</para></description></methodstep>"); + //If there are sampling nodes listed before methodStep nodes, then reorder them + if ( + objectDOM.children().index(objectDOM.find("methodstep").last()) > + objectDOM.children().index(objectDOM.find("sampling").last()) + ) { + //Detach all the sampling nodes and append them to the parent node + objectDOM.append(objectDOM.children("sampling").detach()); } - else if(objectDOM.find("methodstep").length > 1){ - //If there is more than one method step, remove any that have the default filler text - objectDOM.find("methodstep:contains('No method step description provided.')").remove(); - //Double check that there is always at least one method step (or there will be an EML validation error) - if(!objectDOM.find("methodstep").length){ - objectDOM.prepend("<methodstep><description><para>No method step description provided.</para></description></methodstep>"); + + //If there are sampling nodes but no method nodes, make method nodes + if ( + objectDOM.find("samplingdescription").length > 0 && + objectDOM.find("studyextent").length > 0 + ) { + //Make a filler method node + if (!objectDOM.find("methodstep").length) { + objectDOM.prepend( + "<methodstep><description><para>No method step description provided.</para></description></methodstep>", + ); + } else if (objectDOM.find("methodstep").length > 1) { + //If there is more than one method step, remove any that have the default filler text + objectDOM + .find( + "methodstep:contains('No method step description provided.')", + ) + .remove(); + //Double check that there is always at least one method step (or there will be an EML validation error) + if (!objectDOM.find("methodstep").length) { + objectDOM.prepend( + "<methodstep><description><para>No method step description provided.</para></description></methodstep>", + ); + } } } - } - - return objectDOM.length? objectDOM[0] : objectDOM; - }, - - /** - * Creates a new EMLMethodStep model and adds it to this model - * @param {object} [attr] A literal object of attributes to set on the EMLMethodStep - * @since 2.19.0 - */ - addMethodStep: function(attr){ - - try{ + return objectDOM.length ? objectDOM[0] : objectDOM; + }, + + /** + * Creates a new EMLMethodStep model and adds it to this model + * @param {object} [attr] A literal object of attributes to set on the EMLMethodStep + * @since 2.19.0 + */ + addMethodStep: function (attr) { + try { + if (!attr) { + let attr = {}; + } - if(!attr){ - let attr = {} + let newStep = new EMLMethodStep(attr); + this.get("methodSteps").push(newStep); + this.set("methodSteps", this.get("methodSteps")); + return newStep; + } catch (e) { + console.error(e); } - - let newStep = new EMLMethodStep(attr); - this.get("methodSteps").push(newStep); - this.set("methodSteps", this.get("methodSteps")); - return newStep; - - } - catch(e){ - console.error(e); - } - - }, - - /** - * Removes the given EMLMethodStep from the overall EMLMethods - * @param {EMLMethodStep} step The EMLMethodStep to remove - * @since 2.19.0 - */ - removeMethodStep: function(step){ - try{ - - if( !step ) return; - - //Remove the EMLMethodStep from the steps list - this.set("methodSteps", _.without(this.get("methodSteps"), step)); - - //If this was the last step to be removed, and the rest of the EMLMethods - // model is empty, then remove the model from the parent EML model - if( this.isEmpty() ){ - //Get the parent EML model - var parentEML = this.getParentEML(); - - //Make sure this model type is EML211 - if( parentEML && parentEML.type == "EML" ){ - - //If the methods are an array, - if( Array.isArray(parentEML.get("methods")) ){ - //remove this EMLMethods model from the array - parentEML.set( "methods", _.without(parentEML.get("methods"), this) ); - } - else{ - //If the methods attribute is set to this EMLMethods model, - // then just set it back to it's default - if( parentEML.get("methods") == this ) - parentEML.set("methods", parentEML.defaults().methods); + }, + + /** + * Removes the given EMLMethodStep from the overall EMLMethods + * @param {EMLMethodStep} step The EMLMethodStep to remove + * @since 2.19.0 + */ + removeMethodStep: function (step) { + try { + if (!step) return; + + //Remove the EMLMethodStep from the steps list + this.set("methodSteps", _.without(this.get("methodSteps"), step)); + + //If this was the last step to be removed, and the rest of the EMLMethods + // model is empty, then remove the model from the parent EML model + if (this.isEmpty()) { + //Get the parent EML model + var parentEML = this.getParentEML(); + + //Make sure this model type is EML211 + if (parentEML && parentEML.type == "EML") { + //If the methods are an array, + if (Array.isArray(parentEML.get("methods"))) { + //remove this EMLMethods model from the array + parentEML.set( + "methods", + _.without(parentEML.get("methods"), this), + ); + } else { + //If the methods attribute is set to this EMLMethods model, + // then just set it back to it's default + if (parentEML.get("methods") == this) + parentEML.set("methods", parentEML.defaults().methods); + } } } + this.trickleUpChange(); + } catch (e) { + console.error("Error while trying to remove a method step: ", e); } - - this.trickleUpChange(); - - } - catch(e){ - console.error("Error while trying to remove a method step: ", e); - } - }, - - /** - * Returns the EMLMethodSteps that are not custom methods, as configured in {@link AppConfig#customEMLMethods} - * @returns {EMLMethodStep[]} - * @since 2.19.0 - */ - getNonCustomSteps: function(){ - return this.get("methodSteps").filter(step => !step.isCustom()); - }, - - /** - * Returns the EMLMethodSteps that are custom methods, as configured in {@link AppConfig#customEMLMethods} - * @returns {EMLMethodStep[]} - * @since 2.19.0 - */ - getCustomSteps: function(){ - return this.get("methodSteps").filter(step => step.isCustom()); - }, - - /** - * function isEmpty() - Will check if there are any values set on this model - * that are different than the default values and would be serialized to the EML. - * - * @return {boolean} - Returns true is this model is empty, false if not - */ - isEmpty: function(){ - - var methodsStepsEmpty = false, + }, + + /** + * Returns the EMLMethodSteps that are not custom methods, as configured in {@link AppConfig#customEMLMethods} + * @returns {EMLMethodStep[]} + * @since 2.19.0 + */ + getNonCustomSteps: function () { + return this.get("methodSteps").filter((step) => !step.isCustom()); + }, + + /** + * Returns the EMLMethodSteps that are custom methods, as configured in {@link AppConfig#customEMLMethods} + * @returns {EMLMethodStep[]} + * @since 2.19.0 + */ + getCustomSteps: function () { + return this.get("methodSteps").filter((step) => step.isCustom()); + }, + + /** + * function isEmpty() - Will check if there are any values set on this model + * that are different than the default values and would be serialized to the EML. + * + * @return {boolean} - Returns true is this model is empty, false if not + */ + isEmpty: function () { + var methodsStepsEmpty = false, studyExtentEmpty = false, samplingEmpty = false; - if( !this.get("methodSteps").length || !this.get("methodSteps") || this.get("methodSteps").every(step => step.isEmpty()) ){ - methodsStepsEmpty = true; - } + if ( + !this.get("methodSteps").length || + !this.get("methodSteps") || + this.get("methodSteps").every((step) => step.isEmpty()) + ) { + methodsStepsEmpty = true; + } - if( this.get("studyExtentDescription") == this.defaults().studyExtentDescription || + if ( + this.get("studyExtentDescription") == + this.defaults().studyExtentDescription || !this.get("studyExtentDescription") || - (this.get("studyExtentDescription").isEmpty && this.get("studyExtentDescription").isEmpty()) || - (Array.isArray(this.get("studyExtentDescription")) && !this.get("studyExtentDescription").length ) || + (this.get("studyExtentDescription").isEmpty && + this.get("studyExtentDescription").isEmpty()) || (Array.isArray(this.get("studyExtentDescription")) && - this.get("studyExtentDescription").length == 1 && - this.get("studyExtentDescription")[0].get("text").length == 1 && - this.get("studyExtentDescription")[0].get("text")[0] == "No study extent description provided.") ){ - + !this.get("studyExtentDescription").length) || + (Array.isArray(this.get("studyExtentDescription")) && + this.get("studyExtentDescription").length == 1 && + this.get("studyExtentDescription")[0].get("text").length == 1 && + this.get("studyExtentDescription")[0].get("text")[0] == + "No study extent description provided.") + ) { studyExtentEmpty = true; + } - } - - if( this.get("samplingDescription") == this.defaults().samplingDescription || + if ( + this.get("samplingDescription") == + this.defaults().samplingDescription || !this.get("samplingDescription") || - (this.get("samplingDescription").isEmpty && this.get("samplingDescription").isEmpty()) || - (Array.isArray(this.get("samplingDescription")) && !this.get("samplingDescription").length ) || + (this.get("samplingDescription").isEmpty && + this.get("samplingDescription").isEmpty()) || (Array.isArray(this.get("samplingDescription")) && - this.get("samplingDescription").length == 1 && - this.get("samplingDescription")[0].get("text").length == 1 && - this.get("samplingDescription")[0].get("text")[0] == "No sampling description provided.") ){ - + !this.get("samplingDescription").length) || + (Array.isArray(this.get("samplingDescription")) && + this.get("samplingDescription").length == 1 && + this.get("samplingDescription")[0].get("text").length == 1 && + this.get("samplingDescription")[0].get("text")[0] == + "No sampling description provided.") + ) { samplingEmpty = true; + } - } - - if( methodsStepsEmpty && studyExtentEmpty && samplingEmpty ) - return true; - - }, - - /** - * Overloads Backbone.Model.validate() to check if this model has valid values set on it. - * For now, only the custom method steps are validated, because they could be required. - * @extends Backbone.Model.validate - * @returns {object} - */ - validate: function(){ - - try{ - - let validationErrors = {} - - //Validate each custom Method Step - let customSteps = this.getCustomSteps(), + if (methodsStepsEmpty && studyExtentEmpty && samplingEmpty) return true; + }, + + /** + * Overloads Backbone.Model.validate() to check if this model has valid values set on it. + * For now, only the custom method steps are validated, because they could be required. + * @extends Backbone.Model.validate + * @returns {object} + */ + validate: function () { + try { + let validationErrors = {}; + + //Validate each custom Method Step + let customSteps = this.getCustomSteps(), methodStepValidationErrors = {}; - customSteps.forEach(step => { - if( !step.isValid() ){ - methodStepValidationErrors[step.get("customMethodID")] = step.validationError; - } - }); - - if( Object.keys(methodStepValidationErrors).length ){ - validationErrors.methodSteps = methodStepValidationErrors; - } + customSteps.forEach((step) => { + if (!step.isValid()) { + methodStepValidationErrors[step.get("customMethodID")] = + step.validationError; + } + }); - //Check for the required fields - let isRequired = MetacatUI.appModel.get("emlEditorRequiredFields").methods === true; - if( isRequired ){ - let steps = this.getNonCustomSteps(); - if( !steps || !steps.length){ - validationErrors.methodSteps = "At least one method step is required."; + if (Object.keys(methodStepValidationErrors).length) { + validationErrors.methodSteps = methodStepValidationErrors; } - } - - return Object.keys(validationErrors).length? validationErrors : false; - } - catch(e){ - console.error("Error while validating the Methods: ", e); - return false; - } - - }, + //Check for the required fields + let isRequired = + MetacatUI.appModel.get("emlEditorRequiredFields").methods === true; + if (isRequired) { + let steps = this.getNonCustomSteps(); + if (!steps || !steps.length) { + validationErrors.methodSteps = + "At least one method step is required."; + } + } - /** - * Climbs up the model heirarchy until it finds the EML model - * - * @return {EML211|false} - Returns the EML 211 Model or false if not found - */ - getParentEML: function(){ - var emlModel = this.get("parentModel"), + return Object.keys(validationErrors).length + ? validationErrors + : false; + } catch (e) { + console.error("Error while validating the Methods: ", e); + return false; + } + }, + + /** + * Climbs up the model heirarchy until it finds the EML model + * + * @return {EML211|false} - Returns the EML 211 Model or false if not found + */ + getParentEML: function () { + var emlModel = this.get("parentModel"), tries = 0; - while (emlModel.type !== "EML" && tries < 6){ - emlModel = emlModel.get("parentModel"); - tries++; - } - - if( emlModel && emlModel.type == "EML") - return emlModel; - else - return false; + while (emlModel.type !== "EML" && tries < 6) { + emlModel = emlModel.get("parentModel"); + tries++; + } - }, + if (emlModel && emlModel.type == "EML") return emlModel; + else return false; + }, + + trickleUpChange: function () { + MetacatUI.rootDataPackage.packageModel.set("changed", true); + }, + + formatXML: function (xmlString) { + return DataONEObject.prototype.formatXML.call(this, xmlString); + }, + + /** + * Creates and returns the custom Method Step models, as configured in the {@link AppConfig} + * @returns {EMLMethodStep[]} + * @since 2.19.0 + */ + createCustomMethodSteps: function () { + //Get the custom methods configured in the app + let configCustomMethods = MetacatUI.appModel.get("customEMLMethods"), + customMethods = []; - trickleUpChange: function(){ - MetacatUI.rootDataPackage.packageModel.set("changed", true); - }, + //If there is at least one + configCustomMethods.forEach((config) => { + customMethods.push( + new EMLMethodStep({ + customMethodID: config.id, + required: config.required, + }), + ); + }); - formatXML: function(xmlString){ - return DataONEObject.prototype.formatXML.call(this, xmlString); + return customMethods; + }, }, - - /** - * Creates and returns the custom Method Step models, as configured in the {@link AppConfig} - * @returns {EMLMethodStep[]} - * @since 2.19.0 - */ - createCustomMethodSteps: function(){ - //Get the custom methods configured in the app - let configCustomMethods = MetacatUI.appModel.get("customEMLMethods"), - customMethods = []; - - //If there is at least one - configCustomMethods.forEach(config => { - customMethods.push(new EMLMethodStep({ - customMethodID: config.id, - required: config.required - })) - }); - - return customMethods; - } - }); + ); return EMLMethods; }); diff --git a/docs/docs/src_js_models_metadata_eml211_EMLMissingValueCode.js.html b/docs/docs/src_js_models_metadata_eml211_EMLMissingValueCode.js.html index 01d219c8c..c2fc797f8 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLMissingValueCode.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLMissingValueCode.js.html @@ -174,7 +174,7 @@

Source: src/js/models/metadata/eml211/EMLMissingValueCode return Object.keys(errors).length > 0 ? errors : undefined; }, - } + }, ); return EMLMissingValueCode; diff --git a/docs/docs/src_js_models_metadata_eml211_EMLNonNumericDomain.js.html b/docs/docs/src_js_models_metadata_eml211_EMLNonNumericDomain.js.html index fbe6f2d71..3f43ab3df 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLNonNumericDomain.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLNonNumericDomain.js.html @@ -44,896 +44,943 @@

Source: src/js/models/metadata/eml211/EMLNonNumericDomain
-
define(["jquery", "underscore", "backbone",
-        "models/DataONEObject"],
-    function($, _, Backbone, DataONEObject) {
-
-        /**
-         * @class EMLNonNumericDomain
-         * @classdesc EMLNonNumericDomain represents the measurement scale of a nominal
-         * or ordinal measurement scale attribute, and is an extension of
-         * EMLMeasurementScale.
-         * @classcategory Models/Metadata/EML211
-         * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_AttributeType_measurementScale_AttributeType_AttributeType_measurementScale_nominal_nonNumericDomain
-         * @extends Backbone.Model
-         * @constructor
-         */
-        var EMLNonNumericDomain = Backbone.Model.extend(
-            /** @lends EMLNonNumericDomain.prototype */{
-
-        	type: "EMLNonNumericDomain",
-
-            /* Attributes of an EMLNonNumericDomain object */
-            defaults: function(){
-                return {
-                  /* Attributes from EML, extends attributes from EMLMeasurementScale */
-                  measurementScale: null, // the name of this measurement scale
-                  nonNumericDomain: [] // One or more of enumeratedDomain, textDomain, references
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+  $,
+  _,
+  Backbone,
+  DataONEObject,
+) {
+  /**
+   * @class EMLNonNumericDomain
+   * @classdesc EMLNonNumericDomain represents the measurement scale of a nominal
+   * or ordinal measurement scale attribute, and is an extension of
+   * EMLMeasurementScale.
+   * @classcategory Models/Metadata/EML211
+   * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_AttributeType_measurementScale_AttributeType_AttributeType_measurementScale_nominal_nonNumericDomain
+   * @extends Backbone.Model
+   * @constructor
+   */
+  var EMLNonNumericDomain = Backbone.Model.extend(
+    /** @lends EMLNonNumericDomain.prototype */ {
+      type: "EMLNonNumericDomain",
+
+      /* Attributes of an EMLNonNumericDomain object */
+      defaults: function () {
+        return {
+          /* Attributes from EML, extends attributes from EMLMeasurementScale */
+          measurementScale: null, // the name of this measurement scale
+          nonNumericDomain: [], // One or more of enumeratedDomain, textDomain, references
+        };
+      },
+
+      /**
+       * The map of lower case to camel case node names
+       * needed to deal with parsing issues with $.parseHTML().
+       * Use this until we can figure out issues with $.parseXML().
+       * @type {object}
+       */
+      nodeNameMap: {
+        nonnumericdomain: "nonNumericDomain",
+        enumerateddomain: "enumeratedDomain",
+        textdomain: "textDomain",
+        externalcodeset: "externalCodeSet",
+        codesetname: "codesetName",
+        codeseturl: "codesetURL",
+        entityCodeList: "entityCodeList",
+        entityreference: "entityReference",
+        valueattributereference: "valueAttributeReference",
+        definitionattributereference: "definitionAttributeReference",
+        orderattributereference: "orderAttributeReference",
+        sourced: "source",
+      },
+
+      /* Initialize an EMLNonNumericDomain object */
+      initialize: function (attributes, options) {
+        this.on("change:nonNumericDomain", this.trickleUpChange);
+      },
+
+      /**
+       * Parse the incoming measurementScale's XML elements
+       */
+      parse: function (attributes, options) {
+        var $objectDOM;
+        var nonNumericDomainNodeList;
+        var domainNodeList; // the list of domain elements
+        var domain; // the text or enumerated domain to parse
+        var domainObject; // The parsed domain object to be added to attributes.nonNumericDomain
+        var rootNodeName; // Name of the fragment root elements
+
+        if (attributes.objectDOM) {
+          rootNodeName = $(attributes.objectDOM)[0].localName;
+          $objectDOM = $(attributes.objectDOM);
+        } else if (attributes.objectXML) {
+          rootNodeName = $(attributes.objectXML)[0].localName;
+          $objectDOM = $($(attributes.objectXML)[0]);
+        } else {
+          return {};
+        }
+
+        // do we have an appropriate measurementScale tree?
+        var index = _.indexOf(
+          ["measurementscale", "nominal", "ordinal"],
+          rootNodeName,
+        );
+        if (index == -1) {
+          throw new Error(
+            "The measurement scale XML does not have a root " +
+              "node of 'measurementScale', 'nominal', or 'ordinal'.",
+          );
+        }
+
+        // If measurementScale is present, add it
+        if (rootNodeName == "measurementscale") {
+          attributes.measurementScale = $objectDOM
+            .children()
+            .first()[0].localName;
+          $objectDOM = $objectDOM.children().first();
+        } else {
+          attributes.measurementScale = $objectDOM.localName;
+        }
+
+        nonNumericDomainNodeList = $objectDOM.find("nonnumericdomain");
+
+        if (nonNumericDomainNodeList && nonNumericDomainNodeList.length > 0) {
+          domainNodeList = nonNumericDomainNodeList[0].children;
+        } else {
+          // No content is available, return
+          return attributes;
+        }
+
+        // Initialize an array of nonNumericDomain objects
+        attributes.nonNumericDomain = [];
+
+        // Set each domain if we have it
+        if (domainNodeList && domainNodeList.length > 0) {
+          _.each(
+            domainNodeList,
+            function (domain) {
+              if (domain) {
+                // match the camelCase name since DOMParser() is XML-aware
+                switch (domain.localName) {
+                  case "textdomain":
+                    domainObject = this.parseTextDomain(domain);
+                    break;
+                  case "enumerateddomain":
+                    domainObject = this.parseEnumeratedDomain(domain);
+                    break;
+                  case "references":
+                    // TODO: Support references
+                    console.log(
+                      "In EMLNonNumericDomain.parse()" +
+                        "We don't support references yet ",
+                    );
+                  default:
+                    console.log(
+                      "Unrecognized nonNumericDomain: " + domain.nodeName,
+                    );
                 }
+              }
+              attributes.nonNumericDomain.push(domainObject);
             },
+            this,
+          );
+        }
+
+        // Add in the textDomain content if present
+        // TODO
+
+        attributes.objectDOM = $objectDOM[0];
+
+        return attributes;
+      },
+
+      /* Parse the nonNumericDomain/textDomain fragment
+       * returning an object with a textDomain attribute, like:
+       * {
+       *     textDomain: {
+       *         definition: "Some definition",
+       *         pattern: ["*", "\w", "[0-9]"],
+       *         source: "Some source reference"
+       *     }
+       * }
+       */
+      parseTextDomain: function (domain) {
+        var domainObject = {};
+        domainObject.textDomain = {};
+        var xmlID;
+        var definition;
+        let patterns = [];
+        var source;
+
+        // Add the XML id attribute
+        if ($(domain).attr("id")) {
+          xmlID = $(domain).attr("id");
+        } else {
+          // Generate an id if it's not found
+          xmlID = DataONEObject.generateId();
+        }
+        domainObject.textDomain.xmlID = xmlID;
+
+        // Add the definition
+        definition = $(domain).children("definition").text();
+        domainObject.textDomain.definition = definition;
+
+        // Add the pattern
+        _.each($(domain).children("pattern"), function (pattern) {
+          patterns.push(pattern.textContent);
+        });
+        domainObject.textDomain.pattern = patterns;
+
+        // Add the source
+        source = $(domain).children("sourced").text();
+        domainObject.textDomain.source = source;
+
+        return domainObject;
+      },
+
+      /* Parse the nonNumericDomain/enumeratedDomain fragment
+       * returning an object with an enumeratedDomain attribute, like:
+       * var emlCitation = {};
+       * var nonNumericDomain = [
+       *     {
+       *         enumeratedDomain: {
+       *             codeDefinition: [
+       *                 {
+       *                     code: "Some code", // required
+       *                     definition: "Some definition", // required
+       *                     source: "Some source"
+       *                 } // repeatable
+       *             ]
+       *         }
+       *     }, // or
+       *     {
+       *         enumeratedDomain: {
+       *             externalCodeSet: [
+       *                 {
+       *                     codesetName: "Some code", // required
+       *                     citation: [emlCitation], // one of citation or codesetURL
+       *                     codesetURL: ["Some URL"] // is required, both repeatable
+       *                 } // repeatable
+       *             ]
+       *         }
+       *     }, // or
+       *     {
+       *         enumeratedDomain: {
+       *             entityCodeList: {
+       *                 entityReference: "Some reference", // required
+       *                 valueAttributeReference: "Some attr reference", // required
+       *                 definitionAttributeReference: "Some definition attr reference", // required
+       *                 orderAttributeReference: "Some order attr reference"
+       *             }
+       *         }
+       *     }
+       * ]
+       */
+      parseEnumeratedDomain: function (domain) {
+        var domainObject = {};
+        domainObject.enumeratedDomain = {};
+        var codeDefinition = {};
+        var externalCodeSet = {};
+        var entityCodeList = {};
+        var xmlID;
+
+        // Add the XML id attribute
+        if ($(domain).attr("id")) {
+          xmlID = $(domain).attr("id");
+        } else {
+          // Generate an id if it's not found
+          xmlID = DataONEObject.generateId();
+        }
+        domainObject.enumeratedDomain.xmlID = xmlID;
+
+        // Add the codeDefinitions if present
+        var codeDefinitions = $(domain).children("codedefinition");
+
+        if (codeDefinitions.length) {
+          domainObject.enumeratedDomain.codeDefinition = [];
+          _.each(codeDefinitions, function (codeDef) {
+            var code = $(codeDef).children("code").text();
+            var definition = $(codeDef).children("definition").text();
+            var source = $(codeDef).children("sourced").text() || undefined;
+            domainObject.enumeratedDomain.codeDefinition.push({
+              code: code,
+              definition: definition,
+              source: source,
+            });
+          });
+        }
+        return domainObject;
+      },
+
+      /* Serialize the model to XML */
+      serialize: function () {
+        var objectDOM = this.updateDOM();
+        var xmlString = objectDOM.outerHTML;
+
+        // Camel-case the XML
+        xmlString = this.formatXML(xmlString);
+
+        return xmlString;
+      },
+
+      /* Copy the original XML DOM and update it with new values from the model */
+      updateDOM: function (objectDOM) {
+        var objectDOM;
+        var xmlID; // The id of the textDomain or enumeratedDomain fragment
+        var nonNumericDomainNode;
+        var domainType; // Either textDomain or enumeratedDomain
+        var $domainInDOM; // The jQuery object of the text or enumerated domain from the DOM
+        var nodeToInsertAfter;
+        var domainNode; // Either a textDomain or enumeratedDomain node
+        var definitionNode;
+        var patternNode;
+        var sourceNode;
+        var enumeratedDomainNode;
+        var codeDefinitions;
+        var codeDefinitionNode;
+        var codeNode;
+
+        var type = this.get("measurementScale");
+        if (typeof type === "undefined") {
+          console.warn("Defaulting to an nominal measurementScale.");
+          type = "nominal";
+        }
+        if (!objectDOM) {
+          objectDOM = this.get("objectDOM");
+        }
+        var objectXML = this.get("objectXML");
+
+        // If present, use the cached DOM
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+
+          // otherwise, use the cached XML
+        } else if (objectXML) {
+          objectDOM = $(objectXML)[0].cloneNode(true);
+
+          // This is new, create it
+        } else {
+          objectDOM = document.createElement(type);
+        }
+
+        if (this.get("nonNumericDomain").length) {
+          // Update each nonNumericDomain in the DOM
+          _.each(
+            this.get("nonNumericDomain"),
+            function (domain, i) {
+              // Is this a textDomain or enumeratedDomain?
+              if (typeof domain.textDomain === "object") {
+                domainType = "textDomain";
+                xmlID = domain.textDomain.xmlID;
+              } else if (typeof domain.enumeratedDomain === "object") {
+                domainType = "enumeratedDomain";
+                xmlID = domain.enumeratedDomain.xmlID;
+              } else {
+                console.log("Unrecognized NonNumericDomain type. Skipping.");
+                // TODO: Handle references here
+              }
 
-            /**
-             * The map of lower case to camel case node names
-             * needed to deal with parsing issues with $.parseHTML().
-             * Use this until we can figure out issues with $.parseXML().
-             * @type {object}
-             */
-            nodeNameMap: {
-                "nonnumericdomain": "nonNumericDomain",
-                "enumerateddomain": "enumeratedDomain",
-                "textdomain": "textDomain",
-                "externalcodeset": "externalCodeSet",
-                "codesetname": "codesetName",
-                "codeseturl": "codesetURL",
-                "entityCodeList": "entityCodeList",
-                "entityreference": "entityReference",
-                "valueattributereference": "valueAttributeReference",
-                "definitionattributereference": "definitionAttributeReference",
-                "orderattributereference": "orderAttributeReference",
-                "sourced": "source"
-            },
-
-            /* Initialize an EMLNonNumericDomain object */
-            initialize: function(attributes, options) {
-
-                this.on("change:nonNumericDomain", this.trickleUpChange);
-            },
-
-            /**
-             * Parse the incoming measurementScale's XML elements
-             */
-            parse: function(attributes, options) {
-
-                var $objectDOM;
-                var nonNumericDomainNodeList;
-                var domainNodeList; // the list of domain elements
-                var domain; // the text or enumerated domain to parse
-                var domainObject; // The parsed domain object to be added to attributes.nonNumericDomain
-                var rootNodeName; // Name of the fragment root elements
-
-                if ( attributes.objectDOM ) {
-                    rootNodeName = $(attributes.objectDOM)[0].localName;
-                    $objectDOM = $(attributes.objectDOM);
-                } else if ( attributes.objectXML ) {
-                    rootNodeName = $(attributes.objectXML)[0].localName;
-                    $objectDOM = $($(attributes.objectXML)[0]);
-                } else {
-                    return {};
-                }
-
-                // do we have an appropriate measurementScale tree?
-                var index = _.indexOf(["measurementscale", "nominal", "ordinal"], rootNodeName);
-                if ( index == -1 ) {
-                    throw new Error("The measurement scale XML does not have a root " +
-                        "node of 'measurementScale', 'nominal', or 'ordinal'.");
-                }
-
-                // If measurementScale is present, add it
-                if ( rootNodeName == "measurementscale" ) {
-                    attributes.measurementScale = $objectDOM.children().first()[0].localName;
-                    $objectDOM = $objectDOM.children().first();
-                } else {
-                    attributes.measurementScale = $objectDOM.localName;
-                }
-
-                nonNumericDomainNodeList = $objectDOM.find("nonnumericdomain");
-
-                if ( nonNumericDomainNodeList && nonNumericDomainNodeList.length > 0 ) {
-                    domainNodeList = nonNumericDomainNodeList[0].children;
-
-                } else {
-                    // No content is available, return
-                    return attributes;
-                }
-
-                // Initialize an array of nonNumericDomain objects
-                attributes.nonNumericDomain = [];
-
-                // Set each domain if we have it
-                if ( domainNodeList && domainNodeList.length > 0 ) {
-
-                    _.each(domainNodeList, function(domain) {
-                        if ( domain ) {
-                            // match the camelCase name since DOMParser() is XML-aware
-                            switch ( domain.localName ) {
-                                case "textdomain":
-                                    domainObject = this.parseTextDomain(domain);
-                                    break;
-                                case "enumerateddomain":
-                                    domainObject = this.parseEnumeratedDomain(domain);
-                                    break;
-                                case "references":
-                                    // TODO: Support references
-                                    console.log("In EMLNonNumericDomain.parse()" +
-                                        "We don't support references yet ");
-                                default:
-                                    console.log("Unrecognized nonNumericDomain: " + domain.nodeName);
-                            }
-                        }
-                        attributes.nonNumericDomain.push(domainObject);
-                    }, this);
-
+              // Update the existing DOM node by id
+              if (xmlID && $(objectDOM).find("#" + xmlID).length) {
+                if (domainType === "textDomain") {
+                  let originalTextDomain = $(objectDOM)
+                    .find("#" + xmlID)
+                    .find("textdomain");
+
+                  //If there are existing textDomain nodes in the DOM, update them
+                  if (originalTextDomain.length) {
+                    let updatedTextDomain = this.updateTextDomain(
+                      domain.textDomain,
+                      originalTextDomain,
+                    );
+                    originalTextDomain.replaceWith(updatedTextDomain);
+                  }
+                  //If there are no textDomain nodes in the DOM, create new ones
+                  else {
+                    //Create new textDomain nodes
+                    let newTextDomain = this.createTextDomain(
+                      domain.textDomain,
+                    );
+
+                    //Insert the new textDomain nodes into the nonNumericDomain node
+                    $($(objectDOM).children("nonnumericdomain")[i]).html(
+                      newTextDomain,
+                    );
+                  }
+                } else if (domainType === "enumeratedDomain") {
+                  this.updateEnumeratedDomainDOM(
+                    domain.enumeratedDomain,
+                    $domainInDOM,
+                  );
                 }
 
-                // Add in the textDomain content if present
-                // TODO
-
-                attributes.objectDOM = $objectDOM[0];
-
-                return attributes;
-            },
+                //If there is no XML ID but there are the same number of nonNumericDomains in the model and DOM
+              } else if (
+                this.get("nonNumericDomain").length ==
+                  $(objectDOM).children("nonnumericdomain").length &&
+                $(objectDOM).children("nonnumericdomain").length >= i
+              ) {
+                //If this is a text domain,
+                if (typeof domain.textDomain === "object") {
+                  let originalTextDomain = $(
+                    $(objectDOM).children("nonnumericdomain")[i],
+                  ).find("textdomain");
+
+                  //If there are existing textDomain nodes in the DOM, update them
+                  if (originalTextDomain.length) {
+                    let updatedTextDomain = this.updateTextDomain(
+                      domain.textDomain,
+                      originalTextDomain,
+                    );
+                    originalTextDomain.replaceWith(updatedTextDomain);
+                  }
+                  //If there are no textDomain nodes in the DOM, create new ones
+                  else {
+                    //Create new textDomain nodes
+                    var newTextDomain = this.createTextDomain(
+                      domain.textDomain,
+                    );
+
+                    //Insert the new textDomain nodes into the nonNumericDomain node
+                    $($(objectDOM).children("nonnumericdomain")[i]).html(
+                      newTextDomain,
+                    );
+                  }
+                } else if (typeof domain.enumeratedDomain === "object") {
+                  //Get the nonNumericDomain node from the DOM
+                  var nonNumericDomainNode =
+                      $(objectDOM).children("nonnumericdomain")[i],
+                    enumeratedDomain =
+                      $(nonNumericDomainNode).children("enumerateddomain");
+
+                  if (enumeratedDomain.length) {
+                    this.updateEnumeratedDomainDOM(
+                      domain.enumeratedDomain,
+                      enumeratedDomain,
+                    );
+                  } else {
+                    //Remove the textDomain node and replace it with an enumeratedDomain node
+                    var textDomainToReplace = $(objectDOM).find("textdomain");
 
-            /* Parse the nonNumericDomain/textDomain fragment
-             * returning an object with a textDomain attribute, like:
-             * {
-             *     textDomain: {
-             *         definition: "Some definition",
-             *         pattern: ["*", "\w", "[0-9]"],
-             *         source: "Some source reference"
-             *     }
-             * }
-             */
-            parseTextDomain: function(domain) {
-                var domainObject = {};
-                domainObject.textDomain = {};
-                var xmlID;
-                var definition;
-                let patterns = [];
-                var source;
-
-                // Add the XML id attribute
-                if ( $(domain).attr("id") ) {
-                    xmlID = $(domain).attr("id");
-                } else {
-                    // Generate an id if it's not found
-                    xmlID = DataONEObject.generateId();
+                    if (textDomainToReplace.length > 0) {
+                      $(textDomainToReplace[i]).replaceWith(
+                        this.createEnumeratedDomainDOM(domain.enumeratedDomain),
+                      );
+                    } else {
+                      nonNumericDomainNode.html(
+                        this.createEnumeratedDomainDOM(
+                          domain.enumeratedDomain,
+                          document.createElement("enumerateddomain"),
+                        ),
+                      );
+                    }
+                  }
                 }
-                domainObject.textDomain.xmlID = xmlID;
 
-                // Add the definition
-                definition = $(domain).children("definition").text();
-                domainObject.textDomain.definition = definition;
-
-                // Add the pattern
-                _.each($(domain).children("pattern"), function(pattern) {
-                     patterns.push(pattern.textContent);
-                });
-                domainObject.textDomain.pattern = patterns;
-
-                // Add the source
-                source = $(domain).children("sourced").text();
-                domainObject.textDomain.source = source;
+                // Otherwise append to the DOM
+              } else {
+                // Add the nonNumericDomain element
+                nonNumericDomainNode =
+                  document.createElement("nonnumericdomain");
+
+                if (domainType === "textDomain") {
+                  // Add the definiton element
+                  domainNode = document.createElement("textdomain");
+                  if (domain.textDomain.definition) {
+                    definitionNode = document.createElement("definition");
+                    $(definitionNode).text(domain.textDomain.definition);
+                    $(domainNode).append(definitionNode);
+                  }
 
-                 return domainObject;
-            },
+                  // Add the pattern element(s)
+                  if (domain.textDomain.pattern.length) {
+                    _.each(
+                      domain.textDomain.pattern,
+                      function (pattern) {
+                        patternNode = document.createElement("pattern");
+                        $(patternNode).text(pattern);
+                        $(domainNode).append(patternNode);
+                      },
+                      this,
+                    );
+                  }
 
-            /* Parse the nonNumericDomain/enumeratedDomain fragment
-             * returning an object with an enumeratedDomain attribute, like:
-             * var emlCitation = {};
-             * var nonNumericDomain = [
-             *     {
-             *         enumeratedDomain: {
-             *             codeDefinition: [
-             *                 {
-             *                     code: "Some code", // required
-             *                     definition: "Some definition", // required
-             *                     source: "Some source"
-             *                 } // repeatable
-             *             ]
-             *         }
-             *     }, // or
-             *     {
-             *         enumeratedDomain: {
-             *             externalCodeSet: [
-             *                 {
-             *                     codesetName: "Some code", // required
-             *                     citation: [emlCitation], // one of citation or codesetURL
-             *                     codesetURL: ["Some URL"] // is required, both repeatable
-             *                 } // repeatable
-             *             ]
-             *         }
-             *     }, // or
-             *     {
-             *         enumeratedDomain: {
-             *             entityCodeList: {
-             *                 entityReference: "Some reference", // required
-             *                 valueAttributeReference: "Some attr reference", // required
-             *                 definitionAttributeReference: "Some definition attr reference", // required
-             *                 orderAttributeReference: "Some order attr reference"
-             *             }
-             *         }
-             *     }
-             * ]
-             */
-            parseEnumeratedDomain: function(domain) {
-                var domainObject = {};
-                domainObject.enumeratedDomain = {};
-                var codeDefinition = {};
-                var externalCodeSet = {};
-                var entityCodeList = {};
-                var xmlID;
-
-                // Add the XML id attribute
-                if ( $(domain).attr("id") ) {
-                    xmlID = $(domain).attr("id");
+                  // Add the source element
+                  if (domain.textDomain.source) {
+                    sourceNode = document.createElement("sourced"); // Accommodate parseHTML() with "d"
+                    $(sourceNode).text(domain.textDomain.source);
+                    $(domainNode).append(sourceNode);
+                  }
+                } else if (domainType === "enumeratedDomain") {
+                  nonNumericDomainNode.append(
+                    this.createEnumeratedDomainDOM(domain.enumeratedDomain),
+                  );
                 } else {
-                    // Generate an id if it's not found
-                    xmlID = DataONEObject.generateId();
+                  console.log(
+                    "The domainType: " + domainType + " is not recognized.",
+                  );
                 }
-                domainObject.enumeratedDomain.xmlID = xmlID;
-
-                // Add the codeDefinitions if present
-                var codeDefinitions = $(domain).children("codedefinition");
-
-                if ( codeDefinitions.length ) {
-                    domainObject.enumeratedDomain.codeDefinition = [];
-                    _.each(codeDefinitions, function(codeDef) {
-                        var code = $(codeDef).children("code").text();
-                        var definition = $(codeDef).children("definition").text();
-                        var source = $(codeDef).children("sourced").text() || undefined;
-                        domainObject.enumeratedDomain.codeDefinition.push({
-                            code: code,
-                            definition: definition,
-                            source: source
-                        });
-                    })
-                }
-                return domainObject;
+                $(nonNumericDomainNode).append(domainNode);
+                $(objectDOM).append(nonNumericDomainNode);
+              }
             },
-
-            /* Serialize the model to XML */
-            serialize: function() {
-                var objectDOM = this.updateDOM();
-                var xmlString = objectDOM.outerHTML;
-
-                // Camel-case the XML
-                xmlString = this.formatXML(xmlString);
-
-                return xmlString;
+            this,
+          );
+        } else {
+          // We have no content, so can't create a valid domain
+          console.log(
+            "In EMLNonNumericDomain.updateDOM(),\n" +
+              "references are not handled yet. Returning undefined.",
+          );
+          // TODO: handle references here
+          return undefined;
+        }
+        return objectDOM;
+      },
+
+      /*
+       * Update the codeDefinitionList in the  first enumeratedDomain
+       * found in the nonNumericDomain array.
+       * TODO: Refactor this to support externalCodeSet and entityCodeList
+       * TODO: Support the source field
+       * TODO: Support repeatable enumeratedDomains
+       * var nonNumericDomain = [
+       *     {
+       *         enumeratedDomain: {
+       *             codeDefinition: [
+       *                 {
+       *                     code: "Some code", // required
+       *                     definition: "Some definition", // required
+       *                     source: "Some source"
+       *                 } // repeatable
+       *             ]
+       *         }
+       *     }
+       * ]
+       */
+      updateEnumeratedDomain: function (code, definition, index) {
+        var nonNumericDomain = this.get("nonNumericDomain");
+        var enumeratedDomain = {};
+        var codeDefinitions;
+
+        if (typeof code == "string" && !code.trim().length) {
+          code = "";
+        }
+
+        if (typeof definition == "string" && !definition.trim().length) {
+          definition = "";
+        }
+
+        // Create from scratch
+        if (
+          !nonNumericDomain.length ||
+          !nonNumericDomain[0] ||
+          !nonNumericDomain[0].enumeratedDomain
+        ) {
+          nonNumericDomain[0] = {
+            enumeratedDomain: {
+              codeDefinition: [
+                {
+                  code: code,
+                  definition: definition,
+                },
+              ],
             },
+          };
+        }
+        // Update existing
+        else {
+          enumeratedDomain = this.get("nonNumericDomain")[0].enumeratedDomain;
+
+          if (typeof enumeratedDomain !== "undefined") {
+            //If there is no code or definition, then remove it from the code list
+            if (!code && code !== 0 && !definition && definition !== 0) {
+              this.removeCode(index);
+            } else if (enumeratedDomain.codeDefinition.length >= index) {
+              //Create a new code object and insert it into the array
+              enumeratedDomain.codeDefinition[index] = {
+                code: code,
+                definition: definition,
+              };
+            } else {
+              //Create a new code object and append it to the end of the array
+              enumeratedDomain.codeDefinition.push({
+                code: code,
+                definition: definition,
+              });
+            }
+          }
+        }
+
+        //Manually trigger the change event since we're updating an array on the model
+        this.trigger("change:nonNumericDomain");
+      },
+
+      /*
+       * Given a `codeDefinition` HTML node and an enumeratedDomain list,
+       *   this function will update the HTML node code definitions with
+       *   all the code definitions listed in the enumeratedDomain
+       *
+       * @param {object} enumeratedDomain - A literal object with an array of codeDefinitions
+       * @param {DOM Element or jQuery Object} - A DOM Element or jQuery selection that represents the <enumeratedDomain> node
+       */
+      updateEnumeratedDomainDOM: function (
+        enumeratedDomain,
+        enumeratedDomainNode,
+      ) {
+        if (enumeratedDomain.codeDefinition.length) {
+          // Update each codeDefinition
+          _.each(
+            enumeratedDomain.codeDefinition,
+            function (codeDef, i) {
+              var codeDefNode = $(
+                $(enumeratedDomainNode).children("codedefinition")[i],
+              );
+
+              if (!codeDefNode.length) {
+                codeDefNode = $(document.createElement("codedefinition"));
+                $(enumeratedDomainNode).append(codeDefNode);
+              }
 
-            /* Copy the original XML DOM and update it with new values from the model */
-            updateDOM: function(objectDOM) {
-                var objectDOM;
-                var xmlID; // The id of the textDomain or enumeratedDomain fragment
-                var nonNumericDomainNode;
-                var domainType; // Either textDomain or enumeratedDomain
-                var $domainInDOM; // The jQuery object of the text or enumerated domain from the DOM
-                var nodeToInsertAfter;
-                var domainNode; // Either a textDomain or enumeratedDomain node
-                var definitionNode;
-                var patternNode;
-                var sourceNode;
-                var enumeratedDomainNode;
-                var codeDefinitions;
-                var codeDefinitionNode;
-                var codeNode;
-
-                var type = this.get("measurementScale");
-                if ( typeof type === "undefined") {
-                    console.warn("Defaulting to an nominal measurementScale.");
-                    type = "nominal";
-                }
-                if ( ! objectDOM ) {
-                    objectDOM = this.get("objectDOM");
-                }
-                var objectXML = this.get("objectXML");
-
-                // If present, use the cached DOM
-                if ( objectDOM ) {
-                    objectDOM = objectDOM.cloneNode(true);
-
-                // otherwise, use the cached XML
-                } else if ( objectXML ){
-                    objectDOM = $(objectXML)[0].cloneNode(true);
-
-                // This is new, create it
-                } else {
-                    objectDOM = document.createElement(type);
+              // Update the required code element
+              if (codeDef.code) {
+                var codeNode = codeDefNode.children("code");
 
+                //If there is no <code> XML node, make one
+                if (!codeNode.length) {
+                  codeNode = $(document.createElement("code"));
+                  codeDefNode.append(codeNode);
                 }
 
-                if ( this.get("nonNumericDomain").length ) {
-
-                    // Update each nonNumericDomain in the DOM
-                    _.each(this.get("nonNumericDomain"), function(domain, i) {
-
-                        // Is this a textDomain or enumeratedDomain?
-                        if ( typeof domain.textDomain === "object" ) {
-                            domainType = "textDomain";
-                            xmlID = domain.textDomain.xmlID;
-
-                        } else if ( typeof domain.enumeratedDomain === "object" ) {
-                            domainType = "enumeratedDomain";
-                            xmlID = domain.enumeratedDomain.xmlID;
-                        } else {
-                            console.log("Unrecognized NonNumericDomain type. Skipping.");
-                            // TODO: Handle references here
-                        }
-
-                        // Update the existing DOM node by id
-                        if ( xmlID && $(objectDOM).find("#" + xmlID).length ) {
-
-                            if ( domainType === "textDomain" ) {
-
-                                let originalTextDomain = $(objectDOM).find("#" + xmlID).find("textdomain");
-
-                                //If there are existing textDomain nodes in the DOM, update them
-                                if( originalTextDomain.length ){
-                                    let updatedTextDomain = this.updateTextDomain(domain.textDomain, originalTextDomain);
-                                    originalTextDomain.replaceWith(updatedTextDomain);
-                                }
-                                //If there are no textDomain nodes in the DOM, create new ones
-                                else{
-                                  //Create new textDomain nodes
-                                  let newTextDomain = this.createTextDomain(domain.textDomain);
-
-                                  //Insert the new textDomain nodes into the nonNumericDomain node
-                                  $( $(objectDOM).children("nonnumericdomain")[i] ).html( newTextDomain );
-                                }
-
-                            } else if ( domainType === "enumeratedDomain") {
-
-                              this.updateEnumeratedDomainDOM(domain.enumeratedDomain, $domainInDOM);
-                            }
-
-
-                        //If there is no XML ID but there are the same number of nonNumericDomains in the model and DOM
-                      } else if( this.get("nonNumericDomain").length == $(objectDOM).children("nonnumericdomain").length
-                          && $(objectDOM).children("nonnumericdomain").length >= i){
-
-                            //If this is a text domain,
-                            if( typeof domain.textDomain === "object" ){
-
-                                let originalTextDomain = $($(objectDOM).children("nonnumericdomain")[i]).find("textdomain");
-
-                                //If there are existing textDomain nodes in the DOM, update them
-                                if( originalTextDomain.length ){
-                                    let updatedTextDomain = this.updateTextDomain(domain.textDomain, originalTextDomain);
-                                    originalTextDomain.replaceWith(updatedTextDomain);
-                                }
-                                //If there are no textDomain nodes in the DOM, create new ones
-                                else{
-                                  //Create new textDomain nodes
-                                  var newTextDomain = this.createTextDomain(domain.textDomain);
-
-                                  //Insert the new textDomain nodes into the nonNumericDomain node
-                                  $( $(objectDOM).children("nonnumericdomain")[i] ).html( newTextDomain );
-                                }
-
-                            }
-                            else if(typeof domain.enumeratedDomain === "object"){
-                              //Get the nonNumericDomain node from the DOM
-                              var nonNumericDomainNode = $(objectDOM).children("nonnumericdomain")[i],
-                                  enumeratedDomain     = $(nonNumericDomainNode).children("enumerateddomain");
-
-                              if( enumeratedDomain.length ){
-                                this.updateEnumeratedDomainDOM(domain.enumeratedDomain, enumeratedDomain);
-                              }
-                              else{
-                                //Remove the textDomain node and replace it with an enumeratedDomain node
-                                var textDomainToReplace = $(objectDOM).find("textdomain");
-
-                                if( textDomainToReplace.length > 0 ){
-                                  $(textDomainToReplace[i]).replaceWith(this.createEnumeratedDomainDOM(domain.enumeratedDomain));
-                                }
-                                else{
-                                  nonNumericDomainNode.html(this.createEnumeratedDomainDOM(domain.enumeratedDomain, document.createElement("enumerateddomain")));
-                                }
-
-
-                              }
-                            }
-
-                        // Otherwise append to the DOM
-                        } else {
-
-                            // Add the nonNumericDomain element
-                            nonNumericDomainNode = document.createElement("nonnumericdomain");
-
-                            if ( domainType === "textDomain" ) {
-
-                                // Add the definiton element
-                                domainNode = document.createElement("textdomain");
-                                if ( domain.textDomain.definition ) {
-                                    definitionNode = document.createElement("definition");
-                                    $(definitionNode).text(domain.textDomain.definition);
-                                    $(domainNode).append(definitionNode);
-                                }
-
-                                // Add the pattern element(s)
-                                if ( domain.textDomain.pattern.length ) {
-                                    _.each(domain.textDomain.pattern, function(pattern) {
-                                        patternNode = document.createElement("pattern");
-                                        $(patternNode).text(pattern);
-                                        $(domainNode).append(patternNode);
-                                    }, this);
-                                }
-
-                                // Add the source element
-                                if ( domain.textDomain.source ) {
-                                    sourceNode = document.createElement("sourced"); // Accommodate parseHTML() with "d"
-                                    $(sourceNode).text(domain.textDomain.source);
-                                    $(domainNode).append(sourceNode);
-                                }
-
-                            } else if ( domainType === "enumeratedDomain" ) {
-
-                              nonNumericDomainNode.append(this.createEnumeratedDomainDOM(domain.enumeratedDomain));
-
-                            } else {
-                                console.log("The domainType: " + domainType + " is not recognized.");
-                            }
-                            $(nonNumericDomainNode).append(domainNode);
-                            $(objectDOM).append(nonNumericDomainNode);
-                        }
-                    }, this);
-
-                } else {
-                    // We have no content, so can't create a valid domain
-                    console.log("In EMLNonNumericDomain.updateDOM(),\n" +
-                        "references are not handled yet. Returning undefined.");
-                    // TODO: handle references here
-                    return undefined;
-                }
-                return objectDOM;
-            },
-
-            /*
-             * Update the codeDefinitionList in the  first enumeratedDomain
-             * found in the nonNumericDomain array.
-             * TODO: Refactor this to support externalCodeSet and entityCodeList
-             * TODO: Support the source field
-             * TODO: Support repeatable enumeratedDomains
-             * var nonNumericDomain = [
-             *     {
-             *         enumeratedDomain: {
-             *             codeDefinition: [
-             *                 {
-             *                     code: "Some code", // required
-             *                     definition: "Some definition", // required
-             *                     source: "Some source"
-             *                 } // repeatable
-             *             ]
-             *         }
-             *     }
-             * ]
-             */
-            updateEnumeratedDomain: function(code, definition, index) {
-                var nonNumericDomain = this.get("nonNumericDomain");
-                var enumeratedDomain = {};
-                var codeDefinitions;
-
-                if( typeof code == "string" && !code.trim().length ){
-                  code = "";
-                }
+                //Add the code text to the <code> node
+                codeNode.text(codeDef.code);
+              } else {
+                codeDefNode.children("code").remove();
+              }
 
-                if( typeof definition == "string" && !definition.trim().length ){
-                  definition = "";
-                }
+              // Update the required definition element
+              if (codeDef.definition) {
+                var defNode = codeDefNode.children("definition");
 
-                // Create from scratch
-                if ( !nonNumericDomain.length || !nonNumericDomain[0] || !nonNumericDomain[0].enumeratedDomain) {
-                    nonNumericDomain[0] = {
-                        enumeratedDomain: {
-                            codeDefinition: [
-                                {
-                                    code: code,
-                                    definition: definition
-                                }
-                            ]
-                        }
-                    }
-                }
-                // Update existing
-                else {
-                    enumeratedDomain = this.get("nonNumericDomain")[0].enumeratedDomain;
-
-                    if ( typeof enumeratedDomain !== "undefined" ) {
-
-                      //If there is no code or definition, then remove it from the code list
-                      if( !code && code !== 0 && !definition && definition !== 0 ){
-                        this.removeCode(index);
-                      }
-                      else if ( enumeratedDomain.codeDefinition.length >= index ) {
-                        //Create a new code object and insert it into the array
-                        enumeratedDomain.codeDefinition[index] = {
-                            code: code,
-                            definition: definition
-                        }
-                      }
-                      else {
-                        //Create a new code object and append it to the end of the array
-                        enumeratedDomain.codeDefinition.push({
-                            code: code,
-                            definition: definition
-                        });
-                      }
-                    }
+                //If there is no <definition> XML node, make one
+                if (!defNode.length) {
+                  defNode = $(document.createElement("definition"));
+                  codeDefNode.append(defNode);
                 }
 
-                //Manually trigger the change event since we're updating an array on the model
-                this.trigger("change:nonNumericDomain");
-            },
-
-            /*
-             * Given a `codeDefinition` HTML node and an enumeratedDomain list,
-             *   this function will update the HTML node code definitions with
-             *   all the code definitions listed in the enumeratedDomain
-             *
-             * @param {object} enumeratedDomain - A literal object with an array of codeDefinitions
-             * @param {DOM Element or jQuery Object} - A DOM Element or jQuery selection that represents the <enumeratedDomain> node
-             */
-            updateEnumeratedDomainDOM: function( enumeratedDomain, enumeratedDomainNode ){
-
-              if ( enumeratedDomain.codeDefinition.length ) {
-
-                  // Update each codeDefinition
-                  _.each(enumeratedDomain.codeDefinition, function(codeDef, i) {
-
-                      var codeDefNode = $($(enumeratedDomainNode).children("codedefinition")[i]);
-
-                      if( !codeDefNode.length ){
-                        codeDefNode = $(document.createElement("codedefinition"));
-                        $(enumeratedDomainNode).append(codeDefNode);
-                      }
-
-                      // Update the required code element
-                      if ( codeDef.code ) {
-                        var codeNode = codeDefNode.children("code");
-
-                        //If there is no <code> XML node, make one
-                        if( !codeNode.length ){
-                          codeNode = $(document.createElement("code"));
-                          codeDefNode.append(codeNode);
-                        }
-
-                        //Add the code text to the <code> node
-                        codeNode.text(codeDef.code);
-                      }
-                      else{
-                        codeDefNode.children("code").remove();
-                      }
-
-                      // Update the required definition element
-                      if ( codeDef.definition ) {
-                        var defNode = codeDefNode.children("definition");
-
-                        //If there is no <definition> XML node, make one
-                        if( !defNode.length ){
-                          defNode = $(document.createElement("definition"));
-                          codeDefNode.append(defNode);
-                        }
-
-                        //Add the definition text to the <definition> node
-                        defNode.text(codeDef.definition);
-                      }
-                      else{
-                        codeDefNode.children("definition").remove();
-                      }
-
-                      // Update the optional source element
-                      if ( codeDef.source ) {
-                        // Accommodate parseHTML() with source"d"
-                        var sourceNode = codeDefNode.children("sourced");
-
-                        //If there is no <source> XML node, make one
-                        if( !sourceNode.length ){
-                          sourceNode = $(document.createElement("sourced"));
-                          codeDefNode.append(sourceNode);
-                        }
-
-                        sourceNode.text(codeDef.source);
-                      }
-                      else{
-                        codeDefNode.children("sourced").remove();
-                      }
-
-                  }, this);
-
-                  // If there are more codeDefinition nodes than there are codeDefinitions
-                  // in the model, then we need to remove the extraneous nodes
-                  var numNodes = $(enumeratedDomainNode).children("codedefinition").length,
-                      numCodes = enumeratedDomain.codeDefinition.length;
-
-                  if( numNodes > numCodes ){
-                    //Get the extraneous nodes by selecting the last X child elements
-                    var nodesToRemove = $(enumeratedDomainNode).children("codedefinition").slice( (numNodes - numCodes) * -1 );
-                    //Remove them from the DOM
-                    nodesToRemove.remove();
-                  }
-
-              } else if ( domain.enumeratedDomain.externalCodeSet ) {
-                  // TODO Handle externalCodeSet
-
-              } else if ( domain.enumeratedDomain.entityCodeList ) {
-                  // TODO Handle entityCodeList
+                //Add the definition text to the <definition> node
+                defNode.text(codeDef.definition);
+              } else {
+                codeDefNode.children("definition").remove();
               }
 
-              return enumeratedDomainNode;
+              // Update the optional source element
+              if (codeDef.source) {
+                // Accommodate parseHTML() with source"d"
+                var sourceNode = codeDefNode.children("sourced");
 
-            },
+                //If there is no <source> XML node, make one
+                if (!sourceNode.length) {
+                  sourceNode = $(document.createElement("sourced"));
+                  codeDefNode.append(sourceNode);
+                }
 
-            /*
-             * Given an enumeratedDomain list, this function will create an
-             *   <enumeratedDomain> HTML element with all the code definitions
-             *   listed in the enumeratedDomain object
-             *
-             * @param {object} enumeratedDomain - A literal object with an array of codeDefinitions
-             * @return {DOM Element} - An <enumerateddomain> DOM element tree with code definitions
-             */
-            createEnumeratedDomainDOM: function( enumeratedDomain ){
-
-              var enumeratedDomainNode = document.createElement("enumerateddomain");
-
-              if ( enumeratedDomain.codeDefinition.length ) {
-
-                  // Add each codeDefinition
-                  _.each(enumeratedDomain.codeDefinition, function(codeDef) {
-
-                      var codeDefinitionNode = document.createElement("codedefinition");
-
-                      // Add the required code element
-                      if ( codeDef.code ) {
-                          var codeNode = document.createElement("code");
-                          $(codeNode).text(codeDef.code);
-                          $(codeDefinitionNode).append(codeNode);
-                      }
-
-                      // Add the required definition element
-                      if ( codeDef.definition ) {
-                          var definitionNode = document.createElement("definition");
-                          $(definitionNode).text(codeDef.definition);
-                          $(codeDefinitionNode).append(definitionNode);
-                      }
-
-                      // Add the optional source element
-                      if ( codeDef.source ) {
-                          var sourceNode = document.createElement("sourced"); // Accommodate parseHTML() with "d"
-                          $(sourceNode).text(codeDef.source);
-                          $(codeDefinitionNode).append(sourceNode);
-                      }
-                      $(enumeratedDomainNode).append(codeDefinitionNode);
-
-                  }, this);
-
-              } else if ( domain.enumeratedDomain.externalCodeSet ) {
-                  // TODO Handle externalCodeSet
-
-              } else if ( domain.enumeratedDomain.entityCodeList ) {
-                  // TODO Handle entityCodeList
+                sourceNode.text(codeDef.source);
+              } else {
+                codeDefNode.children("sourced").remove();
               }
-
-              return enumeratedDomainNode;
-
             },
-
-            /*
-             * Given a textDomain object, and textDomain DOM object, this function
-             *  will update all the DOM elements with the textDomain object values
-             *
-             * @param {object} textDomain - A literal object representing an EML text domain
-             * @param {DOM Element} textDomainEl - The <textDomain> DOM Element to update
-             * @return {DOM Element} - An <textdomain> DOM element tree to update
-             */
-            updateTextDomain: function(textDomain, textDomainEl){
-
-              if( typeof textDomainEl === "undefined" || (typeof textDomainEl == "object" && textDomainEl.length == 0) )
-                var textDomainEl = document.createElement("textdomain");
-
-              //Create a shortcut to the jQuery object of the text domain element
-              var $textDomainEl = $(textDomainEl);
-
-              var definitionEl = $textDomainEl.find("definition");
-
-              //Update the definition element text
-              if( definitionEl.length > 0 )
-                definitionEl.text(textDomain.definition);
-              else {
-                $textDomainEl.prepend( $(document.createElement("definition")).text(textDomain.definition) );
+            this,
+          );
+
+          // If there are more codeDefinition nodes than there are codeDefinitions
+          // in the model, then we need to remove the extraneous nodes
+          var numNodes =
+              $(enumeratedDomainNode).children("codedefinition").length,
+            numCodes = enumeratedDomain.codeDefinition.length;
+
+          if (numNodes > numCodes) {
+            //Get the extraneous nodes by selecting the last X child elements
+            var nodesToRemove = $(enumeratedDomainNode)
+              .children("codedefinition")
+              .slice((numNodes - numCodes) * -1);
+            //Remove them from the DOM
+            nodesToRemove.remove();
+          }
+        } else if (domain.enumeratedDomain.externalCodeSet) {
+          // TODO Handle externalCodeSet
+        } else if (domain.enumeratedDomain.entityCodeList) {
+          // TODO Handle entityCodeList
+        }
+
+        return enumeratedDomainNode;
+      },
+
+      /*
+       * Given an enumeratedDomain list, this function will create an
+       *   <enumeratedDomain> HTML element with all the code definitions
+       *   listed in the enumeratedDomain object
+       *
+       * @param {object} enumeratedDomain - A literal object with an array of codeDefinitions
+       * @return {DOM Element} - An <enumerateddomain> DOM element tree with code definitions
+       */
+      createEnumeratedDomainDOM: function (enumeratedDomain) {
+        var enumeratedDomainNode = document.createElement("enumerateddomain");
+
+        if (enumeratedDomain.codeDefinition.length) {
+          // Add each codeDefinition
+          _.each(
+            enumeratedDomain.codeDefinition,
+            function (codeDef) {
+              var codeDefinitionNode = document.createElement("codedefinition");
+
+              // Add the required code element
+              if (codeDef.code) {
+                var codeNode = document.createElement("code");
+                $(codeNode).text(codeDef.code);
+                $(codeDefinitionNode).append(codeNode);
               }
 
-              // Remove existing patterns
-              $textDomainEl.find("pattern").remove();
-
-              // Add any new patterns
-              if ( textDomain.pattern && textDomain.pattern.length ) {
-
-                  let patterns = Array.from(textDomain.pattern).reverse();
-
-                  _.each(patterns, function(pattern) {
-
-                    //Don't serialize strings with only empty characters
-                    if( typeof pattern == "string" && !pattern.trim().length )
-                      return;
-
-                    var patternNode = document.createElement("pattern");
-
-                    $(patternNode).text(pattern);
-
-                    // Prepend before the sourced element if present
-                    if ( $textDomainEl.find("sourced").length ) {
-                        $textDomainEl.find("sourced").before(patternNode);
-                    } else {
-                        $textDomainEl.append(patternNode);
-                    }
-                  });
+              // Add the required definition element
+              if (codeDef.definition) {
+                var definitionNode = document.createElement("definition");
+                $(definitionNode).text(codeDef.definition);
+                $(codeDefinitionNode).append(definitionNode);
               }
 
-              // Update any new source
-              if ( textDomain.source ) {
-                  if ( $textDomainEl.find("sourced").length ) {
-                      $textDomainEl.find("sourced").text(textDomain.source);
-                  } else {
-                      //
-                      var src = document.createElement("sourced");
-                      src.textContent = textDomain.source;
-                      $textDomainEl.find("textDomain").append(src);
-                  }
-              } else {
-                  // Remove the source in the DOM not present in the textDomain
-                  // TODO: Uncomment this when we support "source" in the UI
-                  // $domainInDOM.children("source").remove();
-
+              // Add the optional source element
+              if (codeDef.source) {
+                var sourceNode = document.createElement("sourced"); // Accommodate parseHTML() with "d"
+                $(sourceNode).text(codeDef.source);
+                $(codeDefinitionNode).append(sourceNode);
               }
-
-              return textDomainEl;
+              $(enumeratedDomainNode).append(codeDefinitionNode);
             },
-
-            /*
-             * Creates a textDomain DOM object with the textDomain object values
-             *
-             * @param {object} textDomain - A literal object representing an EML text domain
-             * @return {DOM Element} - An <textdomain> DOM element tree to update
-             */
-            createTextDomain: function(textDomain){
-              var textDomainEl = document.createElement("textdomain");
-
-              this.updateTextDomain(textDomain, textDomainEl);
-
-              return textDomainEl;
-
-            },
-
-            /*
-             * Get the DOM node preceding the given nodeName
-             * to find what position in the EML document
-             * the named node should be appended
-             */
-            getEMLPosition: function(objectDOM, nodeName) {
-                // TODO: set the node order
-                var nodeOrder = ["enumerateddomain", "textdomain"];
-
-                var position = _.indexOf(nodeOrder, nodeName);
-
-                // Append to the bottom if not found
-                if ( position == -1 ) {
-                    return $(objectDOM).children().last()[0];
-                }
-
-                // Otherwise, go through each node in the node list and find the
-                // position where this node will be inserted after
-                for ( var i = position - 1; i >= 0; i-- ) {
-                    if ( $(objectDOM).find(nodeOrder[i]).length ) {
-                        return $(objectDOM).find(nodeOrder[i]).last()[0];
-                    }
+            this,
+          );
+        } else if (domain.enumeratedDomain.externalCodeSet) {
+          // TODO Handle externalCodeSet
+        } else if (domain.enumeratedDomain.entityCodeList) {
+          // TODO Handle entityCodeList
+        }
+
+        return enumeratedDomainNode;
+      },
+
+      /*
+       * Given a textDomain object, and textDomain DOM object, this function
+       *  will update all the DOM elements with the textDomain object values
+       *
+       * @param {object} textDomain - A literal object representing an EML text domain
+       * @param {DOM Element} textDomainEl - The <textDomain> DOM Element to update
+       * @return {DOM Element} - An <textdomain> DOM element tree to update
+       */
+      updateTextDomain: function (textDomain, textDomainEl) {
+        if (
+          typeof textDomainEl === "undefined" ||
+          (typeof textDomainEl == "object" && textDomainEl.length == 0)
+        )
+          var textDomainEl = document.createElement("textdomain");
+
+        //Create a shortcut to the jQuery object of the text domain element
+        var $textDomainEl = $(textDomainEl);
+
+        var definitionEl = $textDomainEl.find("definition");
+
+        //Update the definition element text
+        if (definitionEl.length > 0) definitionEl.text(textDomain.definition);
+        else {
+          $textDomainEl.prepend(
+            $(document.createElement("definition")).text(textDomain.definition),
+          );
+        }
+
+        // Remove existing patterns
+        $textDomainEl.find("pattern").remove();
+
+        // Add any new patterns
+        if (textDomain.pattern && textDomain.pattern.length) {
+          let patterns = Array.from(textDomain.pattern).reverse();
+
+          _.each(patterns, function (pattern) {
+            //Don't serialize strings with only empty characters
+            if (typeof pattern == "string" && !pattern.trim().length) return;
+
+            var patternNode = document.createElement("pattern");
+
+            $(patternNode).text(pattern);
+
+            // Prepend before the sourced element if present
+            if ($textDomainEl.find("sourced").length) {
+              $textDomainEl.find("sourced").before(patternNode);
+            } else {
+              $textDomainEl.append(patternNode);
+            }
+          });
+        }
+
+        // Update any new source
+        if (textDomain.source) {
+          if ($textDomainEl.find("sourced").length) {
+            $textDomainEl.find("sourced").text(textDomain.source);
+          } else {
+            //
+            var src = document.createElement("sourced");
+            src.textContent = textDomain.source;
+            $textDomainEl.find("textDomain").append(src);
+          }
+        } else {
+          // Remove the source in the DOM not present in the textDomain
+          // TODO: Uncomment this when we support "source" in the UI
+          // $domainInDOM.children("source").remove();
+        }
+
+        return textDomainEl;
+      },
+
+      /*
+       * Creates a textDomain DOM object with the textDomain object values
+       *
+       * @param {object} textDomain - A literal object representing an EML text domain
+       * @return {DOM Element} - An <textdomain> DOM element tree to update
+       */
+      createTextDomain: function (textDomain) {
+        var textDomainEl = document.createElement("textdomain");
+
+        this.updateTextDomain(textDomain, textDomainEl);
+
+        return textDomainEl;
+      },
+
+      /*
+       * Get the DOM node preceding the given nodeName
+       * to find what position in the EML document
+       * the named node should be appended
+       */
+      getEMLPosition: function (objectDOM, nodeName) {
+        // TODO: set the node order
+        var nodeOrder = ["enumerateddomain", "textdomain"];
+
+        var position = _.indexOf(nodeOrder, nodeName);
+
+        // Append to the bottom if not found
+        if (position == -1) {
+          return $(objectDOM).children().last()[0];
+        }
+
+        // Otherwise, go through each node in the node list and find the
+        // position where this node will be inserted after
+        for (var i = position - 1; i >= 0; i--) {
+          if ($(objectDOM).find(nodeOrder[i]).length) {
+            return $(objectDOM).find(nodeOrder[i]).last()[0];
+          }
+        }
+      },
+
+      /* Let the top level package know of attribute changes from this object */
+      trickleUpChange: function () {
+        MetacatUI.rootDataPackage.packageModel.set("changed", true);
+      },
+
+      validate: function () {
+        var errors = {};
+
+        if (!this.get("nonNumericDomain").length)
+          errors.nonNumericDomain = "Choose a possible value type.";
+        else {
+          var domain = this.get("nonNumericDomain")[0];
+
+          _.each(
+            Object.keys(domain),
+            function (key) {
+              //For enumerated domain types
+              if (key == "enumeratedDomain" && domain[key].codeDefinition) {
+                var isEmpty =
+                  domain[key].codeDefinition.length == 0 ? true : false;
+
+                //Validate the list of codes
+                for (var i = 0; i < domain[key].codeDefinition.length; i++) {
+                  var codeDef = domain[key].codeDefinition[i];
+
+                  //If either the code or definition is missing in at least one codeDefinition set,
+                  //then this model is invalid
+                  if (
+                    (codeDef.code && !codeDef.definition) ||
+                    (!codeDef.code && codeDef.definition)
+                  ) {
+                    errors.enumeratedDomain =
+                      "Provide both a code and definition in each row.";
+                    i = domain[key].codeDefinition.length;
+                  } else if (
+                    domain[key].codeDefinition.length == 1 &&
+                    !codeDef.code &&
+                    !codeDef.definition
+                  )
+                    isEmpty = true;
                 }
-            },
-
-            /* Let the top level package know of attribute changes from this object */
-            trickleUpChange: function(){
-                MetacatUI.rootDataPackage.packageModel.set("changed", true);
-            },
-
-            validate: function(){
-            	var errors = {};
-
-            	if( !this.get("nonNumericDomain").length )
-            		errors.nonNumericDomain = "Choose a possible value type.";
-            	else{
-            		var domain = this.get("nonNumericDomain")[0];
-
-            		_.each(Object.keys(domain), function(key){
-
-            			//For enumerated domain types
-            			if(key == "enumeratedDomain" && domain[key].codeDefinition){
-
-            				var isEmpty = (domain[key].codeDefinition.length == 0) ? true : false;
-
-            				//Validate the list of codes
-            				for(var i=0; i < domain[key].codeDefinition.length; i++){
-
-            					var codeDef = domain[key].codeDefinition[i];
-
-            					//If either the code or definition is missing in at least one codeDefinition set,
-            					//then this model is invalid
-            					if((codeDef.code && !codeDef.definition) || (!codeDef.code && codeDef.definition)){
-            						errors.enumeratedDomain = "Provide both a code and definition in each row.";
-            						i = domain[key].codeDefinition.length;
-            					}
-            					else if(domain[key].codeDefinition.length == 1 && !codeDef.code && !codeDef.definition)
-            						isEmpty = true;
-
-            				}
-
-            				if(isEmpty)
-            					errors.enumeratedDomain = "Define at least one code and definition.";
 
-            			}
-            			else if(key == "textDomain" && (typeof domain[key] != "object" || !domain[key].definition)){
-            				errors.definition = "Provide a description of the kind of text allowed.";
-            			}
-
-            		}, this);
-
-            	}
-
-            	if(Object.keys(errors).length)
-            		return errors;
-            	else{
-
-            		this.trigger("valid");
-
-            		return;
-            	}
-            },
-
-            /*
-            * Climbs up the model heirarchy until it finds the EML model
-            *
-            * @return {EML211 or false} - Returns the EML 211 Model or false if not found
-            */
-            getParentEML: function(){
-              var emlModel = this.get("parentModel"),
-                  tries = 0;
-
-              while (emlModel.type !== "EML" && tries < 6){
-                emlModel = emlModel.get("parentModel");
-                tries++;
+                if (isEmpty)
+                  errors.enumeratedDomain =
+                    "Define at least one code and definition.";
+              } else if (
+                key == "textDomain" &&
+                (typeof domain[key] != "object" || !domain[key].definition)
+              ) {
+                errors.definition =
+                  "Provide a description of the kind of text allowed.";
               }
-
-              if( emlModel && emlModel.type == "EML")
-                return emlModel;
-              else
-                return false;
-
             },
-
-            removeCode: function(index){
-            	var codeToRemove = this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition[index];
-
-            	var newCodeList = _.without(this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition, codeToRemove);
-
-            	this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition = newCodeList;
-
-            	this.trigger("change:nonNumericDomain");
-            }
-
-        });
-
-        return EMLNonNumericDomain;
-    }
-);
+            this,
+          );
+        }
+
+        if (Object.keys(errors).length) return errors;
+        else {
+          this.trigger("valid");
+
+          return;
+        }
+      },
+
+      /*
+       * Climbs up the model heirarchy until it finds the EML model
+       *
+       * @return {EML211 or false} - Returns the EML 211 Model or false if not found
+       */
+      getParentEML: function () {
+        var emlModel = this.get("parentModel"),
+          tries = 0;
+
+        while (emlModel.type !== "EML" && tries < 6) {
+          emlModel = emlModel.get("parentModel");
+          tries++;
+        }
+
+        if (emlModel && emlModel.type == "EML") return emlModel;
+        else return false;
+      },
+
+      removeCode: function (index) {
+        var codeToRemove =
+          this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition[
+            index
+          ];
+
+        var newCodeList = _.without(
+          this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition,
+          codeToRemove,
+        );
+
+        this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition =
+          newCodeList;
+
+        this.trigger("change:nonNumericDomain");
+      },
+    },
+  );
+
+  return EMLNonNumericDomain;
+});
 
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLNumericDomain.js.html b/docs/docs/src_js_models_metadata_eml211_EMLNumericDomain.js.html index 704767e7c..908c30125 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLNumericDomain.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLNumericDomain.js.html @@ -44,439 +44,437 @@

Source: src/js/models/metadata/eml211/EMLNumericDomain.js
-
define(["jquery", "underscore", "backbone",
-        "models/DataONEObject"],
-    function($, _, Backbone, DataONEObject) {
-
-        /**
-         * @class EMLNumericDomain
-         * @classdesc EMLNumericDomain represents the measurement scale of an interval
-         * or ratio measurement scale attribute, and is an extension of
-         * EMLMeasurementScale.
-         * @classcategory Models/Metadata/EML211
-         * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_measurementScale
-         * @extends Backbone.Model
-         */
-        var EMLNumericDomain = Backbone.Model.extend(
-            /** @lends EMLNumericDomain.prototype */{
-
-        	type: "EMLNumericDomain",
-
-            /* Attributes of an EMLNonNumericDomain object */
-            defaults: function(){
-              return {
-                /* Attributes from EML, extends attributes from EMLMeasurementScale */
-                measurementScale: null, // the required name of this measurement scale
-                unit: null, // the required standard or custom unit definition
-                precision: null, // the precision of the observed number
-                numericDomain: {} // a required numeric domain object or its reference
-              }
-            },
-
-            /**
-             * The map of lower case to camel case node names
-             * needed to deal with parsing issues with $.parseHTML().
-             * Use this until we can figure out issues with $.parseXML().
-             */
-            nodeNameMap: {
-                "standardunit": "standardUnit",
-                "customunit": "customUnit",
-                "numericdomain": "numericDomain",
-                "numbertype": "numberType"
-            },
-
-            /* Initialize an EMLNonNumericDomain object */
-            initialize: function(attributes, options) {
-
-                this.on("change:numericDomain", this.trickleUpChange);
-
-            },
-
-            /**
-             * Parse the incoming measurementScale's XML elements
-             */
-            parse: function(attributes, options) {
-
-                var $objectDOM;
-                var measurementScale;
-                var rootNodeName;
-
-                if ( attributes.objectDOM ) {
-                    rootNodeName = $(attributes.objectDOM)[0].localName;
-                    $objectDOM = $(attributes.objectDOM);
-                } else if ( attributes.objectXML ) {
-                    rootNodeName = $(attributes.objectXML)[0].localName;
-                    $objectDOM = $($(attributes.objectXML)[0]);
-                } else {
-                    return {};
-                }
-
-                // do we have an appropriate measurementScale tree?
-                var index = _.indexOf(["measurementscale","interval", "ratio"], rootNodeName);
-                if ( index == -1 ) {
-                    throw new Error("The measurement scale XML does not have a root " +
-                        "node of 'measurementScale', 'interval', or 'ratio'.");
-                }
-
-                // If measurementScale is present, add it
-                if ( rootNodeName == "measurementscale" ) {
-                    attributes.measurementScale = $objectDOM.children().first()[0].localName;
-                    $objectDOM = $objectDOM.children().first();
-                } else {
-                    attributes.measurementScale = $objectDOM.localName;
-                }
-
-
-                // Add the unit
-                var unitObject = {};
-                var unit = $objectDOM.children("unit");
-                var standardUnitNodes = unit.children("standardunit"),
-                    customUnitNodes   = unit.children("customunit"),
-                    standardUnit,
-                    customUnit;
-
-                if ( standardUnitNodes.length ) {
-                    standardUnit = standardUnitNodes.text();
-
-                    if( standardUnit )
-                      unitObject.standardUnit = standardUnit;
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+  $,
+  _,
+  Backbone,
+  DataONEObject,
+) {
+  /**
+   * @class EMLNumericDomain
+   * @classdesc EMLNumericDomain represents the measurement scale of an interval
+   * or ratio measurement scale attribute, and is an extension of
+   * EMLMeasurementScale.
+   * @classcategory Models/Metadata/EML211
+   * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_measurementScale
+   * @extends Backbone.Model
+   */
+  var EMLNumericDomain = Backbone.Model.extend(
+    /** @lends EMLNumericDomain.prototype */ {
+      type: "EMLNumericDomain",
+
+      /* Attributes of an EMLNonNumericDomain object */
+      defaults: function () {
+        return {
+          /* Attributes from EML, extends attributes from EMLMeasurementScale */
+          measurementScale: null, // the required name of this measurement scale
+          unit: null, // the required standard or custom unit definition
+          precision: null, // the precision of the observed number
+          numericDomain: {}, // a required numeric domain object or its reference
+        };
+      },
+
+      /**
+       * The map of lower case to camel case node names
+       * needed to deal with parsing issues with $.parseHTML().
+       * Use this until we can figure out issues with $.parseXML().
+       */
+      nodeNameMap: {
+        standardunit: "standardUnit",
+        customunit: "customUnit",
+        numericdomain: "numericDomain",
+        numbertype: "numberType",
+      },
+
+      /* Initialize an EMLNonNumericDomain object */
+      initialize: function (attributes, options) {
+        this.on("change:numericDomain", this.trickleUpChange);
+      },
+
+      /**
+       * Parse the incoming measurementScale's XML elements
+       */
+      parse: function (attributes, options) {
+        var $objectDOM;
+        var measurementScale;
+        var rootNodeName;
+
+        if (attributes.objectDOM) {
+          rootNodeName = $(attributes.objectDOM)[0].localName;
+          $objectDOM = $(attributes.objectDOM);
+        } else if (attributes.objectXML) {
+          rootNodeName = $(attributes.objectXML)[0].localName;
+          $objectDOM = $($(attributes.objectXML)[0]);
+        } else {
+          return {};
+        }
+
+        // do we have an appropriate measurementScale tree?
+        var index = _.indexOf(
+          ["measurementscale", "interval", "ratio"],
+          rootNodeName,
+        );
+        if (index == -1) {
+          throw new Error(
+            "The measurement scale XML does not have a root " +
+              "node of 'measurementScale', 'interval', or 'ratio'.",
+          );
+        }
+
+        // If measurementScale is present, add it
+        if (rootNodeName == "measurementscale") {
+          attributes.measurementScale = $objectDOM
+            .children()
+            .first()[0].localName;
+          $objectDOM = $objectDOM.children().first();
+        } else {
+          attributes.measurementScale = $objectDOM.localName;
+        }
+
+        // Add the unit
+        var unitObject = {};
+        var unit = $objectDOM.children("unit");
+        var standardUnitNodes = unit.children("standardunit"),
+          customUnitNodes = unit.children("customunit"),
+          standardUnit,
+          customUnit;
+
+        if (standardUnitNodes.length) {
+          standardUnit = standardUnitNodes.text();
+
+          if (standardUnit) unitObject.standardUnit = standardUnit;
+        } else if (customUnitNodes.length) {
+          customUnit = customUnitNodes.text();
+
+          if (customUnit) unitObject.customUnit = customUnit;
+        }
+
+        attributes.unit = unitObject;
+
+        // Add the precision
+        var precision = $objectDOM.children("precision").text();
+        if (precision) {
+          attributes.precision = precision;
+        }
+
+        // Add the numericDomain
+        var numericDomainObject = {};
+        var numericDomain = $objectDOM.children("numericdomain");
+        var numberType;
+        var boundsArray = [];
+        var boundsObject;
+        var bounds;
+        var minimum;
+        var maximum;
+        var references;
+        if (numericDomain) {
+          // Add the XML id of the numeric domain
+          if ($(numericDomain).attr("id")) {
+            numericDomainObject.xmlID = $(numericDomain).attr("id");
+          }
+
+          // Add the numberType
+          numberType = $(numericDomain).children("numbertype");
+
+          if (numberType) {
+            numericDomainObject.numberType = numberType.text();
+
+            // Add optional bounds
+            bounds = $(numericDomain).children("bounds");
+            if (bounds.length) {
+              _.each(bounds, function (bound) {
+                boundsObject = {}; // initialize on each
+                minimum = $(bound).children("minimum").text();
+                maximum = $(bound).children("maximum").text();
+                if (minimum && maximum) {
+                  boundsObject.minimum = minimum;
+                  boundsObject.maximum = maximum;
+                } else if (minimum) {
+                  boundsObject.minimum = minimum;
+                } else if (maximum) {
+                  boundsObject.maximum = maximum;
                 }
-                else if( customUnitNodes.length ){
-                    customUnit = customUnitNodes.text();
-
-                    if( customUnit )
-                      unitObject.customUnit = customUnit;
-                }
-
-                attributes.unit = unitObject;
-
-                // Add the precision
-                var precision = $objectDOM.children("precision").text();
-                if ( precision ) {
-                    attributes.precision = precision;
+                // If one of min or max is defined, add to the bounds array
+                if (boundsObject.minimum || boundsObject.maximum) {
+                  boundsArray.push(boundsObject);
                 }
-
-                // Add the numericDomain
-                var numericDomainObject = {};
-                var numericDomain = $objectDOM.children("numericdomain");
-                var numberType;
-                var boundsArray = [];
-                var boundsObject;
-                var bounds;
-                var minimum;
-                var maximum;
-                var references;
-                if ( numericDomain ) {
-                    // Add the XML id of the numeric domain
-                    if ( $(numericDomain).attr("id") ) {
-                        numericDomainObject.xmlID = $(numericDomain).attr("id");
-                    }
-
-                    // Add the numberType
-                    numberType = $(numericDomain).children("numbertype");
-
-                    if ( numberType ) {
-                        numericDomainObject.numberType = numberType.text();
-
-                        // Add optional bounds
-                        bounds = $(numericDomain).children("bounds");
-                        if ( bounds.length ) {
-                            _.each(bounds, function(bound) {
-                                boundsObject = {}; // initialize on each
-                                minimum = $(bound).children("minimum").text();
-                                maximum = $(bound).children("maximum").text();
-                                if ( minimum && maximum ) {
-                                    boundsObject.minimum = minimum;
-                                    boundsObject.maximum = maximum;
-                                } else if ( minimum ) {
-                                    boundsObject.minimum = minimum;
-                                } else if ( maximum ) {
-                                    boundsObject.maximum = maximum;
-                                }
-                                // If one of min or max is defined, add to the bounds array
-                                if ( boundsObject.minimum || boundsObject.maximum ) {
-                                    boundsArray.push(boundsObject);
-                                }
-                            });
-                        }
-                        numericDomainObject.bounds = boundsArray;
-
-                    } else {
-                        // Otherwise look for references
-                        references = $(numericDomain).children("references");
-                        if ( references ) {
-                            numericDomainObject.references = references.text();
-                        }
-                    }
-                    attributes.numericDomain = numericDomainObject;
-                }
-                attributes.objectDOM = $objectDOM[0];
-
-                return attributes;
-            },
-
-            /* Serialize the model to XML */
-            serialize: function() {
-                var objectDOM = this.updateDOM();
-                var xmlString = objectDOM.outerHTML;
-
-                // Camel-case the XML
-                xmlString = this.formatXML(xmlString);
-
-                return xmlString;
-            },
-
-            /* Copy the original XML DOM and update it with new values from the model */
-            updateDOM: function(objectDOM) {
-                var nodeToInsertAfter;
-                var type = this.get("measurementScale");
-                if ( typeof type === "undefined") {
-                    console.warn("Defaulting to an interval measurementScale.");
-                    type = "interval";
-                }
-                if ( ! objectDOM ) {
-                    objectDOM = this.get("objectDOM");
-                }
-                var objectXML = this.get("objectXML");
-
-                // If present, use the cached DOM
-                if ( objectDOM ) {
-                    objectDOM = objectDOM.cloneNode(true);
-
-                // otherwise, use the cached XML
-                } else if ( objectXML ){
-                    objectDOM = $(objectXML)[0].cloneNode(true);
-
-                // This is new, create it
-                } else {
-                    objectDOM = document.createElement(type);
-
-                }
-
-                // Update the unit
-                var unit = this.get("unit");
-                var unitNode;
-                var unitTypeNode;
-                if ( unit ) {
-                    // Remove any existing unit
-                    $(objectDOM).find("unit").remove();
-
-                    // Build a unit element, and populate a standard or custom child
-                    unitNode = document.createElement("unit");
-
-                    if ( typeof unit.standardUnit !== "undefined") {
-                        unitTypeNode = document.createElement("standardUnit");
-                        $(unitTypeNode).text(unit.standardUnit);
-                    } else if ( typeof unit.customUnit !== "undefined" ) {
-                        unitTypeNode = document.createElement("customUnit");
-                        $(unitTypeNode).text(unit.customUnit);
-                    } else {
-                        // Hmm, unit isn't an object?
-                        // Default to a standard unit
-                        unitTypeNode = document.createElement("standardUnit");
-                        if ( typeof unit === "string") {
-                            $(unitTypeNode).text(unit);
-                            console.warn("EMLNumericDomain.unit should be an object.");
-                        } else {
-                            // We're really striking out. Default to dimensionless.
-                            $(unitTypeNode).text("dimensionless");
-                            console.warn("Defaulting EMLNumericDomain.unit to dimensionless.");
-                        }
-                    }
-                    $(unitNode).append(unitTypeNode);
-
-                    // Add the unit to the DOM
-                    nodeToInsertAfter = this.getEMLPosition(objectDOM, "unit");
-
-                    if( ! nodeToInsertAfter ) {
-                        $(objectDOM).prepend(unitNode);
-                    } else {
-                        $(nodeToInsertAfter).after(unitNode);
-                    }
-                }
-
-                // Update the precision
-                if ( this.get("precision") ) {
-                    if ( $(objectDOM).find("precision").length ) {
-                        $(objectDOM).find("precision").text(this.get("precision"));
-                    } else {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "precision");
-
-                        if( ! nodeToInsertAfter ) {
-                            $(objectDOM).append($(document.createElement("precision"))
-                                .text(this.get("precision"))[0]);
-                        } else {
-                            $(nodeToInsertAfter).after(
-                                $(document.createElement("precision"))
-                                    .text(this.get("precision"))[0]
-                            );
-                        }
-                    }
-                }
-
-                // Update the numericDomain
-                var numericDomain = this.get("numericDomain");
-                var numericDomainNode = $(objectDOM).find("numericdomain")[0];
-                var numberType;
-                var numberTypeNode;
-                var minBound;
-                var maxBound;
-                var boundsNode;
-                var minBoundNode;
-                var maxBoundNode;
-                if ( numericDomain ) {
-
-                	var oldNumericDomainNode = $(numericDomainNode).clone();
-
-                    // Remove the existing numericDomainNode node
-                    if ( typeof numericDomainNode !== "undefined" ) {
-                        numericDomainNode.remove();
-                    }
-
-                    // Build the new numericDomain node
-                    numericDomainNode = document.createElement("numericdomain");
-
-                    // Do we have numberType?
-                    if ( typeof numericDomain.numberType !== "undefined" ) {
-                        numberTypeNode = document.createElement("numbertype");
-                        $(numberTypeNode).text(numericDomain.numberType);
-                        $(numericDomainNode).append(numberTypeNode);
-                    }
-
-                    // Do we have bounds?
-                    if ( typeof numericDomain.bounds !== "undefined" &&
-                         numericDomain.bounds.length ) {
-
-                        _.each(numericDomain.bounds, function(bound) {
-                            minBound = bound.minimum;
-                            maxBound = bound.maximum;
-                            boundsNode = document.createElement("bounds");
-
-                            var hasBounds = typeof minBound !== "undefined" || typeof maxBound !== "undefined";
-
-                            if ( hasBounds ) {
-                                // Populate the minimum element
-                                if ( typeof minBound !== "undefined" ) {
-                                    minBoundNode = $(document.createElement("minimum"));
-                                    minBoundNode.text(minBound);
-
-                                    var existingExclusive = oldNumericDomainNode.find("minimum").attr("exclusive");
-
-                                    if( !existingExclusive || existingExclusive === "false" )
-                                    	minBoundNode.attr("exclusive", "false");
-                                    else
-                                    	minBoundNode.attr("exclusive", "true");
-                                }
-
-                                // Populate the maximum element
-                                if ( typeof maxBound !== "undefined" ) {
-                                    maxBoundNode = $(document.createElement("maximum"));
-                                    maxBoundNode.text(maxBound);
-
-                                    var existingExclusive = oldNumericDomainNode.find("maximum").attr("exclusive");
-
-                                    if( !existingExclusive || existingExclusive === "false" )
-                                    	maxBoundNode.attr("exclusive", "false");
-                                    else
-                                    	maxBoundNode.attr("exclusive", "true");
-                                }
-
-                                $(boundsNode).append(minBoundNode, maxBoundNode);
-                                $(numericDomainNode).append(boundsNode);
-
-                            } else {
-                                // Do nothing. Content is missing, don't append the node
-                            }
-                        });
-                    } else {
-                        // Basically do nothing. Don't append the numericDomain element
-                        // TODO: handle numericDomain.references
-
-                    }
-                    nodeToInsertAfter = this.getEMLPosition(objectDOM, "numericDomain");
-
-                    if( ! nodeToInsertAfter ) {
-                        $(objectDOM).append(numericDomainNode);
-                    } else {
-                        $(nodeToInsertAfter).after(numericDomainNode);
-                    }
+              });
+            }
+            numericDomainObject.bounds = boundsArray;
+          } else {
+            // Otherwise look for references
+            references = $(numericDomain).children("references");
+            if (references) {
+              numericDomainObject.references = references.text();
+            }
+          }
+          attributes.numericDomain = numericDomainObject;
+        }
+        attributes.objectDOM = $objectDOM[0];
+
+        return attributes;
+      },
+
+      /* Serialize the model to XML */
+      serialize: function () {
+        var objectDOM = this.updateDOM();
+        var xmlString = objectDOM.outerHTML;
+
+        // Camel-case the XML
+        xmlString = this.formatXML(xmlString);
+
+        return xmlString;
+      },
+
+      /* Copy the original XML DOM and update it with new values from the model */
+      updateDOM: function (objectDOM) {
+        var nodeToInsertAfter;
+        var type = this.get("measurementScale");
+        if (typeof type === "undefined") {
+          console.warn("Defaulting to an interval measurementScale.");
+          type = "interval";
+        }
+        if (!objectDOM) {
+          objectDOM = this.get("objectDOM");
+        }
+        var objectXML = this.get("objectXML");
+
+        // If present, use the cached DOM
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+
+          // otherwise, use the cached XML
+        } else if (objectXML) {
+          objectDOM = $(objectXML)[0].cloneNode(true);
+
+          // This is new, create it
+        } else {
+          objectDOM = document.createElement(type);
+        }
+
+        // Update the unit
+        var unit = this.get("unit");
+        var unitNode;
+        var unitTypeNode;
+        if (unit) {
+          // Remove any existing unit
+          $(objectDOM).find("unit").remove();
+
+          // Build a unit element, and populate a standard or custom child
+          unitNode = document.createElement("unit");
+
+          if (typeof unit.standardUnit !== "undefined") {
+            unitTypeNode = document.createElement("standardUnit");
+            $(unitTypeNode).text(unit.standardUnit);
+          } else if (typeof unit.customUnit !== "undefined") {
+            unitTypeNode = document.createElement("customUnit");
+            $(unitTypeNode).text(unit.customUnit);
+          } else {
+            // Hmm, unit isn't an object?
+            // Default to a standard unit
+            unitTypeNode = document.createElement("standardUnit");
+            if (typeof unit === "string") {
+              $(unitTypeNode).text(unit);
+              console.warn("EMLNumericDomain.unit should be an object.");
+            } else {
+              // We're really striking out. Default to dimensionless.
+              $(unitTypeNode).text("dimensionless");
+              console.warn(
+                "Defaulting EMLNumericDomain.unit to dimensionless.",
+              );
+            }
+          }
+          $(unitNode).append(unitTypeNode);
+
+          // Add the unit to the DOM
+          nodeToInsertAfter = this.getEMLPosition(objectDOM, "unit");
+
+          if (!nodeToInsertAfter) {
+            $(objectDOM).prepend(unitNode);
+          } else {
+            $(nodeToInsertAfter).after(unitNode);
+          }
+        }
+
+        // Update the precision
+        if (this.get("precision")) {
+          if ($(objectDOM).find("precision").length) {
+            $(objectDOM).find("precision").text(this.get("precision"));
+          } else {
+            nodeToInsertAfter = this.getEMLPosition(objectDOM, "precision");
+
+            if (!nodeToInsertAfter) {
+              $(objectDOM).append(
+                $(document.createElement("precision")).text(
+                  this.get("precision"),
+                )[0],
+              );
+            } else {
+              $(nodeToInsertAfter).after(
+                $(document.createElement("precision")).text(
+                  this.get("precision"),
+                )[0],
+              );
+            }
+          }
+        }
+
+        // Update the numericDomain
+        var numericDomain = this.get("numericDomain");
+        var numericDomainNode = $(objectDOM).find("numericdomain")[0];
+        var numberType;
+        var numberTypeNode;
+        var minBound;
+        var maxBound;
+        var boundsNode;
+        var minBoundNode;
+        var maxBoundNode;
+        if (numericDomain) {
+          var oldNumericDomainNode = $(numericDomainNode).clone();
+
+          // Remove the existing numericDomainNode node
+          if (typeof numericDomainNode !== "undefined") {
+            numericDomainNode.remove();
+          }
+
+          // Build the new numericDomain node
+          numericDomainNode = document.createElement("numericdomain");
+
+          // Do we have numberType?
+          if (typeof numericDomain.numberType !== "undefined") {
+            numberTypeNode = document.createElement("numbertype");
+            $(numberTypeNode).text(numericDomain.numberType);
+            $(numericDomainNode).append(numberTypeNode);
+          }
+
+          // Do we have bounds?
+          if (
+            typeof numericDomain.bounds !== "undefined" &&
+            numericDomain.bounds.length
+          ) {
+            _.each(numericDomain.bounds, function (bound) {
+              minBound = bound.minimum;
+              maxBound = bound.maximum;
+              boundsNode = document.createElement("bounds");
+
+              var hasBounds =
+                typeof minBound !== "undefined" ||
+                typeof maxBound !== "undefined";
+
+              if (hasBounds) {
+                // Populate the minimum element
+                if (typeof minBound !== "undefined") {
+                  minBoundNode = $(document.createElement("minimum"));
+                  minBoundNode.text(minBound);
+
+                  var existingExclusive = oldNumericDomainNode
+                    .find("minimum")
+                    .attr("exclusive");
+
+                  if (!existingExclusive || existingExclusive === "false")
+                    minBoundNode.attr("exclusive", "false");
+                  else minBoundNode.attr("exclusive", "true");
                 }
-                return objectDOM;
-            },
 
-            formatXML: function(xmlString){
-                return DataONEObject.prototype.formatXML.call(this, xmlString);
-            },
+                // Populate the maximum element
+                if (typeof maxBound !== "undefined") {
+                  maxBoundNode = $(document.createElement("maximum"));
+                  maxBoundNode.text(maxBound);
 
-            /**/
-            getEMLPosition: function(objectDOM, nodeName) {
-                // TODO: set the node order
-                var nodeOrder = ["unit", "precision", "numericDomain"];
-
-                var position = _.indexOf(nodeOrder, nodeName);
-
-                // Append to the bottom if not found
-                if ( position == -1 ) {
-                    return $(objectDOM).children().last()[0];
-                }
+                  var existingExclusive = oldNumericDomainNode
+                    .find("maximum")
+                    .attr("exclusive");
 
-                // Otherwise, go through each node in the node list and find the
-                // position where this node will be inserted after
-                for ( var i = position - 1; i >= 0; i-- ) {
-                    if ( $(objectDOM).find(nodeOrder[i]).length ) {
-                        return $(objectDOM).find(nodeOrder[i]).last()[0];
-                    }
+                  if (!existingExclusive || existingExclusive === "false")
+                    maxBoundNode.attr("exclusive", "false");
+                  else maxBoundNode.attr("exclusive", "true");
                 }
-            },
 
-            validate: function(){
-            	var errors = {};
-
-            	if(!this.get("unit"))
-            		errors.unit = "Choose a unit.";
-
-            	if( Object.keys(errors).length )
-            		return errors;
-            	else{
-
-            		this.trigger("valid");
-            		return;
-
-            	}
-
-            },
-
-            /*
-            * Climbs up the model heirarchy until it finds the EML model
-            *
-            * @return {EML211 or false} - Returns the EML 211 Model or false if not found
-            */
-            getParentEML: function(){
-              var emlModel = this.get("parentModel"),
-                  tries = 0;
-
-              while (emlModel.type !== "EML" && tries < 6){
-                emlModel = emlModel.get("parentModel");
-                tries++;
+                $(boundsNode).append(minBoundNode, maxBoundNode);
+                $(numericDomainNode).append(boundsNode);
+              } else {
+                // Do nothing. Content is missing, don't append the node
               }
-
-              if( emlModel && emlModel.type == "EML")
-                return emlModel;
-              else
-                return false;
-
-            },
-
-            /* Let the top level package know of attribute changes from this object */
-            trickleUpChange: function(){
-                MetacatUI.rootDataPackage.packageModel.set("changed", true);
-            }
-
-        });
-
-        return EMLNumericDomain;
-    }
-);
+            });
+          } else {
+            // Basically do nothing. Don't append the numericDomain element
+            // TODO: handle numericDomain.references
+          }
+          nodeToInsertAfter = this.getEMLPosition(objectDOM, "numericDomain");
+
+          if (!nodeToInsertAfter) {
+            $(objectDOM).append(numericDomainNode);
+          } else {
+            $(nodeToInsertAfter).after(numericDomainNode);
+          }
+        }
+        return objectDOM;
+      },
+
+      formatXML: function (xmlString) {
+        return DataONEObject.prototype.formatXML.call(this, xmlString);
+      },
+
+      /**/
+      getEMLPosition: function (objectDOM, nodeName) {
+        // TODO: set the node order
+        var nodeOrder = ["unit", "precision", "numericDomain"];
+
+        var position = _.indexOf(nodeOrder, nodeName);
+
+        // Append to the bottom if not found
+        if (position == -1) {
+          return $(objectDOM).children().last()[0];
+        }
+
+        // Otherwise, go through each node in the node list and find the
+        // position where this node will be inserted after
+        for (var i = position - 1; i >= 0; i--) {
+          if ($(objectDOM).find(nodeOrder[i]).length) {
+            return $(objectDOM).find(nodeOrder[i]).last()[0];
+          }
+        }
+      },
+
+      validate: function () {
+        var errors = {};
+
+        if (!this.get("unit")) errors.unit = "Choose a unit.";
+
+        if (Object.keys(errors).length) return errors;
+        else {
+          this.trigger("valid");
+          return;
+        }
+      },
+
+      /*
+       * Climbs up the model heirarchy until it finds the EML model
+       *
+       * @return {EML211 or false} - Returns the EML 211 Model or false if not found
+       */
+      getParentEML: function () {
+        var emlModel = this.get("parentModel"),
+          tries = 0;
+
+        while (emlModel.type !== "EML" && tries < 6) {
+          emlModel = emlModel.get("parentModel");
+          tries++;
+        }
+
+        if (emlModel && emlModel.type == "EML") return emlModel;
+        else return false;
+      },
+
+      /* Let the top level package know of attribute changes from this object */
+      trickleUpChange: function () {
+        MetacatUI.rootDataPackage.packageModel.set("changed", true);
+      },
+    },
+  );
+
+  return EMLNumericDomain;
+});
 
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLOtherEntity.js.html b/docs/docs/src_js_models_metadata_eml211_EMLOtherEntity.js.html index c4572466d..7c7f42a23 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLOtherEntity.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLOtherEntity.js.html @@ -44,193 +44,199 @@

Source: src/js/models/metadata/eml211/EMLOtherEntity.js
-
define(["jquery", "underscore", "backbone", "models/metadata/eml211/EMLEntity"],
-    function($, _, Backbone, EMLEntity) {
-
-        /**
-        * @class EMLOtherEntity
-         * @classdesc EMLOtherEntity represents a generic data entity, corresponding
-         * with the EML otherEntity module.
-         * @classcategory Models/Metadata/EML211
-         * @see https://eml.ecoinformatics.org/schema/eml-dataset_xsd.html#DatasetType_otherEntity
-         * @extends EMLEntity
-         */
-        var EMLOtherEntity = EMLEntity.extend(
-          /** @lends EMLOtherEntity.prototype */{
-
-        	//The class name for this model
-        	type: "EMLOtherEntity",
-
-            /* Attributes of any entity */
-            defaults: function(){
-	            return	_.extend({
-
-		                /* Attributes from EML */
-                    entityType: "data entity",
-
-		                /* Attributes not from EML */
-		                nodeOrder: [ // The order of the top level XML element nodes
-		                    "alternateIdentifier",
-		                    "entityName",
-		                    "entityDescription",
-		                    "physical",
-		                    "coverage",
-		                    "methods",
-		                    "additionalInfo",
-		                    "attributeList",
-		                    "constraint",
-		                    "entityType"
-		                ],
-
-	            	}, EMLEntity.prototype.defaults());
-            },
-
-            /*
-             * The map of lower case to camel case node names
-             * needed to deal with parsing issues with $.parseHTML().
-             * Use this until we can figure out issues with $.parseXML().
-             */
-            nodeNameMap: _.extend({
-                "entitytype": "entityType"
-
-            }, EMLEntity.prototype.nodeNameMap),
-
-            /* Initialize an EMLOtherEntity object */
-            initialize: function(attributes) {
-
-                // if options.parse = true, Backbone will call parse()
-
-                // Call super() first
-                this.constructor.__super__.initialize.apply(this, [attributes]);
-
-                // EMLOtherEntity-specific work
-                this.set("type", "otherEntity", {silent: true});
-
-                // Register change events
-                this.on( "change:entityType", EMLEntity.trickleUpChange);
-
-            },
-
-            /*
-             * Parse the incoming other entity's XML elements
-             */
-            parse: function(attributes, options) {
-
-                var attributes = attributes || {};
-
-                // Call super() first
-                attributes = this.constructor.__super__.parse.apply(this, [attributes, options]);
-
-                // EMLOtherEntity-specific work
-                var objectXML  = attributes.objectXML; // The otherEntity XML fragment
-                var objectDOM; // The W3C DOM of the object XML fragment
-                var $objectDOM; // The JQuery object of the XML fragment
-
-                // Use the updated objectDOM if we have it
-                if ( attributes.objectDOM ) {
-                    $objectDOM = $(attributes.objectDOM);
-                } else {
-                    // Hmm, oddly not there, start from scratch =/
-                    $objectDOM = $(objectXML);
-                }
-
-                // Add the entityType
-                attributes.entityType = $objectDOM.children("entitytype").text();
-
-                return attributes;
-            },
-
-            /* Copy the original XML and update fields in a DOM object */
-            updateDOM: function(objectDOM) {
-                var nodeToInsertAfter;
-                var type = this.get("type") || "otherEntity";
-                if ( ! objectDOM ) {
-                    objectDOM = this.get("objectDOM");
-                }
-                var objectXML = this.get("objectXML");
-
-                // If present, use the cached DOM
-                if ( objectDOM ) {
-                    objectDOM = objectDOM.cloneNode(true);
-
-                // otherwise, use the cached XML
-                } else if ( objectXML ){
-                    objectDOM = $(objectXML)[0].cloneNode(true);
-
-                // This is new, create it
-                } else {
-                    objectDOM = document.createElement(type);
-
-                }
-
-                // Now call the superclass
-                objectDOM = this.constructor.__super__.updateDOM.apply(this, [objectDOM]);
-
-                // And then update the EMLOtherEntity-specific fields
-                // Update the entityName
-                if ( this.get("entityType") ) {
-                    if ( $(objectDOM).find("entityType").length ) {
-                        $(objectDOM).find("entityType").text(this.get("entityType"));
-
-                    } else {
-                        nodeToInsertAfter = this.getEMLPosition(objectDOM, "entityType");
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/metadata/eml211/EMLEntity",
+], function ($, _, Backbone, EMLEntity) {
+  /**
+   * @class EMLOtherEntity
+   * @classdesc EMLOtherEntity represents a generic data entity, corresponding
+   * with the EML otherEntity module.
+   * @classcategory Models/Metadata/EML211
+   * @see https://eml.ecoinformatics.org/schema/eml-dataset_xsd.html#DatasetType_otherEntity
+   * @extends EMLEntity
+   */
+  var EMLOtherEntity = EMLEntity.extend(
+    /** @lends EMLOtherEntity.prototype */ {
+      //The class name for this model
+      type: "EMLOtherEntity",
+
+      /* Attributes of any entity */
+      defaults: function () {
+        return _.extend(
+          {
+            /* Attributes from EML */
+            entityType: "data entity",
+
+            /* Attributes not from EML */
+            nodeOrder: [
+              // The order of the top level XML element nodes
+              "alternateIdentifier",
+              "entityName",
+              "entityDescription",
+              "physical",
+              "coverage",
+              "methods",
+              "additionalInfo",
+              "attributeList",
+              "constraint",
+              "entityType",
+            ],
+          },
+          EMLEntity.prototype.defaults(),
+        );
+      },
+
+      /*
+       * The map of lower case to camel case node names
+       * needed to deal with parsing issues with $.parseHTML().
+       * Use this until we can figure out issues with $.parseXML().
+       */
+      nodeNameMap: _.extend(
+        {
+          entitytype: "entityType",
+        },
+        EMLEntity.prototype.nodeNameMap,
+      ),
+
+      /* Initialize an EMLOtherEntity object */
+      initialize: function (attributes) {
+        // if options.parse = true, Backbone will call parse()
+
+        // Call super() first
+        this.constructor.__super__.initialize.apply(this, [attributes]);
+
+        // EMLOtherEntity-specific work
+        this.set("type", "otherEntity", { silent: true });
+
+        // Register change events
+        this.on("change:entityType", EMLEntity.trickleUpChange);
+      },
+
+      /*
+       * Parse the incoming other entity's XML elements
+       */
+      parse: function (attributes, options) {
+        var attributes = attributes || {};
+
+        // Call super() first
+        attributes = this.constructor.__super__.parse.apply(this, [
+          attributes,
+          options,
+        ]);
+
+        // EMLOtherEntity-specific work
+        var objectXML = attributes.objectXML; // The otherEntity XML fragment
+        var objectDOM; // The W3C DOM of the object XML fragment
+        var $objectDOM; // The JQuery object of the XML fragment
+
+        // Use the updated objectDOM if we have it
+        if (attributes.objectDOM) {
+          $objectDOM = $(attributes.objectDOM);
+        } else {
+          // Hmm, oddly not there, start from scratch =/
+          $objectDOM = $(objectXML);
+        }
+
+        // Add the entityType
+        attributes.entityType = $objectDOM.children("entitytype").text();
+
+        return attributes;
+      },
+
+      /* Copy the original XML and update fields in a DOM object */
+      updateDOM: function (objectDOM) {
+        var nodeToInsertAfter;
+        var type = this.get("type") || "otherEntity";
+        if (!objectDOM) {
+          objectDOM = this.get("objectDOM");
+        }
+        var objectXML = this.get("objectXML");
+
+        // If present, use the cached DOM
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+
+          // otherwise, use the cached XML
+        } else if (objectXML) {
+          objectDOM = $(objectXML)[0].cloneNode(true);
+
+          // This is new, create it
+        } else {
+          objectDOM = document.createElement(type);
+        }
+
+        // Now call the superclass
+        objectDOM = this.constructor.__super__.updateDOM.apply(this, [
+          objectDOM,
+        ]);
+
+        // And then update the EMLOtherEntity-specific fields
+        // Update the entityName
+        if (this.get("entityType")) {
+          if ($(objectDOM).find("entityType").length) {
+            $(objectDOM).find("entityType").text(this.get("entityType"));
+          } else {
+            nodeToInsertAfter = this.getEMLPosition(objectDOM, "entityType");
+
+            if (!nodeToInsertAfter) {
+              $(objectDOM).append(
+                $(document.createElement("entitytype")).text(
+                  this.get("entityType"),
+                )[0],
+              );
+            } else {
+              $(nodeToInsertAfter).after(
+                $(document.createElement("entitytype")).text(
+                  this.get("entityType"),
+                )[0],
+              );
+            }
+          }
+        }
 
-                        if ( ! nodeToInsertAfter ) {
-                            $(objectDOM).append($(document.createElement("entitytype"))
-                                .text(this.get("entityType"))[0]);
-                        } else {
-                            $(nodeToInsertAfter).after($(document.createElement("entitytype"))
-                                .text(this.get("entityType"))[0]);
-                        }
-                    }
-                }
+        return objectDOM;
+      },
 
-                return objectDOM;
-            },
+      /*
+       * Climbs up the model heirarchy until it finds the EML model
+       *
+       * @return {EML211 or false} - Returns the EML 211 Model or false if not found
+       */
+      getParentEML: function () {
+        var emlModel = this.get("parentModel"),
+          tries = 0;
 
-            /*
-            * Climbs up the model heirarchy until it finds the EML model
-            *
-            * @return {EML211 or false} - Returns the EML 211 Model or false if not found
-            */
-            getParentEML: function(){
-              var emlModel = this.get("parentModel"),
-                  tries = 0;
+        while (emlModel.type !== "EML" && tries < 6) {
+          emlModel = emlModel.get("parentModel");
+          tries++;
+        }
 
-              while (emlModel.type !== "EML" && tries < 6){
-                emlModel = emlModel.get("parentModel");
-                tries++;
-              }
+        if (emlModel && emlModel.type == "EML") return emlModel;
+        else return false;
+      },
 
-              if( emlModel && emlModel.type == "EML")
-                return emlModel;
-              else
-                return false;
+      /* Serialize the EML DOM to XML */
+      serialize: function () {
+        var xmlString = "";
 
-            },
+        // Update the superclass fields in the objectDOM first
+        var objectDOM = this.constructor.__super__.updateDOM.apply(this, []);
 
-            /* Serialize the EML DOM to XML */
-            serialize: function() {
-
-                var xmlString = "";
+        // Then update the subclass fields in the objectDOM
+        // TODO
 
-                // Update the superclass fields in the objectDOM first
-                var objectDOM = this.constructor.__super__.updateDOM.apply(this, []);
-
-                // Then update the subclass fields in the objectDOM
-                // TODO
-
-
-                this.set("objectXML", xmlString);
-
-                return xmlString;
-            }
+        this.set("objectXML", xmlString);
 
-        });
+        return xmlString;
+      },
+    },
+  );
 
-        return EMLOtherEntity;
-    }
-);
+  return EMLOtherEntity;
+});
 

diff --git a/docs/docs/src_js_models_metadata_eml211_EMLParty.js.html b/docs/docs/src_js_models_metadata_eml211_EMLParty.js.html index 86e750da4..78cd99cca 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLParty.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLParty.js.html @@ -44,645 +44,692 @@

Source: src/js/models/metadata/eml211/EMLParty.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'],
-    function($, _, Backbone, DataONEObject) {
-
-      /**
-      * @class EMLParty
-      * @classcategory Models/Metadata/EML211
-      * @classdesc EMLParty represents a single Party from the EML 2.1.1 and 2.2.0
-      * metadata schema. This can be a person or organization.
-      * @see https://eml.ecoinformatics.org/schema/eml-party_xsd.html#ResponsibleParty
-      * @extends Backbone.Model
-      * @constructor
-      */
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+  $,
+  _,
+  Backbone,
+  DataONEObject,
+) {
+  /**
+   * @class EMLParty
+   * @classcategory Models/Metadata/EML211
+   * @classdesc EMLParty represents a single Party from the EML 2.1.1 and 2.2.0
+   * metadata schema. This can be a person or organization.
+   * @see https://eml.ecoinformatics.org/schema/eml-party_xsd.html#ResponsibleParty
+   * @extends Backbone.Model
+   * @constructor
+   */
   var EMLParty = Backbone.Model.extend(
-    /** @lends EMLParty.prototype */{
-
-    defaults: function(){
-      return {
-        objectXML: null,
-        objectDOM: null,
-        individualName: null,
-        organizationName: null,
-        positionName: null,
-        address: [],
-        phone: [],
-        fax: [],
-        email: [],
-        onlineUrl: [],
-        roles: [],
-        references: null,
-        userId: [],
-        xmlID: null,
-        type: null,
-        typeOptions: ["associatedParty", "contact", "creator", "metadataProvider", "publisher"],
-        roleOptions: ["custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator",
-                      "coPrincipalInvestigator", "user"],
-        parentModel: null,
-        removed: false //Indicates whether this model has been removed from the parent model
-      }
-    },
+    /** @lends EMLParty.prototype */ {
+      defaults: function () {
+        return {
+          objectXML: null,
+          objectDOM: null,
+          individualName: null,
+          organizationName: null,
+          positionName: null,
+          address: [],
+          phone: [],
+          fax: [],
+          email: [],
+          onlineUrl: [],
+          roles: [],
+          references: null,
+          userId: [],
+          xmlID: null,
+          type: null,
+          typeOptions: [
+            "associatedParty",
+            "contact",
+            "creator",
+            "metadataProvider",
+            "publisher",
+          ],
+          roleOptions: [
+            "custodianSteward",
+            "principalInvestigator",
+            "collaboratingPrincipalInvestigator",
+            "coPrincipalInvestigator",
+            "user",
+          ],
+          parentModel: null,
+          removed: false, //Indicates whether this model has been removed from the parent model
+        };
+      },
 
-    initialize: function(options){
-      if(options && options.objectDOM)
-        this.set(this.parse(options.objectDOM));
+      initialize: function (options) {
+        if (options && options.objectDOM)
+          this.set(this.parse(options.objectDOM));
 
-      if(!this.get("xmlID"))
-        this.createID();
-      this.on("change:roles", this.setType);
-    },
+        if (!this.get("xmlID")) this.createID();
+        this.on("change:roles", this.setType);
+      },
 
-    /*
-         * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
-         * Used during parse() and serialize()
-         */
-    nodeNameMap: function(){
-      return {
-        "administrativearea"    : "administrativeArea",
-        "associatedparty"       : "associatedParty",
-        "deliverypoint"         : "deliveryPoint",
-        "electronicmailaddress" : "electronicMailAddress",
-        "givenname"             : "givenName",
-        "individualname"        : "individualName",
-        "metadataprovider"      : "metadataProvider",
-        "onlineurl"             : "onlineUrl",
-        "organizationname"      : "organizationName",
-        "positionname"          : "positionName",
-        "postalcode"            : "postalCode",
-        "surname"               : "surName",
-        "userid"                : "userId"
-      }
-    },
+      /*
+       * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
+       * Used during parse() and serialize()
+       */
+      nodeNameMap: function () {
+        return {
+          administrativearea: "administrativeArea",
+          associatedparty: "associatedParty",
+          deliverypoint: "deliveryPoint",
+          electronicmailaddress: "electronicMailAddress",
+          givenname: "givenName",
+          individualname: "individualName",
+          metadataprovider: "metadataProvider",
+          onlineurl: "onlineUrl",
+          organizationname: "organizationName",
+          positionname: "positionName",
+          postalcode: "postalCode",
+          surname: "surName",
+          userid: "userId",
+        };
+      },
 
-        /*
+      /*
             Parse the object DOM to create the model
             @param objectDOM the XML DOM to parse
             @return modelJSON the resulting model attributes object
          */
-    parse: function(objectDOM){
-      if(!objectDOM)
-        var objectDOM = this.get("objectDOM");
-
-      var model = this,
-        modelJSON = {};
-
-      //Set the name
-      var person = $(objectDOM).children("individualname, individualName");
-
-      if(person.length)
-        modelJSON.individualName = this.parsePerson(person);
-
-      //Set the phone and fax numbers
-      var phones = $(objectDOM).children("phone"),
-        phoneNums = [],
-        faxNums = [];
-
-      phones.each(function(i, phone){
-        if($(phone).attr("phonetype") == "voice")
-          phoneNums.push($(phone).text());
-        else if($(phone).attr("phonetype") == "facsimile")
-          faxNums.push($(phone).text());
-      });
-
-      modelJSON.phone = phoneNums;
-      modelJSON.fax   = faxNums;
-
-      //Set the address
-      var addresses = $(objectDOM).children("address") || [],
-        addressesJSON = [];
-
-      addresses.each(function(i, address){
-        addressesJSON.push(model.parseAddress(address));
-      });
-
-      modelJSON.address = addressesJSON;
-
-      //Set the text fields
-      modelJSON.organizationName = $(objectDOM).children("organizationname, organizationName").text() || null;
-      modelJSON.positionName     = $(objectDOM).children("positionname, positionName").text() || null;
-      // roles
-      modelJSON.roles = [];
-      $(objectDOM).find("role").each(function(i,role){
-        modelJSON.roles.push($(role).text());
-      });
-
-      //Set the id attribute
-      modelJSON.xmlID = $(objectDOM).attr("id");
-
-      //Email - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
-      if( $(objectDOM).children("electronicmailaddress, electronicMailAddress").length ){
-        modelJSON.email = _.map($(objectDOM).children("electronicmailaddress, electronicMailAddress"), function(email){
-                    return  $(email).text();
-                  });
-      }
-
-      //Online URL - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
-      if( $(objectDOM).find("onlineurl, onlineUrl").length ){
-        // modelJSON.onlineUrl = [$(objectDOM).find("onlineurl, onlineUrl").first().text()];
-        modelJSON.onlineUrl = $(objectDOM).find("onlineurl, onlineUrl").map(function(i,v) {
-          return $(v).text();
-        }).get();
-      }
-
-      //User ID - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
-      if( $(objectDOM).find("userid, userId").length ){
-        modelJSON.userId = [$(objectDOM).find("userid, userId").first().text()];
-      }
-
-      return modelJSON;
-    },
+      parse: function (objectDOM) {
+        if (!objectDOM) var objectDOM = this.get("objectDOM");
 
-    parseNode: function(node){
-      if(!node || (Array.isArray(node) && !node.length))
-        return;
+        var model = this,
+          modelJSON = {};
 
-      this.set($(node)[0].localName, $(node).text());
-    },
+        //Set the name
+        var person = $(objectDOM).children("individualname, individualName");
 
-    parsePerson: function(personXML){
-      var person = {
-          givenName: [],
-          surName: "",
-          salutation: []
-        },
-        givenName   = $(personXML).find("givenname, givenName"),
-        surName     = $(personXML).find("surname, surName"),
-        salutations = $(personXML).find("salutation");
+        if (person.length) modelJSON.individualName = this.parsePerson(person);
+
+        //Set the phone and fax numbers
+        var phones = $(objectDOM).children("phone"),
+          phoneNums = [],
+          faxNums = [];
 
-      //Concatenate all the given names into one, for now
-      //TODO: Support multiple given names
-      givenName.each(function(i, name){
-        if(i==0)
-          person.givenName[0] = "";
+        phones.each(function (i, phone) {
+          if ($(phone).attr("phonetype") == "voice")
+            phoneNums.push($(phone).text());
+          else if ($(phone).attr("phonetype") == "facsimile")
+            faxNums.push($(phone).text());
+        });
+
+        modelJSON.phone = phoneNums;
+        modelJSON.fax = faxNums;
 
-        person.givenName[0] += $(name).text() + " ";
+        //Set the address
+        var addresses = $(objectDOM).children("address") || [],
+          addressesJSON = [];
 
-        if(i==givenName.length-1)
-          person.givenName[0] = person.givenName[0].trim();
-      });
+        addresses.each(function (i, address) {
+          addressesJSON.push(model.parseAddress(address));
+        });
 
-      person.surName = surName.text();
+        modelJSON.address = addressesJSON;
+
+        //Set the text fields
+        modelJSON.organizationName =
+          $(objectDOM).children("organizationname, organizationName").text() ||
+          null;
+        modelJSON.positionName =
+          $(objectDOM).children("positionname, positionName").text() || null;
+        // roles
+        modelJSON.roles = [];
+        $(objectDOM)
+          .find("role")
+          .each(function (i, role) {
+            modelJSON.roles.push($(role).text());
+          });
+
+        //Set the id attribute
+        modelJSON.xmlID = $(objectDOM).attr("id");
+
+        //Email - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
+        if (
+          $(objectDOM).children("electronicmailaddress, electronicMailAddress")
+            .length
+        ) {
+          modelJSON.email = _.map(
+            $(objectDOM).children(
+              "electronicmailaddress, electronicMailAddress",
+            ),
+            function (email) {
+              return $(email).text();
+            },
+          );
+        }
 
-      salutations.each(function(i, name){
-        person.salutation.push($(name).text());
-      });
+        //Online URL - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
+        if ($(objectDOM).find("onlineurl, onlineUrl").length) {
+          // modelJSON.onlineUrl = [$(objectDOM).find("onlineurl, onlineUrl").first().text()];
+          modelJSON.onlineUrl = $(objectDOM)
+            .find("onlineurl, onlineUrl")
+            .map(function (i, v) {
+              return $(v).text();
+            })
+            .get();
+        }
 
-      return person;
-    },
+        //User ID - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
+        if ($(objectDOM).find("userid, userId").length) {
+          modelJSON.userId = [
+            $(objectDOM).find("userid, userId").first().text(),
+          ];
+        }
 
-    parseAddress: function(addressXML){
-      var address    = {},
-        delPoint   = $(addressXML).find("deliverypoint, deliveryPoint"),
-        city       = $(addressXML).find("city"),
-        adminArea  = $(addressXML).find("administrativearea, administrativeArea"),
-        postalCode = $(addressXML).find("postalcode, postalCode"),
-        country    = $(addressXML).find("country");
+        return modelJSON;
+      },
 
-      address.city               = city.length? city.text() : "";
-      address.administrativeArea = adminArea.length? adminArea.text() : "";
-      address.postalCode         = postalCode.length? postalCode.text() : "";
-      address.country            = country.length? country.text() : "";
+      parseNode: function (node) {
+        if (!node || (Array.isArray(node) && !node.length)) return;
 
-      //Get an array of all the address line (or delivery point) values
-      var addressLines = [];
-      _.each(delPoint, function(addressLine, i){
-        addressLines.push($(addressLine).text());
-      }, this);
+        this.set($(node)[0].localName, $(node).text());
+      },
 
-      address.deliveryPoint = addressLines;
+      parsePerson: function (personXML) {
+        var person = {
+            givenName: [],
+            surName: "",
+            salutation: [],
+          },
+          givenName = $(personXML).find("givenname, givenName"),
+          surName = $(personXML).find("surname, surName"),
+          salutations = $(personXML).find("salutation");
+
+        //Concatenate all the given names into one, for now
+        //TODO: Support multiple given names
+        givenName.each(function (i, name) {
+          if (i == 0) person.givenName[0] = "";
+
+          person.givenName[0] += $(name).text() + " ";
+
+          if (i == givenName.length - 1)
+            person.givenName[0] = person.givenName[0].trim();
+        });
 
-      return  address;
-    },
+        person.surName = surName.text();
 
-    serialize: function(){
-      var objectDOM = this.updateDOM(),
-        xmlString = objectDOM.outerHTML;
+        salutations.each(function (i, name) {
+          person.salutation.push($(name).text());
+        });
 
-      //Camel-case the XML
+        return person;
+      },
+
+      parseAddress: function (addressXML) {
+        var address = {},
+          delPoint = $(addressXML).find("deliverypoint, deliveryPoint"),
+          city = $(addressXML).find("city"),
+          adminArea = $(addressXML).find(
+            "administrativearea, administrativeArea",
+          ),
+          postalCode = $(addressXML).find("postalcode, postalCode"),
+          country = $(addressXML).find("country");
+
+        address.city = city.length ? city.text() : "";
+        address.administrativeArea = adminArea.length ? adminArea.text() : "";
+        address.postalCode = postalCode.length ? postalCode.text() : "";
+        address.country = country.length ? country.text() : "";
+
+        //Get an array of all the address line (or delivery point) values
+        var addressLines = [];
+        _.each(
+          delPoint,
+          function (addressLine, i) {
+            addressLines.push($(addressLine).text());
+          },
+          this,
+        );
+
+        address.deliveryPoint = addressLines;
+
+        return address;
+      },
+
+      serialize: function () {
+        var objectDOM = this.updateDOM(),
+          xmlString = objectDOM.outerHTML;
+
+        //Camel-case the XML
         xmlString = this.formatXML(xmlString);
 
         return xmlString;
-    },
+      },
 
-    /*
-     * Updates the attributes on this model based on the application user (the app UserModel)
-     */
-    createFromUser: function(){
-      //Create the name from the user
-      var name = this.get("individualName") || {};
-      name.givenName  = [MetacatUI.appUserModel.get("firstName")];
-      name.surName    = MetacatUI.appUserModel.get("lastName");
-      this.set("individualName", name);
-
-      //Get the email and username
-      if(MetacatUI.appUserModel.get("email"))
-        this.set("email", [MetacatUI.appUserModel.get("email")]);
-
-      this.set("userId", [MetacatUI.appUserModel.get("username")]);
-    },
+      /*
+       * Updates the attributes on this model based on the application user (the app UserModel)
+       */
+      createFromUser: function () {
+        //Create the name from the user
+        var name = this.get("individualName") || {};
+        name.givenName = [MetacatUI.appUserModel.get("firstName")];
+        name.surName = MetacatUI.appUserModel.get("lastName");
+        this.set("individualName", name);
+
+        //Get the email and username
+        if (MetacatUI.appUserModel.get("email"))
+          this.set("email", [MetacatUI.appUserModel.get("email")]);
+
+        this.set("userId", [MetacatUI.appUserModel.get("username")]);
+      },
 
-    /*
-     * Makes a copy of the original XML DOM and updates it with the new values from the model.
-     */
-    updateDOM: function(){
-      var type = this.get("type") || "associatedParty",
-        objectDOM = this.get("objectDOM");
-
-      // If there is already an XML node for this model and it is the wrong type,
-      //   then replace the XML node contents
-      if(objectDOM && objectDOM.nodeName != type.toUpperCase()){
-        objectDOM = $(document.createElement(type)).html( objectDOM.innerHTML );
-      }
-      // If there is already an XML node for this model and it is the correct type,
-      //   then simply clone the XML node
-      else if(objectDOM){
-        objectDOM = objectDOM.cloneNode(true);
-      }
-      // Otherwise, create a new XML node
-      else{
-        objectDOM = document.createElement(type);
-      }
-
-      //There needs to be at least one individual name, organization name, or position name
-      if( this.nameIsEmpty() && !this.get("organizationName") && !this.get("positionName"))
-        return "";
-
-      var name = this.get("individualName");
-      if(name){
-        //Get the individualName node
-        var nameNode = $(objectDOM).find("individualname");
-        if(!nameNode.length){
-          nameNode = document.createElement("individualname");
-          $(objectDOM).prepend(nameNode);
+      /*
+       * Makes a copy of the original XML DOM and updates it with the new values from the model.
+       */
+      updateDOM: function () {
+        var type = this.get("type") || "associatedParty",
+          objectDOM = this.get("objectDOM");
+
+        // If there is already an XML node for this model and it is the wrong type,
+        //   then replace the XML node contents
+        if (objectDOM && objectDOM.nodeName != type.toUpperCase()) {
+          objectDOM = $(document.createElement(type)).html(objectDOM.innerHTML);
         }
+        // If there is already an XML node for this model and it is the correct type,
+        //   then simply clone the XML node
+        else if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+        }
+        // Otherwise, create a new XML node
+        else {
+          objectDOM = document.createElement(type);
+        }
+
+        //There needs to be at least one individual name, organization name, or position name
+        if (
+          this.nameIsEmpty() &&
+          !this.get("organizationName") &&
+          !this.get("positionName")
+        )
+          return "";
+
+        var name = this.get("individualName");
+        if (name) {
+          //Get the individualName node
+          var nameNode = $(objectDOM).find("individualname");
+          if (!nameNode.length) {
+            nameNode = document.createElement("individualname");
+            $(objectDOM).prepend(nameNode);
+          }
+
+          //Empty the individualName node
+          $(nameNode).empty();
 
-        //Empty the individualName node
-        $(nameNode).empty();
+          // salutation[s]
+          if (!Array.isArray(name.salutation) && name.salutation)
+            name.salutation = [name.salutation];
 
-         // salutation[s]
-         if(!Array.isArray(name.salutation) && name.salutation)
-          name.salutation = [name.salutation];
+          _.each(name.salutation, function (salutation) {
+            $(nameNode).prepend("<salutation>" + salutation + "</salutation>");
+          });
 
-         _.each(name.salutation, function(salutation) {
-           $(nameNode).prepend("<salutation>" + salutation + "</salutation>");
-         });
+          //Given name
+          if (!Array.isArray(name.givenName) && name.givenName)
+            name.givenName = [name.givenName];
+          _.each(name.givenName, function (givenName) {
+            //If there is a given name string, create a givenName node
+            if (typeof givenName == "string" && givenName) {
+              $(nameNode).append("<givenname>" + givenName + "</givenname>");
+            }
+          });
 
-         //Given name
-         if(!Array.isArray(name.givenName) && name.givenName) name.givenName = [name.givenName];
-         _.each(name.givenName, function(givenName) {
+          // surname
+          if (name.surName)
+            $(nameNode).append("<surname>" + name.surName + "</surname>");
+        }
+        //If there is no name set on the model, remove it from the DOM
+        else {
+          $(objectDOM).find("individualname").remove();
+        }
 
-          //If there is a given name string, create a givenName node
-          if(typeof givenName == "string" && givenName){
-            $(nameNode).append("<givenname>" + givenName + "</givenname>");
+        // organizationName
+        if (this.get("organizationName")) {
+          //Get the organization name node
+          if ($(objectDOM).find("organizationname").length)
+            var orgNameNode = $(objectDOM).find("organizationname").detach();
+          else var orgNameNode = document.createElement("organizationname");
+
+          //Insert the text
+          $(orgNameNode).text(this.get("organizationName"));
+
+          //If the DOM is empty, append it
+          if (!$(objectDOM).children().length) $(objectDOM).append(orgNameNode);
+          else {
+            var insertAfter = this.getEMLPosition(
+              objectDOM,
+              "organizationname",
+            );
+
+            if (insertAfter && insertAfter.length)
+              insertAfter.after(orgNameNode);
+            else $(objectDOM).prepend(orgNameNode);
           }
+        }
+        //Remove the organization name node if there is no organization name
+        else {
+          var orgNameNode = $(objectDOM).find("organizationname").remove();
+        }
 
-         });
-
-         // surname
-         if(name.surName)
-           $(nameNode).append("<surname>" +  name.surName + "</surname>");
-      }
-      //If there is no name set on the model, remove it from the DOM
-      else{
-        $(objectDOM).find("individualname").remove();
-      }
-
-       // organizationName
-      if(this.get("organizationName")){
-        //Get the organization name node
-        if($(objectDOM).find("organizationname").length)
-          var orgNameNode = $(objectDOM).find("organizationname").detach();
-        else
-          var orgNameNode = document.createElement("organizationname");
-
-        //Insert the text
-        $(orgNameNode).text(this.get("organizationName"));
-
-        //If the DOM is empty, append it
-        if( !$(objectDOM).children().length )
-          $(objectDOM).append(orgNameNode);
-        else{
-          var insertAfter = this.getEMLPosition(objectDOM, "organizationname");
-
-          if(insertAfter && insertAfter.length)
-            insertAfter.after(orgNameNode);
-          else
-            $(objectDOM).prepend(orgNameNode);
+        // positionName
+        if (this.get("positionName")) {
+          //Get the name node
+          if ($(objectDOM).find("positionname").length)
+            var posNameNode = $(objectDOM).find("positionname").detach();
+          else var posNameNode = document.createElement("positionname");
+
+          //Insert the text
+          $(posNameNode).text(this.get("positionName"));
+
+          //If the DOM is empty, append it
+          if (!$(objectDOM).children().length) $(objectDOM).append(posNameNode);
+          else {
+            let insertAfter = this.getEMLPosition(objectDOM, "positionname");
+            if (insertAfter) insertAfter.after(posNameNode);
+            else $(objectDOM).prepend(posNameNode);
+          }
         }
-      }
-      //Remove the organization name node if there is no organization name
-      else{
-        var orgNameNode = $(objectDOM).find("organizationname").remove();
-      }
-
-       // positionName
-      if(this.get("positionName")){
-        //Get the name node
-        if($(objectDOM).find("positionname").length)
-          var posNameNode = $(objectDOM).find("positionname").detach();
-        else
-          var posNameNode = document.createElement("positionname");
-
-        //Insert the text
-        $(posNameNode).text(this.get("positionName"));
-
-        //If the DOM is empty, append it
-        if( !$(objectDOM).children().length )
-          $(objectDOM).append(posNameNode);
-        else{
-          let insertAfter = this.getEMLPosition(objectDOM, "positionname")
-          if(insertAfter)
-            insertAfter.after(posNameNode);
-          else
-            $(objectDOM).prepend(posNameNode);
+        //Remove the position name node if there is no position name
+        else {
+          $(objectDOM).find("positionname").remove();
         }
-      }
-      //Remove the position name node if there is no position name
-      else{
-        $(objectDOM).find("positionname").remove();
-      }
-
-       // address
-       _.each(this.get("address"), function(address, i) {
-
-         var addressNode =  $(objectDOM).find("address")[i];
-
-         if(!addressNode){
-           addressNode = document.createElement("address");
-           this.getEMLPosition(objectDOM, "address").after(addressNode);
-         }
-
-         //Remove all the delivery points since they'll be reserialized
-         $(addressNode).find("deliverypoint").remove();
-
-         _.each(address.deliveryPoint, function(deliveryPoint, ii){
-           if(!deliveryPoint) return;
-
-           var delPointNode = $(addressNode).find("deliverypoint")[ii];
-
-           if(!delPointNode){
-             delPointNode = document.createElement("deliverypoint");
-
-             //Add the deliveryPoint node to the address node
-             //Insert after the last deliveryPoint node
-             var appendAfter = $(addressNode).find("deliverypoint")[ii-1];
-             if(appendAfter)
-               $(appendAfter).after(delPointNode);
-             //Or just prepend to the beginning
-             else
-               $(addressNode).prepend(delPointNode);
-           }
-
-           $(delPointNode).text(deliveryPoint);
-         });
-
-         if(address.city){
-           var cityNode = $(addressNode).find("city");
 
-           if(!cityNode.length){
-             cityNode = document.createElement("city");
+        // address
+        _.each(
+          this.get("address"),
+          function (address, i) {
+            var addressNode = $(objectDOM).find("address")[i];
 
-             if(this.getEMLPosition(addressNode, "city")){
-               this.getEMLPosition(addressNode, "city").after(cityNode);
-             }
-             else{
-                $(addressNode).append(cityNode);
-             }
-           }
+            if (!addressNode) {
+              addressNode = document.createElement("address");
+              this.getEMLPosition(objectDOM, "address").after(addressNode);
+            }
 
-           $(cityNode).text(address.city);
-         }
-         else{
-           $(addressNode).find("city").remove();
-         }
+            //Remove all the delivery points since they'll be reserialized
+            $(addressNode).find("deliverypoint").remove();
 
-         if(address.administrativeArea){
-           var adminAreaNode = $(addressNode).find("administrativearea");
+            _.each(address.deliveryPoint, function (deliveryPoint, ii) {
+              if (!deliveryPoint) return;
 
-           if(!adminAreaNode.length){
-             adminAreaNode = document.createElement("administrativearea");
+              var delPointNode = $(addressNode).find("deliverypoint")[ii];
 
-             if(this.getEMLPosition(addressNode, "administrativearea")){
-               this.getEMLPosition(addressNode, "administrativearea").after(adminAreaNode);
-             }
-             else{
-                $(addressNode).append(adminAreaNode);
-             }
+              if (!delPointNode) {
+                delPointNode = document.createElement("deliverypoint");
 
-           }
+                //Add the deliveryPoint node to the address node
+                //Insert after the last deliveryPoint node
+                var appendAfter = $(addressNode).find("deliverypoint")[ii - 1];
+                if (appendAfter) $(appendAfter).after(delPointNode);
+                //Or just prepend to the beginning
+                else $(addressNode).prepend(delPointNode);
+              }
 
-           $(adminAreaNode).text(address.administrativeArea);
-         }
-         else{
-           $(addressNode).find("administrativearea").remove();
-         }
+              $(delPointNode).text(deliveryPoint);
+            });
 
-         if(address.postalCode){
-           var postalcodeNode = $(addressNode).find("postalcode");
+            if (address.city) {
+              var cityNode = $(addressNode).find("city");
 
-           if(!postalcodeNode.length){
-             postalcodeNode = document.createElement("postalcode");
+              if (!cityNode.length) {
+                cityNode = document.createElement("city");
 
-             if(this.getEMLPosition(addressNode, "postalcode")){
-               this.getEMLPosition(addressNode, "postalcode").after(postalcodeNode);
-             }
-             else{
-                $(addressNode).append(postalcodeNode);
-             }
+                if (this.getEMLPosition(addressNode, "city")) {
+                  this.getEMLPosition(addressNode, "city").after(cityNode);
+                } else {
+                  $(addressNode).append(cityNode);
+                }
+              }
 
-           }
+              $(cityNode).text(address.city);
+            } else {
+              $(addressNode).find("city").remove();
+            }
 
-           $(postalcodeNode).text(address.postalCode);
-         }
-         else{
-           $(addressNode).find("postalcode").remove();
-         }
+            if (address.administrativeArea) {
+              var adminAreaNode = $(addressNode).find("administrativearea");
 
-         if(address.country){
-           var countryNode = $(addressNode).find("country");
+              if (!adminAreaNode.length) {
+                adminAreaNode = document.createElement("administrativearea");
 
-           if(!countryNode.length){
-             countryNode = document.createElement("country");
+                if (this.getEMLPosition(addressNode, "administrativearea")) {
+                  this.getEMLPosition(addressNode, "administrativearea").after(
+                    adminAreaNode,
+                  );
+                } else {
+                  $(addressNode).append(adminAreaNode);
+                }
+              }
 
-             if(this.getEMLPosition(addressNode, "country")){
-               this.getEMLPosition(addressNode, "country").after(countryNode);
-             }
-             else{
-                $(addressNode).append(countryNode);
-             }
+              $(adminAreaNode).text(address.administrativeArea);
+            } else {
+              $(addressNode).find("administrativearea").remove();
+            }
 
-           }
+            if (address.postalCode) {
+              var postalcodeNode = $(addressNode).find("postalcode");
 
-           $(countryNode).text(address.country);
-         }
-         else{
-           $(addressNode).find("country").remove();
-         }
+              if (!postalcodeNode.length) {
+                postalcodeNode = document.createElement("postalcode");
 
-       }, this);
+                if (this.getEMLPosition(addressNode, "postalcode")) {
+                  this.getEMLPosition(addressNode, "postalcode").after(
+                    postalcodeNode,
+                  );
+                } else {
+                  $(addressNode).append(postalcodeNode);
+                }
+              }
 
-       if( this.get("address").length == 0 ){
-         $(objectDOM).find("address").remove();
-       }
-
-       // phone[s]
-       $(objectDOM).find("phone[phonetype='voice']").remove();
-       _.each(this.get("phone"), function(phone) {
+              $(postalcodeNode).text(address.postalCode);
+            } else {
+              $(addressNode).find("postalcode").remove();
+            }
+
+            if (address.country) {
+              var countryNode = $(addressNode).find("country");
+
+              if (!countryNode.length) {
+                countryNode = document.createElement("country");
+
+                if (this.getEMLPosition(addressNode, "country")) {
+                  this.getEMLPosition(addressNode, "country").after(
+                    countryNode,
+                  );
+                } else {
+                  $(addressNode).append(countryNode);
+                }
+              }
+
+              $(countryNode).text(address.country);
+            } else {
+              $(addressNode).find("country").remove();
+            }
+          },
+          this,
+        );
 
-        var phoneNode = $(document.createElement("phone")).attr("phonetype", "voice").text(phone);
-        this.getEMLPosition(objectDOM, "phone").after(phoneNode);
+        if (this.get("address").length == 0) {
+          $(objectDOM).find("address").remove();
+        }
 
-       }, this);
-
-       // fax[es]
-       $(objectDOM).find("phone[phonetype='facsimile']").remove();
-       _.each(this.get("fax"), function(fax) {
-
-        var faxNode = $(document.createElement("phone")).attr("phonetype", "facsimile").text(fax);
-        this.getEMLPosition(objectDOM, "phone").after(faxNode);
-
-       }, this);
-
-       // electronicMailAddress[es]
-       $(objectDOM).find("electronicmailaddress").remove();
-       _.each(this.get("email"), function(email) {
-
-         var emailNode = document.createElement("electronicmailaddress");
-         this.getEMLPosition(objectDOM, "electronicmailaddress").after(emailNode);
-
-         $(emailNode).text(email);
-
-       }, this);
-
-      // online URL[es]
-      $(objectDOM).find("onlineurl").remove();
-       _.each(this.get("onlineUrl"), function(onlineUrl, i) {
-
-        var urlNode = document.createElement("onlineurl");
-        this.getEMLPosition(objectDOM, "onlineurl").after(urlNode);
-
-        $(urlNode).text(onlineUrl);
-
-       }, this);
-
-       //user ID
-       var userId = Array.isArray(this.get("userId")) ? this.get("userId") : [this.get("userId")];
-       _.each(userId, function(id) {
-         if(!id) return;
-
-         var idNode = $(objectDOM).find("userid");
-
-         //Create the userid node
-         if(!idNode.length){
-
-           idNode = $(document.createElement("userid"));
+        // phone[s]
+        $(objectDOM).find("phone[phonetype='voice']").remove();
+        _.each(
+          this.get("phone"),
+          function (phone) {
+            var phoneNode = $(document.createElement("phone"))
+              .attr("phonetype", "voice")
+              .text(phone);
+            this.getEMLPosition(objectDOM, "phone").after(phoneNode);
+          },
+          this,
+        );
+
+        // fax[es]
+        $(objectDOM).find("phone[phonetype='facsimile']").remove();
+        _.each(
+          this.get("fax"),
+          function (fax) {
+            var faxNode = $(document.createElement("phone"))
+              .attr("phonetype", "facsimile")
+              .text(fax);
+            this.getEMLPosition(objectDOM, "phone").after(faxNode);
+          },
+          this,
+        );
+
+        // electronicMailAddress[es]
+        $(objectDOM).find("electronicmailaddress").remove();
+        _.each(
+          this.get("email"),
+          function (email) {
+            var emailNode = document.createElement("electronicmailaddress");
+            this.getEMLPosition(objectDOM, "electronicmailaddress").after(
+              emailNode,
+            );
+
+            $(emailNode).text(email);
+          },
+          this,
+        );
+
+        // online URL[es]
+        $(objectDOM).find("onlineurl").remove();
+        _.each(
+          this.get("onlineUrl"),
+          function (onlineUrl, i) {
+            var urlNode = document.createElement("onlineurl");
+            this.getEMLPosition(objectDOM, "onlineurl").after(urlNode);
+
+            $(urlNode).text(onlineUrl);
+          },
+          this,
+        );
+
+        //user ID
+        var userId = Array.isArray(this.get("userId"))
+          ? this.get("userId")
+          : [this.get("userId")];
+        _.each(
+          userId,
+          function (id) {
+            if (!id) return;
+
+            var idNode = $(objectDOM).find("userid");
+
+            //Create the userid node
+            if (!idNode.length) {
+              idNode = $(document.createElement("userid"));
+
+              this.getEMLPosition(objectDOM, "userid").after(idNode);
+            }
 
-           this.getEMLPosition(objectDOM, "userid").after(idNode);
-         }
+            //If this is an orcid identifier, format it correctly
+            if (this.isOrcid(id)) {
+              // Add the directory attribute
+              idNode.attr("directory", "https://orcid.org");
+
+              //If this ORCID does not start with "http"
+              if (id.indexOf("http") == -1) {
+                //If this is an ORCID with just the 16-digit numbers and hyphens, then add
+                // the https://orcid.org/ prefix to it
+                if (id.length == 19) {
+                  id = "https://orcid.org/" + id;
+                }
+                //If it starts with "orcid.org", then add the "https://" prefix
+                else if (id.indexOf("orcid.org") == 0) {
+                  id = "https://" + id;
+                }
+                //If it starts with "www.orcid.org", then add the "https" prefix and remove the "www"
+                else if (id.indexOf("www.orcid.org") == 0) {
+                  id = "https://" + id.replace("www.orcid.org", "orcid.org");
+                }
+              }
 
-         //If this is an orcid identifier, format it correctly
-         if(this.isOrcid(id)){
-            // Add the directory attribute
-           idNode.attr("directory", "https://orcid.org");
-
-           //If this ORCID does not start with "http"
-           if( id.indexOf("http") == -1 ){
-             //If this is an ORCID with just the 16-digit numbers and hyphens, then add
-             // the https://orcid.org/ prefix to it
-             if( id.length == 19){
-               id = "https://orcid.org/" + id;
-             }
-             //If it starts with "orcid.org", then add the "https://" prefix
-             else if( id.indexOf("orcid.org") == 0 ){
-               id = "https://" + id;
-             }
-             //If it starts with "www.orcid.org", then add the "https" prefix and remove the "www"
-             else if( id.indexOf("www.orcid.org") == 0 ){
-               id = "https://" + id.replace("www.orcid.org", "orcid.org");
-             }
-           }
-
-           //If there is a "www", remove it
-           if( id.indexOf("www.orcid.org") > -1 ){
-             id = id.replace("www.orcid.org", "orcid.org");
-           }
-
-           //If it has the http:// prefix, add the 's' for secure protocol
-           if( id.indexOf("http://") == 0){
-             id = id.replace("http", "https");
-           }
+              //If there is a "www", remove it
+              if (id.indexOf("www.orcid.org") > -1) {
+                id = id.replace("www.orcid.org", "orcid.org");
+              }
 
-         }
-         else{
-           idNode.attr("directory", "unknown");
-         }
+              //If it has the http:// prefix, add the 's' for secure protocol
+              if (id.indexOf("http://") == 0) {
+                id = id.replace("http", "https");
+              }
+            } else {
+              idNode.attr("directory", "unknown");
+            }
 
-         $(idNode).text(id);
+            $(idNode).text(id);
+          },
+          this,
+        );
 
-       }, this);
+        //Remove all the user id's if there aren't any in the model
+        if (userId.length == 0) {
+          $(objectDOM).find("userid").remove();
+        }
 
-       //Remove all the user id's if there aren't any in the model
-       if( userId.length == 0 ){
-         $(objectDOM).find("userid").remove();
-       }
-
-      // role
-      //If this party type is not an associated party, then remove the role element
-      if( type != "associatedParty" && type != "personnel" ){
-        $(objectDOM).find("role").remove();
-      }
-      //Otherwise, change the value of the role element
-      else {
-        // If for some reason there is no role, create a default role
-        if( !this.get("roles").length ){
-          var roles = ["Associated Party"];
-        } else {
-          var roles = this.get("roles");
+        // role
+        //If this party type is not an associated party, then remove the role element
+        if (type != "associatedParty" && type != "personnel") {
+          $(objectDOM).find("role").remove();
         }
-        _.each(roles, function(role, i){
-          var roleSerialized = $(objectDOM).find("role");
-          if(roleSerialized.length){
-            $(roleSerialized[i]).text(role)
+        //Otherwise, change the value of the role element
+        else {
+          // If for some reason there is no role, create a default role
+          if (!this.get("roles").length) {
+            var roles = ["Associated Party"];
           } else {
-            roleSerialized = $(document.createElement("role")).text(role);
-            this.getEMLPosition(objectDOM, "role").after( roleSerialized );
+            var roles = this.get("roles");
           }
-        }, this);
-
-      }
+          _.each(
+            roles,
+            function (role, i) {
+              var roleSerialized = $(objectDOM).find("role");
+              if (roleSerialized.length) {
+                $(roleSerialized[i]).text(role);
+              } else {
+                roleSerialized = $(document.createElement("role")).text(role);
+                this.getEMLPosition(objectDOM, "role").after(roleSerialized);
+              }
+            },
+            this,
+          );
+        }
 
-      //XML id attribute
-      this.createID();
-      //if(this.get("xmlID"))
+        //XML id attribute
+        this.createID();
+        //if(this.get("xmlID"))
         $(objectDOM).attr("id", this.get("xmlID"));
-      //else
-      //  $(objectDOM).removeAttr("id");
-
-      // Remove empty (zero-length or whitespace-only) nodes
-      $(objectDOM).find("*").filter(function() { return $.trim(this.innerHTML) === ""; } ).remove();
-
-       return objectDOM;
-    },
+        //else
+        //  $(objectDOM).removeAttr("id");
+
+        // Remove empty (zero-length or whitespace-only) nodes
+        $(objectDOM)
+          .find("*")
+          .filter(function () {
+            return $.trim(this.innerHTML) === "";
+          })
+          .remove();
+
+        return objectDOM;
+      },
 
       /*
-      * Adds this EMLParty model to it's parent EML211 model in the appropriate role array
-      *
-      * @return {boolean} - Returns true if the merge was successful, false if the merge was cancelled
-      */
-      mergeIntoParent: function(){
+       * Adds this EMLParty model to it's parent EML211 model in the appropriate role array
+       *
+       * @return {boolean} - Returns true if the merge was successful, false if the merge was cancelled
+       */
+      mergeIntoParent: function () {
         //Get the type of EML Party, in relation to the parent model
-        if(this.get("type") && this.get("type") != "associatedParty")
+        if (this.get("type") && this.get("type") != "associatedParty")
           var type = this.get("type");
-        else
-          var type = "associatedParty";
+        else var type = "associatedParty";
 
         //Update the list of EMLParty models in the parent model
         var parentEML = this.getParentEML();
 
-        if(parentEML.type != "EML")
-          return false;
+        if (parentEML.type != "EML") return false;
 
         //Add this model to the EML model
         var successfulAdd = parentEML.addParty(this);
@@ -693,397 +740,451 @@ 

Source: src/js/models/metadata/eml211/EMLParty.js

return successfulAdd; }, - isEmpty: function(){ + isEmpty: function () { // If we add any new fields, be sure to add the attribute here - var attributes = ["userId", "fax", "phone", "onlineUrl", - "email", "positionName", "organizationName"]; - - //Check each value in the model that gets serialized to see if there is a value - for(var i in attributes) { - //Get the value from the model for this attribute - var modelValue = this.get(attributes[i]); - - //If this is an array, then we want to check if there are any values in it - if( Array.isArray(modelValue) ){ - if( modelValue.length > 0 ) - return false; - } - //Otherwise, check if this value differs from the default value - else if(this.get(attributes[i]) !== this.defaults()[attributes[i]]){ - return false; - } - } + var attributes = [ + "userId", + "fax", + "phone", + "onlineUrl", + "email", + "positionName", + "organizationName", + ]; + + //Check each value in the model that gets serialized to see if there is a value + for (var i in attributes) { + //Get the value from the model for this attribute + var modelValue = this.get(attributes[i]); + + //If this is an array, then we want to check if there are any values in it + if (Array.isArray(modelValue)) { + if (modelValue.length > 0) return false; + } + //Otherwise, check if this value differs from the default value + else if (this.get(attributes[i]) !== this.defaults()[attributes[i]]) { + return false; + } + } - //Check for a first and last name - if( this.get("individualName") && (this.get("individualName").givenName || this.get("individualName").surName) ) + //Check for a first and last name + if ( + this.get("individualName") && + (this.get("individualName").givenName || + this.get("individualName").surName) + ) return false; - //Check for addresses - var isAddress = false; - - if( this.get("address") ){ - - //Checks if there are any values anywhere in the address - _.each(this.get("address"), function(address){ - //Delivery point is an array so we need to check the first and second - //values of that array - if( address.administrativeArea || address.city || - address.country || address.postalCode || - (address.deliveryPoint && address.deliveryPoint.length && - (address.deliveryPoint[0] || address.deliveryPoint[1]) ) ){ - isAddress = true; - } - }); - - } + //Check for addresses + var isAddress = false; + + if (this.get("address")) { + //Checks if there are any values anywhere in the address + _.each(this.get("address"), function (address) { + //Delivery point is an array so we need to check the first and second + //values of that array + if ( + address.administrativeArea || + address.city || + address.country || + address.postalCode || + (address.deliveryPoint && + address.deliveryPoint.length && + (address.deliveryPoint[0] || address.deliveryPoint[1])) + ) { + isAddress = true; + } + }); + } - //If we found an address value anywhere, then it is not empty - if(isAddress) - return false; + //If we found an address value anywhere, then it is not empty + if (isAddress) return false; - //If we never found a value, then return true because this model is empty - return true; + //If we never found a value, then return true because this model is empty + return true; }, - /* - * Returns the node in the given EML snippet that the given node type should be inserted after - */ - getEMLPosition: function(objectDOM, nodeName){ - var nodeOrder = [ "individualname", "organizationname", "positionname", "address", "phone", - "electronicmailaddress", "onlineurl", "userid", "role"]; - var addressOrder = ["deliverypoint", "city", "administrativearea", "postalcode", "country"]; - - //If this is an address node, find the position within the address - if( _.contains(addressOrder, nodeName) ){ - nodeOrder = addressOrder; - } + /* + * Returns the node in the given EML snippet that the given node type should be inserted after + */ + getEMLPosition: function (objectDOM, nodeName) { + var nodeOrder = [ + "individualname", + "organizationname", + "positionname", + "address", + "phone", + "electronicmailaddress", + "onlineurl", + "userid", + "role", + ]; + var addressOrder = [ + "deliverypoint", + "city", + "administrativearea", + "postalcode", + "country", + ]; + + //If this is an address node, find the position within the address + if (_.contains(addressOrder, nodeName)) { + nodeOrder = addressOrder; + } - var position = _.indexOf(nodeOrder, nodeName); - if(position == -1) - return $(objectDOM).children().last(); + var position = _.indexOf(nodeOrder, nodeName); + if (position == -1) return $(objectDOM).children().last(); - //Go through each node in the node list and find the position where this node will be inserted after - for(var i=position-1; i>=0; i--){ - if($(objectDOM).find(nodeOrder[i]).length) - return $(objectDOM).find(nodeOrder[i]).last(); - } + //Go through each node in the node list and find the position where this node will be inserted after + for (var i = position - 1; i >= 0; i--) { + if ($(objectDOM).find(nodeOrder[i]).length) + return $(objectDOM).find(nodeOrder[i]).last(); + } - return false; - }, + return false; + }, - createID: function(){ - this.set("xmlID", Math.ceil(Math.random() * (9999999999999999 - 1000000000000000) + 1000000000000000)); - }, + createID: function () { + this.set( + "xmlID", + Math.ceil( + Math.random() * (9999999999999999 - 1000000000000000) + + 1000000000000000, + ), + ); + }, - setType: function(){ - if(this.get("roles")){ - if(this.get("roles").length && !this.get("type")){ - this.set("type", "associatedParty"); + setType: function () { + if (this.get("roles")) { + if (this.get("roles").length && !this.get("type")) { + this.set("type", "associatedParty"); + } } - } - }, + }, - trickleUpChange: function(){ - if ( this.get("parentModel") ) { - MetacatUI.rootDataPackage.packageModel.set("changed", true); + trickleUpChange: function () { + if (this.get("parentModel")) { + MetacatUI.rootDataPackage.packageModel.set("changed", true); } - }, + }, - removeFromParent: function(){ - if( !this.get("parentModel") ) - return; - else if( typeof this.get("parentModel").removeParty != "function" ) - return; + removeFromParent: function () { + if (!this.get("parentModel")) return; + else if (typeof this.get("parentModel").removeParty != "function") + return; this.get("parentModel").removeParty(this); this.set("removed", true); - }, - - /* - * Checks the values of the model to determine if it is EML-valid - */ - validate: function(){ - - var individualName = this.get("individualName") || {}, - givenName = individualName.givenName || [], - surName = individualName.surName || null, - errors = {}; - - //If there are no values in this model that would be serialized, then the model is valid - if( !this.get("organizationName") && !this.get("positionName") && !givenName[0]?.length && !surName - && !this.get("address").length && !this.get("phone").length && !this.get("fax").length - && !this.get("email").length && !this.get("onlineUrl").length && !this.get("userId").length){ - - return; - - } + }, - //The EMLParty must have either an organization name, position name, or surname. - // It must ALSO have a type or role. - if ( !this.get("organizationName") && !this.get("positionName") && - (!this.get("individualName") || !surName ) ){ + /* + * Checks the values of the model to determine if it is EML-valid + */ + validate: function () { + var individualName = this.get("individualName") || {}, + givenName = individualName.givenName || [], + surName = individualName.surName || null, + errors = {}; + + //If there are no values in this model that would be serialized, then the model is valid + if ( + !this.get("organizationName") && + !this.get("positionName") && + !givenName[0]?.length && + !surName && + !this.get("address").length && + !this.get("phone").length && + !this.get("fax").length && + !this.get("email").length && + !this.get("onlineUrl").length && + !this.get("userId").length + ) { + return; + } - errors = { - surName: "Either a last name, position name, or organization name is required.", - positionName: "", - organizationName: "" + //The EMLParty must have either an organization name, position name, or surname. + // It must ALSO have a type or role. + if ( + !this.get("organizationName") && + !this.get("positionName") && + (!this.get("individualName") || !surName) + ) { + errors = { + surName: + "Either a last name, position name, or organization name is required.", + positionName: "", + organizationName: "", + }; + } + //If there is a first name and no last name, then this is not a valid individualName + else if ( + givenName[0]?.length && + !surName && + this.get("organizationName") && + this.get("positionName") + ) { + errors = { surName: "Provide a last name." }; } - } - //If there is a first name and no last name, then this is not a valid individualName - else if( (givenName[0]?.length && !surName) && this.get("organizationName") && this.get("positionName") ){ + //Check that each required field has a value. Required fields are configured in the {@link AppConfig} + let roles = + this.get("type") == "associatedParty" + ? this.get("roles") + : [this.get("type")]; + for (role of roles) { + let requiredFields = MetacatUI.appModel.get( + "emlEditorRequiredFields_EMLParty", + )[role]; + requiredFields?.forEach((field) => { + let currentVal = this.get(field); + if (!currentVal || !currentVal?.length) { + errors[field] = + `Provide a${["a", "e", "i", "o", "u"].includes(field.charAt(0)) ? "n " : " "} ${field}. `; + } + }); + } - errors = { surName: "Provide a last name." } + return Object.keys(errors)?.length ? errors : false; + }, - } + isOrcid: function (username) { + if (!username) return false; - //Check that each required field has a value. Required fields are configured in the {@link AppConfig} - let roles = this.get("type") == "associatedParty"? this.get("roles") : [this.get("type")]; - for( role of roles ){ - let requiredFields = MetacatUI.appModel.get("emlEditorRequiredFields_EMLParty")[role]; - requiredFields?.forEach(field=>{ - let currentVal = this.get(field); - if( !currentVal || !currentVal?.length ){ - errors[field] = `Provide a${(["a","e","i","o","u"].includes(field.charAt(0))? "n " : " ")} ${field}. `; - } - }); - } + //If the ORCID URL is anywhere in this username string, it is an ORCID + if (username.indexOf("orcid.org") > -1) { + return true; + } - - return Object.keys(errors)?.length? errors : false; + /* The ORCID checksum algorithm to determine is a character string is an ORCiD + * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier + */ + var total = 0, + baseDigits = username.replace(/-/g, "").substr(0, 15); - - }, + for (var i = 0; i < baseDigits.length; i++) { + var digit = parseInt(baseDigits.charAt(i)); + total = (total + digit) * 2; + } - isOrcid: function(username){ - if(!username) return false; + var remainder = total % 11, + result = (12 - remainder) % 11, + checkDigit = result == 10 ? "X" : result.toString(), + isOrcid = checkDigit == username.charAt(username.length - 1); - //If the ORCID URL is anywhere in this username string, it is an ORCID - if(username.indexOf("orcid.org") > -1){ - return true; - } + return isOrcid; + }, - /* The ORCID checksum algorithm to determine is a character string is an ORCiD - * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier + /* + * Clones all the values of this array into a new JS Object. + * Special care is needed for nested objects and arrays + * This is helpful when copying this EMLParty to another role in the EML */ - var total = 0, - baseDigits = username.replace(/-/g, "").substr(0, 15); - - for(var i=0; i<baseDigits.length; i++){ - var digit = parseInt(baseDigits.charAt(i)); - total = (total + digit) * 2; - } + copyValues: function () { + //Get a JSON object of all the model copyValues + var modelValues = this.toJSON(); + + //Go through each model value and properly clone the arrays + _.each(Object.keys(modelValues), function (key, i) { + //Clone the array via slice() + if (Array.isArray(modelValues[key])) + modelValues[key] = modelValues[key].slice(0); + }); - var remainder = total % 11, - result = (12 - remainder) % 11, - checkDigit = (result == 10) ? "X" : result.toString(), - isOrcid = (checkDigit == username.charAt(username.length-1)); + //Individual Names are objects, so properly clone them + if (modelValues.individualName) { + modelValues.individualName = Object.assign( + {}, + modelValues.individualName, + ); + } - return isOrcid; - }, + //Addresses are objects, so properly clone them + if (modelValues.address.length) { + _.each(modelValues.address, function (address, i) { + modelValues.address[i] = Object.assign({}, address); - /* - * Clones all the values of this array into a new JS Object. - * Special care is needed for nested objects and arrays - * This is helpful when copying this EMLParty to another role in the EML - */ - copyValues: function(){ - //Get a JSON object of all the model copyValues - var modelValues = this.toJSON(); - - //Go through each model value and properly clone the arrays - _.each( Object.keys(modelValues), function(key, i){ - - //Clone the array via slice() - if( Array.isArray(modelValues[key]) ) - modelValues[key] = modelValues[key].slice(0); - - }); - - //Individual Names are objects, so properly clone them - if( modelValues.individualName ){ - modelValues.individualName = Object.assign({}, modelValues.individualName); - } - - //Addresses are objects, so properly clone them - if( modelValues.address.length ){ - _.each(modelValues.address, function(address, i){ - modelValues.address[i] = Object.assign({}, address); - - //The delivery point is an array of strings, so properly clone the array - if( Array.isArray(modelValues.address[i].deliveryPoint) ) - modelValues.address[i].deliveryPoint = modelValues.address[i].deliveryPoint.slice(0); - }); - } - return modelValues; - }, + //The delivery point is an array of strings, so properly clone the array + if (Array.isArray(modelValues.address[i].deliveryPoint)) + modelValues.address[i].deliveryPoint = + modelValues.address[i].deliveryPoint.slice(0); + }); + } + return modelValues; + }, - /** - * getName - For an individual, returns the first and last name as a string. Otherwise, - * returns the organization or position name. - * - * @return {string} Returns the name of the party or an empty string if one cannot be found - * - * @since 2.15.0 - */ - getName: function(){ - return this.get("individualName") ? - this.get("individualName").givenName + " " + this.get("individualName").surName : - this.get("organizationName") || this.get("positionName") || ""; + /** + * getName - For an individual, returns the first and last name as a string. Otherwise, + * returns the organization or position name. + * + * @return {string} Returns the name of the party or an empty string if one cannot be found + * + * @since 2.15.0 + */ + getName: function () { + return this.get("individualName") + ? this.get("individualName").givenName + + " " + + this.get("individualName").surName + : this.get("organizationName") || this.get("positionName") || ""; }, - - /** - * Return the EML Party as a CSL JSON object. See - * {@link https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#names}. - * @return {object} The CSL JSON object - * @since 2.23.0 - */ - toCSLJSON: function () { - const name = this.get("individualName"); - const csl= { - family: name?.surName || null, - given: name?.givenName || null, - literal: this.get("organizationName") || this.get("positionName") || "", - "dropping-particle": name?.salutation || null, - } - // If any of the fields are arrays, join them with a space - for (const key in csl) { - if (Array.isArray(csl[key])) { - csl[key] = csl[key].join(" "); + + /** + * Return the EML Party as a CSL JSON object. See + * {@link https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#names}. + * @return {object} The CSL JSON object + * @since 2.23.0 + */ + toCSLJSON: function () { + const name = this.get("individualName"); + const csl = { + family: name?.surName || null, + given: name?.givenName || null, + literal: + this.get("organizationName") || this.get("positionName") || "", + "dropping-particle": name?.salutation || null, + }; + // If any of the fields are arrays, join them with a space + for (const key in csl) { + if (Array.isArray(csl[key])) { + csl[key] = csl[key].join(" "); + } } - } - return csl; - }, + return csl; + }, - /* - * function nameIsEmpty - Returns true if the individualName set on this - * model contains only empty values. Otherwise, returns false. This is just a - * shortcut for manually checking each name field individually. - * - * @return {boolean} - */ - nameIsEmpty: function(){ - var name = this.get("individualName"); - - if( !name || typeof name != "object" ) - return true; + /* + * function nameIsEmpty - Returns true if the individualName set on this + * model contains only empty values. Otherwise, returns false. This is just a + * shortcut for manually checking each name field individually. + * + * @return {boolean} + */ + nameIsEmpty: function () { + var name = this.get("individualName"); - //Check if there are given names - var givenName = name.givenName, + if (!name || typeof name != "object") return true; + + //Check if there are given names + var givenName = name.givenName, givenNameEmpty = false; - if( !givenName || (Array.isArray(givenName) && givenName.length == 0) || - (typeof givenName == "string" && givenName.trim().length == 0) ) + if ( + !givenName || + (Array.isArray(givenName) && givenName.length == 0) || + (typeof givenName == "string" && givenName.trim().length == 0) + ) givenNameEmpty = true; - //Check if there are no sur names - var surName = name.surName, + //Check if there are no sur names + var surName = name.surName, surNameEmpty = false; - if( !surName || (Array.isArray(surName) && surName.length == 0) || - (typeof surName == "string" && surName.trim().length == 0) ) + if ( + !surName || + (Array.isArray(surName) && surName.length == 0) || + (typeof surName == "string" && surName.trim().length == 0) + ) surNameEmpty = true; - //Check if there are no salutations - var salutation = name.salutation, + //Check if there are no salutations + var salutation = name.salutation, salutationEmpty = false; - if( !salutation || (Array.isArray(salutation) && salutation.length == 0) || - (typeof salutation == "string" && salutation.trim().length == 0) ) + if ( + !salutation || + (Array.isArray(salutation) && salutation.length == 0) || + (typeof salutation == "string" && salutation.trim().length == 0) + ) salutationEmpty = true; - if( givenNameEmpty && surNameEmpty && salutationEmpty ) - return true; - else - return false; - - }, + if (givenNameEmpty && surNameEmpty && salutationEmpty) return true; + else return false; + }, - /* - * Climbs up the model heirarchy until it finds the EML model - * - * @return {EML211 or false} - Returns the EML 211 Model or false if not found - */ - getParentEML: function(){ - var emlModel = this.get("parentModel"), + /* + * Climbs up the model heirarchy until it finds the EML model + * + * @return {EML211 or false} - Returns the EML 211 Model or false if not found + */ + getParentEML: function () { + var emlModel = this.get("parentModel"), tries = 0; - while (emlModel.type !== "EML" && tries < 6){ - emlModel = emlModel.get("parentModel"); - tries++; - } + while (emlModel.type !== "EML" && tries < 6) { + emlModel = emlModel.get("parentModel"); + tries++; + } - if( emlModel && emlModel.type == "EML") - return emlModel; - else - return false; + if (emlModel && emlModel.type == "EML") return emlModel; + else return false; + }, - }, + /** + * @type {object[]} + * @property {string} label - The name of the party category to display to the user + * @property {string} dataCategory - The string that is used to represent this party. This value + * should exactly match one of the strings listed in EMLParty typeOptions or EMLParty roleOptions. + * @property {string} description - An optional description to display below the label to help the user + * with this category. + * @property {boolean} createFromUser - If set to true, the information from the logged-in user will be + * used to create an EML party for this category if none exist already when the view loads. + * @property {number} limit - If the number of parties allowed for this category is not unlimited, + * then limit should be set to the maximum allowable number. + * @since 2.21.0 + */ + partyTypes: [ + { + label: "Dataset Creators (Authors/Owners/Originators)", + dataCategory: "creator", + description: + "Each person or organization listed as a Creator will be listed in the data" + + " citation. At least one person, organization, or position with a 'Creator'" + + " role is required.", + createFromUser: true, + }, + { + label: "Contact", + dataCategory: "contact", + createFromUser: true, + }, + { + label: "Principal Investigators", + dataCategory: "principalInvestigator", + }, + { + label: "Co-Principal Investigators", + dataCategory: "coPrincipalInvestigator", + }, + { + label: "Collaborating-Principal Investigators", + dataCategory: "collaboratingPrincipalInvestigator", + }, + { + label: "Metadata Provider", + dataCategory: "metadataProvider", + }, + { + label: "Custodians/Stewards", + dataCategory: "custodianSteward", + }, + { + label: "Publisher", + dataCategory: "publisher", + description: "Only one publisher can be specified.", + limit: 1, + }, + { + label: "Users", + dataCategory: "user", + }, + ], - /** - * @type {object[]} - * @property {string} label - The name of the party category to display to the user - * @property {string} dataCategory - The string that is used to represent this party. This value - * should exactly match one of the strings listed in EMLParty typeOptions or EMLParty roleOptions. - * @property {string} description - An optional description to display below the label to help the user - * with this category. - * @property {boolean} createFromUser - If set to true, the information from the logged-in user will be - * used to create an EML party for this category if none exist already when the view loads. - * @property {number} limit - If the number of parties allowed for this category is not unlimited, - * then limit should be set to the maximum allowable number. - * @since 2.21.0 - */ - partyTypes: [ - { - label: "Dataset Creators (Authors/Owners/Originators)", - dataCategory: "creator", - description: "Each person or organization listed as a Creator will be listed in the data" + - " citation. At least one person, organization, or position with a 'Creator'" + - " role is required.", - createFromUser: true - }, - { - label: "Contact", - dataCategory: "contact", - createFromUser: true - }, - { - label: "Principal Investigators", - dataCategory: "principalInvestigator" - }, - { - label: "Co-Principal Investigators", - dataCategory: "coPrincipalInvestigator" - }, - { - label: "Collaborating-Principal Investigators", - dataCategory: "collaboratingPrincipalInvestigator" - }, - { - label: "Metadata Provider", - dataCategory: "metadataProvider" - }, - { - label: "Custodians/Stewards", - dataCategory: "custodianSteward" - }, - { - label: "Publisher", - dataCategory: "publisher", - description: "Only one publisher can be specified.", - limit: 1 + formatXML: function (xmlString) { + return DataONEObject.prototype.formatXML.call(this, xmlString); + }, }, - { - label: "Users", - dataCategory: "user" - } - ], - - formatXML: function(xmlString){ - return DataONEObject.prototype.formatXML.call(this, xmlString); - } - }); + ); return EMLParty; }); diff --git a/docs/docs/src_js_models_metadata_eml211_EMLTaxonCoverage.js.html b/docs/docs/src_js_models_metadata_eml211_EMLTaxonCoverage.js.html index 0c0e105ca..2ccb6142c 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLTaxonCoverage.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLTaxonCoverage.js.html @@ -44,12 +44,11 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js
-
/* global define */
-define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
   $,
   _,
   Backbone,
-  DataONEObject
+  DataONEObject,
 ) {
   /**
    * @name taxonomicClassification
@@ -78,9 +77,9 @@ 

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js * @classcategory Models/Metadata/EML * @extends Backbone.Model * @constructor - */ + */ var EMLTaxonCoverage = Backbone.Model.extend( - /** @lends EMLTaxonCoverage.prototype */{ + /** @lends EMLTaxonCoverage.prototype */ { /** * Returns the default properties for this model. Defined here. * @type {Object} @@ -122,7 +121,8 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js taxonomicsystem: "taxonomicSystem", classificationsystem: "classificationSystem", classificationsystemcitation: "classificationSystemCitation", - classificationsystemmodifications: "classificationSystemModifications", + classificationsystemmodifications: + "classificationSystemModifications", identificationreference: "identificationReference", identifiername: "identifierName", taxonomicprocedures: "taxonomicProcedures", @@ -137,14 +137,14 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js var model = this, taxonomicClassifications = $(objectDOM).children( - "taxonomicclassification" + "taxonomicclassification", ), modelJSON = { taxonomicClassification: _.map( taxonomicClassifications, function (tc) { return model.parseTaxonomicClassification(tc); - } + }, ), generalTaxonomicCoverage: $(objectDOM) .children("generaltaxonomiccoverage") @@ -162,7 +162,7 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js var commonName = $(classification).children("commonname"); var taxonId = $(classification).children("taxonId"); var taxonomicClassification = $(classification).children( - "taxonomicclassification" + "taxonomicclassification", ); var model = this, @@ -183,7 +183,7 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js taxonomicClassification, function (tc) { return model.parseTaxonomicClassification(tc); - } + }, ), }; @@ -222,8 +222,8 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js if (_.isString(generalCoverage) && generalCoverage.length > 0) { $(objectDOM).append( $(document.createElement("generaltaxonomiccoverage")).text( - this.get("generalTaxonomicCoverage") - ) + this.get("generalTaxonomicCoverage"), + ), ); } @@ -239,7 +239,7 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js for (var i = 0; i < classifications.length; i++) { $(objectDOM).append( - this.createTaxonomicClassificationDOM(classifications[i]) + this.createTaxonomicClassificationDOM(classifications[i]), ); } @@ -276,19 +276,19 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js if (taxonRankName && taxonRankName.length > 0) { $(finishedEl).append( - $(document.createElement("taxonrankname")).text(taxonRankName) + $(document.createElement("taxonrankname")).text(taxonRankName), ); } if (taxonRankValue && taxonRankValue.length > 0) { $(finishedEl).append( - $(document.createElement("taxonrankvalue")).text(taxonRankValue) + $(document.createElement("taxonrankvalue")).text(taxonRankValue), ); } if (commonName && commonName.length > 0) { $(finishedEl).append( - $(document.createElement("commonname")).text(commonName) + $(document.createElement("commonname")).text(commonName), ); } @@ -311,7 +311,7 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js function (tc) { $(finishedEl).append(this.createTaxonomicClassificationDOM(tc)); }, - this + this, ); } @@ -345,7 +345,7 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js !_.every( this.get("taxonomicClassification"), this.isClassificationValid, - this + this, ) ) errors.taxonomicClassification = @@ -383,7 +383,7 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js if (taxonomicClassification.taxonomicClassification) return this.isClassificationValid( - taxonomicClassification.taxonomicClassification + taxonomicClassification.taxonomicClassification, ); else return true; }, @@ -414,7 +414,7 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js if (c.taxonId) stringified.taxonId = c.taxonId; if (c.taxonomicClassification) { stringified.taxonomicClassification = stringifyClassification( - c.taxonomicClassification + c.taxonomicClassification, ); } const st = JSON.stringify(stringified); @@ -444,7 +444,9 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js const classifications = this.get("taxonomicClassification"); for (let i = 0; i < classifications.length; i++) { if (typeof indexToSkip === "number" && i === indexToSkip) continue; - if (this.classificationsAreEqual(classifications[i], classification)) { + if ( + this.classificationsAreEqual(classifications[i], classification) + ) { return true; } } @@ -503,7 +505,8 @@

Source: src/js/models/metadata/eml211/EMLTaxonCoverage.js formatXML: function (xmlString) { return DataONEObject.prototype.formatXML.call(this, xmlString); }, - }); + }, + ); return EMLTaxonCoverage; }); diff --git a/docs/docs/src_js_models_metadata_eml211_EMLTemporalCoverage.js.html b/docs/docs/src_js_models_metadata_eml211_EMLTemporalCoverage.js.html index 2935ea072..930102a0a 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLTemporalCoverage.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLTemporalCoverage.js.html @@ -44,507 +44,531 @@

Source: src/js/models/metadata/eml211/EMLTemporalCoverage
-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'],
-    function($, _, Backbone, DataONEObject) {
-
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+  $,
+  _,
+  Backbone,
+  DataONEObject,
+) {
   /**
-  * @class EMLTemporalCoverage
-  * @classcategory Models/Metadata/EML211
-  * @extends Backbone.Model
-  */
-	var EMLTemporalCoverage = Backbone.Model.extend(
-    /** @lends EMLTemporalCoverage.prototype */{
-
-		defaults: {
-			objectXML: null,
-			objectDOM: null,
-			beginDate: null,
-			beginTime: null,
-			endDate: null,
-			endTime: null
-		},
-
-		initialize: function(attributes){
-			if(attributes && attributes.objectDOM)
-				this.set(this.parse(attributes.objectDOM));
-
-			this.on("change:beginDate change:beginTime change:endDate change:endTime", this.trickleUpChange);
-		},
-
-		/*
-         * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
-         * Used during parse() and serialize()
-         */
-        nodeNameMap: function(){
-        	return {
-        		"begindate" : "beginDate",
-        		"calendardate" : "calendarDate",
-        		"enddate" : "endDate",
-            	"rangeofdates" : "rangeOfDates",
-            	"singledatetime" : "singleDateTime",
-            	"spatialraster" : "spatialRaster",
-            	"spatialvector" : "spatialVector",
-            	"storedprocedure" : "storedProcedure",
-            	"temporalcoverage" : "temporalCoverage"
+   * @class EMLTemporalCoverage
+   * @classcategory Models/Metadata/EML211
+   * @extends Backbone.Model
+   */
+  var EMLTemporalCoverage = Backbone.Model.extend(
+    /** @lends EMLTemporalCoverage.prototype */ {
+      defaults: {
+        objectXML: null,
+        objectDOM: null,
+        beginDate: null,
+        beginTime: null,
+        endDate: null,
+        endTime: null,
+      },
+
+      initialize: function (attributes) {
+        if (attributes && attributes.objectDOM)
+          this.set(this.parse(attributes.objectDOM));
+
+        this.on(
+          "change:beginDate change:beginTime change:endDate change:endTime",
+          this.trickleUpChange,
+        );
+      },
+
+      /*
+       * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
+       * Used during parse() and serialize()
+       */
+      nodeNameMap: function () {
+        return {
+          begindate: "beginDate",
+          calendardate: "calendarDate",
+          enddate: "endDate",
+          rangeofdates: "rangeOfDates",
+          singledatetime: "singleDateTime",
+          spatialraster: "spatialRaster",
+          spatialvector: "spatialVector",
+          storedprocedure: "storedProcedure",
+          temporalcoverage: "temporalCoverage",
+        };
+      },
+
+      parse: function (objectDOM) {
+        if (!objectDOM) var objectDOM = this.get("objectDOM");
+
+        var rangeOfDates = $(objectDOM).children("rangeofdates"),
+          singleDateTime = $(objectDOM).children("singledatetime");
+
+        // If the temporalCoverage element has both a rangeOfDates and a
+        // singleDateTime (invalid EML), the rangeOfDates is preferred.
+        if (rangeOfDates.length) {
+          return this.parseRangeOfDates(rangeOfDates);
+        } else if (singleDateTime.length) {
+          return this.parseSingleDateTime(singleDateTime);
+        }
+      },
+
+      parseRangeOfDates: function (rangeOfDates) {
+        var beginDate = $(rangeOfDates).find("beginDate"),
+          endDate = $(rangeOfDates).find("endDate"),
+          properties = {};
+
+        if (beginDate.length > 0) {
+          if ($(beginDate).find("calendardate")) {
+            properties.beginDate = $(beginDate)
+              .find("calendardate")
+              .first()
+              .text();
+          }
+
+          if ($(beginDate).find("time").length > 0) {
+            properties.beginTime = $(beginDate).find("time").first().text();
+          }
+        }
+
+        if (endDate.length > 0) {
+          if ($(endDate).find("calendardate").length > 0) {
+            properties.endDate = $(endDate).find("calendardate").first().text();
+          }
+
+          if ($(endDate).find("time").length > 0) {
+            properties.endTime = $(endDate).find("time").first().text();
+          }
+        }
+
+        return properties;
+      },
+
+      parseSingleDateTime: function (singleDateTime) {
+        var calendarDate = $(singleDateTime).find("calendardate"),
+          time = $(singleDateTime).find("time");
+
+        return {
+          beginDate:
+            calendarDate.length > 0 ? calendarDate.first().text() : null,
+          beginTime: time.length > 0 ? time.first().text() : null,
+        };
+      },
+
+      serialize: function () {
+        var objectDOM = this.updateDOM(),
+          xmlString = objectDOM.outerHTML;
+
+        //Camel-case the XML
+        xmlString = this.formatXML(xmlString);
+
+        return xmlString;
+      },
+
+      /*
+       * Makes a copy of the original XML DOM and updates it with the new values from the model.
+       */
+      updateDOM: function () {
+        var objectDOM;
+
+        if (this.get("objectDOM")) {
+          objectDOM = this.get("objectDOM").cloneNode(true);
+          //Empty the DOM
+          $(objectDOM).empty();
+        } else {
+          objectDOM = $("<temporalcoverage></temporalcoverage>");
+        }
+
+        if (this.get("beginDate") && this.get("endDate")) {
+          $(objectDOM).append(this.serializeRangeOfDates());
+        } else if (!this.get("endDate")) {
+          $(objectDOM).append(this.serializeSingleDateTime());
+        } else if (this.get("singleDateTime")) {
+          var singleDateTime = $(objectDOM).find("singledatetime");
+          if (!singleDateTime.length) {
+            singleDateTime = document.createElement("singledatetime");
+            $(objectDOM).append(singleDateTime);
+          }
+
+          if (this.get("singleDateTime").calendarDate)
+            $(singleDateTime).html(
+              this.serializeSingleDateTime(
+                this.get("singleDateTime").calendarDate,
+              ),
+            );
+        }
+
+        // Remove empty (zero-length or whitespace-only) nodes
+        $(objectDOM)
+          .find("*")
+          .filter(function () {
+            return $.trim(this.innerHTML) === "";
+          })
+          .remove();
+
+        return objectDOM;
+      },
+
+      serializeRangeOfDates: function () {
+        var objectDOM = $(document.createElement("rangeofdates")),
+          beginDateEl = $(document.createElement("begindate")),
+          endDateEl = $(document.createElement("enddate"));
+
+        if (this.get("beginDate")) {
+          $(beginDateEl).append(
+            this.serializeCalendarDate(this.get("beginDate")),
+          );
+
+          if (this.get("beginTime")) {
+            $(beginDateEl).append(this.serializeTime(this.get("beginTime")));
+          }
+
+          objectDOM.append(beginDateEl);
+        }
+
+        if (this.get("endDate")) {
+          $(endDateEl).append(this.serializeCalendarDate(this.get("endDate")));
+
+          if (this.get("endTime")) {
+            $(endDateEl).append(this.serializeTime(this.get("endTime")));
+          }
+          objectDOM.append(endDateEl);
+        }
+
+        return objectDOM;
+      },
+
+      serializeSingleDateTime: function () {
+        var objectDOM = $(document.createElement("singleDateTime"));
+
+        if (this.get("beginDate")) {
+          $(objectDOM).append(
+            this.serializeCalendarDate(this.get("beginDate")),
+          );
+
+          if (this.get("beginTime")) {
+            $(objectDOM).append(this.serializeTime(this.get("beginTime")));
+          }
+        }
+
+        return objectDOM;
+      },
+
+      serializeCalendarDate: function (date) {
+        return $(document.createElement("calendarDate")).html(date);
+      },
+
+      serializeTime: function (time) {
+        return $(document.createElement("time")).html(time);
+      },
+
+      trickleUpChange: function () {
+        if (
+          _.contains(MetacatUI.rootDataPackage.models, this.get("parentModel"))
+        )
+          MetacatUI.rootDataPackage.packageModel.set("changed", true);
+      },
+
+      mergeIntoParent: function () {
+        if (
+          this.get("parentModel") &&
+          this.get("parentModel").type == "EML" &&
+          !_.contains(this.get("parentModel").get("temporalCoverage"), this)
+        )
+          this.get("parentModel").get("temporalCoverage").push(this);
+      },
+
+      formatXML: function (xmlString) {
+        return DataONEObject.prototype.formatXML.call(this, xmlString);
+      },
+
+      // Checks the values of this model and determines whether they are valid according the the EML 2.1.1 schema.
+      // Returns a hash of error messages
+      validate: function () {
+        var beginDate = this.get("beginDate"),
+          beginTime = this.get("beginTime"),
+          endDate = this.get("endDate"),
+          endTime = this.get("endTime"),
+          errors = {};
+
+        // A valid temporal coverage at least needs a start date
+        if (!beginDate) {
+          errors.beginDate = "Provide a begin date.";
+        }
+        // endTime is set but endDate is not
+        else if (
+          endTime &&
+          endTime.length > 0 &&
+          (!endDate || endDate.length == 0)
+        ) {
+          errors.endDate = "Provide an end date.";
+        }
+
+        //Check the validity of the date format
+        if (beginDate && !this.isDateFormatValid(beginDate)) {
+          errors.beginDate =
+            "The begin date must be formatted as YYYY-MM-DD or YYYY.";
+        }
+
+        //Check the validity of the date format
+        if (endDate && !this.isDateFormatValid(endDate)) {
+          errors.endDate =
+            "The end date must be formatted as YYYY-MM-DD or YYYY.";
+        }
+
+        if (typeof endDate == "string" && endDate.length && beginDate <= 0) {
+          errors.beginDate = "The begin date must be greater than zero.";
+        }
+
+        if (typeof endDate == "string" && endDate.length && endDate <= 0) {
+          errors.endDate = "The end date must be greater than zero.";
+        }
+
+        //Check the validity of the begin time format
+        if (beginTime) {
+          var timeErrorMessage = this.validateTimeFormat(beginTime);
+
+          if (typeof timeErrorMessage == "string")
+            errors.beginTime = timeErrorMessage;
+        }
+
+        //Check the validity of the end time format
+        if (endTime) {
+          var timeErrorMessage = this.validateTimeFormat(endTime);
+
+          if (typeof timeErrorMessage == "string")
+            errors.endTime = timeErrorMessage;
+        }
+
+        // Check if begin date greater than end date for the temporalCoverage
+        if (this.isGreaterDate(beginDate, endDate))
+          errors.beginDate = "The begin date must be before the end date.";
+
+        // Check if begin time greater than end time for the temporalCoverage in case of equal dates.
+        if (this.isGreaterTime(beginDate, endDate, beginTime, endTime))
+          errors.beginTime = "The begin time must be before the end time.";
+
+        if (Object.keys(errors).length) return errors;
+        else return;
+      },
+
+      isDateFormatValid: function (dateString) {
+        //Date strings that are four characters should be a full year. Make sure all characters are numbers
+        if (dateString.length == 4) {
+          var digits = dateString.match(/[0-9]/g);
+          return digits.length == 4;
+        }
+        //Date strings that are 10 characters long should be a valid date
+        else {
+          var dateParts = dateString.split("-");
+
+          if (
+            dateParts.length != 3 ||
+            dateParts[0].length != 4 ||
+            dateParts[1].length != 2 ||
+            dateParts[2].length != 2
+          )
+            return false;
+
+          dateYear = dateParts[0];
+          dateMonth = dateParts[1];
+          dateDay = dateParts[2];
+
+          // Validating the values for the date and month if in YYYY-MM-DD format.
+          if (dateMonth < 1 || dateMonth > 12) return false;
+          else if (dateDay < 1 || dateDay > 31) return false;
+          else if (
+            (dateMonth == 4 ||
+              dateMonth == 6 ||
+              dateMonth == 9 ||
+              dateMonth == 11) &&
+            dateDay == 31
+          )
+            return false;
+          else if (dateMonth == 2) {
+            // Validation for leap year dates.
+            var isleap =
+              dateYear % 4 == 0 && (dateYear % 100 != 0 || dateYear % 400 == 0);
+            if (dateDay > 29 || (dateDay == 29 && !isleap)) return false;
+          }
+
+          var digits = _.filter(dateParts, function (part) {
+            return part.match(/[0-9]/g).length == part.length;
+          });
+
+          return digits.length == 3;
+        }
+      },
+
+      validateTimeFormat: function (timeString) {
+        //If the last character is a "Z", then remove it for now
+        if (
+          timeString.substring(timeString.length - 1, timeString.length) == "Z"
+        ) {
+          timeString = timeString.replace("Z", "", "g");
+        }
+
+        if (timeString.length == 8) {
+          var timeParts = timeString.split(":");
+
+          if (timeParts.length != 3) {
+            return "Time must be formatted as HH:MM:SS";
+          }
+
+          // Validation pattern for HH:MM:SS values.
+          // Range for HH validation : 00-24
+          // Range for MM validation : 00-59
+          // Range for SS validation : 00-59
+          // Leading 0's are must in case of single digit values.
+          var timePattern = /^(?:2[0-4]|[01][0-9]):[0-5][0-9]:[0-5][0-9]$/,
+            validTimePattern = timeString.match(timePattern);
+
+          //If the hour is 24, only accept 00:00 for MM:SS. Any minutes or seconds in the midnight hour should be
+          //formatted as 00:XX:XX not 24:XX:XX
+          if (
+            validTimePattern &&
+            timeParts[0] == "24" &&
+            (timeParts[1] != "00" || timeParts[2] != "00")
+          ) {
+            return "The midnight hour starts at 00:00:00 and ends at 00:59:59.";
+          } else if (!validTimePattern && parseInt(timeParts[0]) > "24") {
+            return "Time of the day starts at 00:00 and ends at 23:59.";
+          } else if (!validTimePattern && parseInt(timeParts[1]) > "59") {
+            return "Minutes should be between 00 and 59.";
+          } else if (!validTimePattern && parseInt(timeParts[2]) > "59") {
+            return "Seconds should be between 00 and 59.";
+          } else return true;
+        } else return "Time must be formatted as HH:MM:SS";
+      },
+
+      /**
+       * This function checks whether the begin date is greater than the end date.
+       *
+       * @param {string} beginDate the begin date string
+       * @param {string} endDate the end date string
+       * @return {boolean}
+       */
+      isGreaterDate: function (beginDate, endDate) {
+        if (typeof beginDate == "undefined" || !beginDate) return false;
+
+        if (typeof endDate == "undefined" || !endDate) return false;
+
+        //Making sure that beginDate year is smaller than endDate year
+        if (beginDate.length == 4 && endDate.length == 4) {
+          if (beginDate > endDate) {
+            return true;
+          }
+        }
+
+        //Checking equality for either dateStrings that are greater than 4 characters
+        else {
+          beginDateParts = beginDate.split("-");
+          endDateParts = endDate.split("-");
+
+          if (beginDateParts.length == endDateParts.length) {
+            if (beginDateParts[0] > endDateParts[0]) {
+              return true;
+            } else if (beginDateParts[0] == endDateParts[0]) {
+              if (beginDateParts[1] > endDateParts[1]) {
+                return true;
+              } else if (beginDateParts[1] == endDateParts[1]) {
+                if (beginDateParts[2] > endDateParts[2]) {
+                  return true;
+                }
+              }
             }
-        },
-
-		parse: function(objectDOM){
-			if(!objectDOM) var objectDOM = this.get("objectDOM");
-
-			var rangeOfDates   = $(objectDOM).children('rangeofdates'),
-				singleDateTime = $(objectDOM).children('singledatetime');
-
-			// If the temporalCoverage element has both a rangeOfDates and a
-			// singleDateTime (invalid EML), the rangeOfDates is preferred.
-			if (rangeOfDates.length) {
-				return this.parseRangeOfDates(rangeOfDates);
-			} else if (singleDateTime.length) {
-				return this.parseSingleDateTime(singleDateTime);
-			}
-		},
-
-		parseRangeOfDates: function(rangeOfDates) {
-			var beginDate = $(rangeOfDates).find('beginDate'),
-				endDate = $(rangeOfDates).find('endDate'),
-				properties = {};
-
-			if (beginDate.length > 0) {
-				if ($(beginDate).find('calendardate')) {
-					properties.beginDate = $(beginDate).find('calendardate').first().text();
-				}
-
-				if ($(beginDate).find('time').length > 0) {
-					properties.beginTime = $(beginDate).find('time').first().text();
-				}
-			}
-
-			if (endDate.length > 0) {
-				if ($(endDate).find('calendardate').length > 0) {
-					properties.endDate = $(endDate).find('calendardate').first().text();
-				}
-
-				if ($(endDate).find('time').length > 0) {
-					properties.endTime = $(endDate).find('time').first().text();
-				}
-			}
-
-			return properties;
-		},
-
-		parseSingleDateTime: function(singleDateTime) {
-			var calendarDate = $(singleDateTime).find("calendardate"),
-			    time = $(singleDateTime).find("time");
-
-			return {
-				beginDate: calendarDate.length > 0 ? calendarDate.first().text() : null,
-				beginTime: time.length > 0 ? time.first().text() : null
-			};
-		},
-
-		serialize: function(){
-			var objectDOM = this.updateDOM(),
-				xmlString = objectDOM.outerHTML;
-
-			//Camel-case the XML
-	    	xmlString = this.formatXML(xmlString);
-
-	    	return xmlString;
-		},
-
-		/*
-		 * Makes a copy of the original XML DOM and updates it with the new values from the model.
-		 */
-		updateDOM: function(){
-			var objectDOM;
-
-			if (this.get("objectDOM")) {
-				objectDOM = this.get("objectDOM").cloneNode(true);
-				//Empty the DOM
-				$(objectDOM).empty();
-			} else {
-				objectDOM = $("<temporalcoverage></temporalcoverage>");
-			}
-
-			if (this.get('beginDate') && this.get('endDate')) {
-				$(objectDOM).append(this.serializeRangeOfDates());
-			} else if (!this.get('endDate')) {
-				$(objectDOM).append(this.serializeSingleDateTime());
-			}
-			else if(this.get("singleDateTime")){
-				var singleDateTime = $(objectDOM).find("singledatetime");
-				if(!singleDateTime.length){
-					singleDateTime = document.createElement("singledatetime");
-					$(objectDOM).append(singleDateTime);
-				}
-
-				if(this.get("singleDateTime").calendarDate)
-					$(singleDateTime).html(this.serializeSingleDateTime( this.get("singleDateTime").calendarDate ));
-			}
-
-			// Remove empty (zero-length or whitespace-only) nodes
-			$(objectDOM).find("*").filter(function() { return $.trim(this.innerHTML) === ""; } ).remove();
-
-			return objectDOM;
-		},
-
-		serializeRangeOfDates: function() {
-			var objectDOM = $(document.createElement('rangeofdates')),
-			    beginDateEl = $(document.createElement('begindate')),
-				endDateEl = $(document.createElement('enddate'));
-
-			if (this.get('beginDate')) {
-				$(beginDateEl).append(this.serializeCalendarDate(this.get('beginDate')));
-
-				if (this.get('beginTime')) {
-					$(beginDateEl).append(this.serializeTime(this.get('beginTime')));
-				}
-
-				objectDOM.append(beginDateEl);
-			}
-
-			if (this.get('endDate')) {
-				$(endDateEl).append(this.serializeCalendarDate(this.get('endDate')));
-
-				if (this.get('endTime')) {
-					$(endDateEl).append(this.serializeTime(this.get('endTime')));
-				}
-				objectDOM.append(endDateEl);
-			}
-
-			return objectDOM;
-		},
-
-		serializeSingleDateTime: function() {
-			var objectDOM = $(document.createElement('singleDateTime'));
-
-			if (this.get('beginDate')) {
-				$(objectDOM).append(this.serializeCalendarDate(this.get('beginDate')));
-
-				if (this.get('beginTime')) {
-					$(objectDOM).append(this.serializeTime(this.get('beginTime')));
-				}
-			}
-
-			return objectDOM;
-		},
-
-		serializeCalendarDate: function(date) {
-			return $(document.createElement('calendarDate')).html(date);
-		},
-
-		serializeTime: function(time) {
-			return $(document.createElement('time')).html(time);
-		},
-
-		trickleUpChange: function(){
-			if(_.contains(MetacatUI.rootDataPackage.models, this.get("parentModel")))
-				MetacatUI.rootDataPackage.packageModel.set("changed", true);
-		},
-
-		mergeIntoParent: function(){
-			if(this.get("parentModel") && this.get("parentModel").type == "EML" && !_.contains(this.get("parentModel").get("temporalCoverage"), this))
-				this.get("parentModel").get("temporalCoverage").push(this);
-		},
-
-		formatXML: function(xmlString){
-			return DataONEObject.prototype.formatXML.call(this, xmlString);
-		},
-
-		// Checks the values of this model and determines whether they are valid according the the EML 2.1.1 schema.
-		// Returns a hash of error messages
-		validate: function() {
-			var beginDate = this.get('beginDate'),
-			    beginTime = this.get('beginTime'),
-				endDate = this.get('endDate'),
-				endTime = this.get('endTime'),
-				errors  = {};
-
-			// A valid temporal coverage at least needs a start date
-			if (!beginDate) {
-				errors.beginDate = "Provide a begin date.";
-			}
-			// endTime is set but endDate is not
-			else if (endTime && endTime.length > 0 && (!endDate || endDate.length == 0)) {
-				errors.endDate = "Provide an end date."
-			}
-
-			//Check the validity of the date format
-			if(beginDate && !this.isDateFormatValid(beginDate)){
-				errors.beginDate = "The begin date must be formatted as YYYY-MM-DD or YYYY.";
-			}
-
-			//Check the validity of the date format
-			if(endDate && !this.isDateFormatValid(endDate)){
-				errors.endDate = "The end date must be formatted as YYYY-MM-DD or YYYY.";
-			}
-
-      if( typeof endDate == "string" && endDate.length && beginDate <= 0 ){
-        errors.beginDate = "The begin date must be greater than zero.";
-      }
-
-      if( typeof endDate == "string" && endDate.length && endDate <= 0 ){
-        errors.endDate = "The end date must be greater than zero.";
-      }
-
-			//Check the validity of the begin time format
-			if(beginTime){
-				var timeErrorMessage = this.validateTimeFormat(beginTime);
-
-				if( typeof timeErrorMessage == "string" )
-					errors.beginTime = timeErrorMessage;
-			}
-
-			//Check the validity of the end time format
-			if(endTime){
-				var timeErrorMessage = this.validateTimeFormat(endTime);
-
-				if( typeof timeErrorMessage == "string" )
-					errors.endTime = timeErrorMessage;
-			}
-
-			// Check if begin date greater than end date for the temporalCoverage
-			if (this.isGreaterDate(beginDate, endDate))
-				errors.beginDate = "The begin date must be before the end date."
-
-			// Check if begin time greater than end time for the temporalCoverage in case of equal dates.
-			if (this.isGreaterTime(beginDate, endDate, beginTime, endTime))
-				errors.beginTime = "The begin time must be before the end time."
-
-			if(Object.keys(errors).length)
-				return errors;
-			else
-				return;
-		},
-
-		isDateFormatValid: function(dateString){
-
-			//Date strings that are four characters should be a full year. Make sure all characters are numbers
-			if(dateString.length == 4){
-				var digits = dateString.match( /[0-9]/g );
-				return (digits.length == 4)
-			}
-			//Date strings that are 10 characters long should be a valid date
-			else{
-				var dateParts = dateString.split("-");
-
-				if(dateParts.length != 3 || dateParts[0].length != 4 || dateParts[1].length != 2 || dateParts[2].length != 2)
-					return false;
-
-				dateYear = dateParts[0];
-				dateMonth = dateParts[1];
-				dateDay = dateParts[2];
-
-				// Validating the values for the date and month if in YYYY-MM-DD format.
-				if (dateMonth < 1 || dateMonth > 12)
-					return false;
-				else if (dateDay < 1 || dateDay > 31)
-					return false;
-				else if ((dateMonth == 4 || dateMonth == 6 || dateMonth == 9 || dateMonth == 11) && dateDay == 31)
-					return false;
-				else if (dateMonth == 2) {
-				// Validation for leap year dates.
-					var isleap = (dateYear % 4 == 0 && (dateYear % 100 != 0 || dateYear % 400 == 0));
-					if ((dateDay > 29) || (dateDay == 29 && !isleap))
-						return false;
-				}
-
-				var digits = _.filter(dateParts, function(part){
-					return (part.match( /[0-9]/g ).length == part.length);
-				});
-
-				return (digits.length == 3);
-			}
-		},
-
-		validateTimeFormat: function(timeString){
-
-			//If the last character is a "Z", then remove it for now
-			if( timeString.substring(timeString.length-1, timeString.length) == "Z"){
-				timeString = timeString.replace("Z", "", "g");
-			}
-
-			if(timeString.length == 8){
-				var timeParts = timeString.split(":");
-
-				if(timeParts.length != 3){
-					return "Time must be formatted as HH:MM:SS";
-				}
-
-				// Validation pattern for HH:MM:SS values.
-				// Range for HH validation : 00-24
-				// Range for MM validation : 00-59
-				// Range for SS validation : 00-59
-				// Leading 0's are must in case of single digit values.
-				var timePattern = /^(?:2[0-4]|[01][0-9]):[0-5][0-9]:[0-5][0-9]$/,
-					validTimePattern = timeString.match(timePattern);
-
-				//If the hour is 24, only accept 00:00 for MM:SS. Any minutes or seconds in the midnight hour should be
-				//formatted as 00:XX:XX not 24:XX:XX
-				if(validTimePattern && timeParts[0] == "24" && (timeParts[1] != "00" || timeParts[2] != "00")){
-					return "The midnight hour starts at 00:00:00 and ends at 00:59:59.";
-				}
-				else if(!validTimePattern && parseInt(timeParts[0]) > "24"){
-					return "Time of the day starts at 00:00 and ends at 23:59.";
-				}
-				else if(!validTimePattern && parseInt(timeParts[1]) > "59"){
-					return "Minutes should be between 00 and 59.";
-				}
-				else if(!validTimePattern && parseInt(timeParts[2]) > "59"){
-					return "Seconds should be between 00 and 59.";
-				}
-				else
-					return true;
-
-			}
-			else
-				return "Time must be formatted as HH:MM:SS";
-		},
-
-		/**
-		 * This function checks whether the begin date is greater than the end date.
-		 *
-		 * @param {string} beginDate the begin date string
-		 * @param {string} endDate the end date string
-		 * @return {boolean}
-		 */
-		isGreaterDate: function(beginDate, endDate) {
-
-			if(typeof beginDate == "undefined" || !beginDate)
-				return false;
-
-			if(typeof endDate == "undefined" || !endDate)
-				return false;
-
-			//Making sure that beginDate year is smaller than endDate year
-			if (beginDate.length == 4 && endDate.length == 4) {
-				if (beginDate > endDate) {
-					return true;
-				}
-			}
-
-			//Checking equality for either dateStrings that are greater than 4 characters
-			else {
-				beginDateParts = beginDate.split("-");
-				endDateParts = endDate.split("-");
-
-				if (beginDateParts.length == endDateParts.length) {
-					if (beginDateParts[0] > endDateParts[0]) {
-						return true;
-					}
-					else if (beginDateParts[0] == endDateParts[0]) {
-						if (beginDateParts[1] > endDateParts[1]) {
-							return true;
-						}
-						else if (beginDateParts[1] == endDateParts[1]) {
-							if (beginDateParts[2] > endDateParts[2]) {
-								return true;
-							}
-						}
-					}
-				}
-				else {
-					if (beginDateParts[0] > endDateParts[0]) {
-						return true;
-					}
-				}
-			}
-			return false;
-		},
-
-        /**
-		 * This function checks whether the begin time is greater than the end time.
-		 *
-		 * @param {string} beginDate the begin date string
-		 * @param {string} endDate the end date string
-		 * @param {string} beginTime the begin time string
-		 * @param {string} endTime the end time string
-		 * @return {boolean}
-		 */
-		isGreaterTime: function (beginDate, endDate, beginTime, endTime) {
-			if(!beginTime || !endTime)
-				return false;
-
-			var equalDates = false;
-
-			//Making sure that beginDate year is smaller than endDate year
-			if (beginDate.length == 4 && endDate.length == 4) {
-				if (beginDate == endDate) {
-					equalDates = true;
-				}
-			}
-
-			else {
-				beginDateParts = beginDate.split("-");
-				endDateParts = endDate.split("-");
-
-				if (beginDateParts.length == endDateParts.length) {
-					if (beginDateParts[0] == endDateParts[0]) {
-						if (beginDateParts[1] == endDateParts[1]) {
-							if (beginDateParts[2] == endDateParts[2]) {
-								equalDates = true;
-							}
-						}
-					}
-				}
-			}
-
-			// If the dates are equal, check for validity of time frame.
-			if (equalDates) {
-				beginTimeParts = beginTime.split(":");
-				endTimeParts = endTime.split(":");
-				if (beginTimeParts[0] > endTimeParts[0]) {
-					return true;
-				}
-				else if (beginTimeParts[0] == endTimeParts[0]) {
-					if (beginTimeParts[1] > endTimeParts[1]) {
-						return true;
-					}
-					else if (beginTimeParts[1] == endTimeParts[1]) {
-						if (beginTimeParts[2] > endTimeParts[2]) {
-							return true;
-						}
-					}
-				}
-			}
-			return false;
-		},
-
-    /**
-    * Checks if this model has no values set on it
-    * @return {boolean}
-    */
-    isEmpty: function(){
-
-      return (!this.get('beginDate') && !this.get('beginTime') && !this.get('endDate')
-              && !this.get('endTime'));
-
-    },
-
-    /*
-    * Climbs up the model heirarchy until it finds the EML model
-    *
-    * @return {EML211 or false} - Returns the EML 211 Model or false if not found
-    */
-    getParentEML: function(){
-      var emlModel = this.get("parentModel"),
+          } else {
+            if (beginDateParts[0] > endDateParts[0]) {
+              return true;
+            }
+          }
+        }
+        return false;
+      },
+
+      /**
+       * This function checks whether the begin time is greater than the end time.
+       *
+       * @param {string} beginDate the begin date string
+       * @param {string} endDate the end date string
+       * @param {string} beginTime the begin time string
+       * @param {string} endTime the end time string
+       * @return {boolean}
+       */
+      isGreaterTime: function (beginDate, endDate, beginTime, endTime) {
+        if (!beginTime || !endTime) return false;
+
+        var equalDates = false;
+
+        //Making sure that beginDate year is smaller than endDate year
+        if (beginDate.length == 4 && endDate.length == 4) {
+          if (beginDate == endDate) {
+            equalDates = true;
+          }
+        } else {
+          beginDateParts = beginDate.split("-");
+          endDateParts = endDate.split("-");
+
+          if (beginDateParts.length == endDateParts.length) {
+            if (beginDateParts[0] == endDateParts[0]) {
+              if (beginDateParts[1] == endDateParts[1]) {
+                if (beginDateParts[2] == endDateParts[2]) {
+                  equalDates = true;
+                }
+              }
+            }
+          }
+        }
+
+        // If the dates are equal, check for validity of time frame.
+        if (equalDates) {
+          beginTimeParts = beginTime.split(":");
+          endTimeParts = endTime.split(":");
+          if (beginTimeParts[0] > endTimeParts[0]) {
+            return true;
+          } else if (beginTimeParts[0] == endTimeParts[0]) {
+            if (beginTimeParts[1] > endTimeParts[1]) {
+              return true;
+            } else if (beginTimeParts[1] == endTimeParts[1]) {
+              if (beginTimeParts[2] > endTimeParts[2]) {
+                return true;
+              }
+            }
+          }
+        }
+        return false;
+      },
+
+      /**
+       * Checks if this model has no values set on it
+       * @return {boolean}
+       */
+      isEmpty: function () {
+        return (
+          !this.get("beginDate") &&
+          !this.get("beginTime") &&
+          !this.get("endDate") &&
+          !this.get("endTime")
+        );
+      },
+
+      /*
+       * Climbs up the model heirarchy until it finds the EML model
+       *
+       * @return {EML211 or false} - Returns the EML 211 Model or false if not found
+       */
+      getParentEML: function () {
+        var emlModel = this.get("parentModel"),
           tries = 0;
 
-      while (emlModel.type !== "EML" && tries < 6){
-        emlModel = emlModel.get("parentModel");
-        tries++;
-      }
+        while (emlModel.type !== "EML" && tries < 6) {
+          emlModel = emlModel.get("parentModel");
+          tries++;
+        }
 
-      if( emlModel && emlModel.type == "EML")
-        return emlModel;
-      else
-        return false;
-
-    }
-	});
+        if (emlModel && emlModel.type == "EML") return emlModel;
+        else return false;
+      },
+    },
+  );
 
-	return EMLTemporalCoverage;
+  return EMLTemporalCoverage;
 });
 
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLText.js.html b/docs/docs/src_js_models_metadata_eml211_EMLText.js.html index 530a6f128..de47ef158 100644 --- a/docs/docs/src_js_models_metadata_eml211_EMLText.js.html +++ b/docs/docs/src_js_models_metadata_eml211_EMLText.js.html @@ -44,269 +44,260 @@

Source: src/js/models/metadata/eml211/EMLText.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'],
-    function($, _, Backbone, DataONEObject) {
-
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+  $,
+  _,
+  Backbone,
+  DataONEObject,
+) {
   /**
-  * @class EMLText211
-  * @classdesc A model that represents the EML 2.1.1 Text module
-  * @classcategory Models/Metadata/EML211
-  * @extends Backbone.Model
-  */
+   * @class EMLText211
+   * @classdesc A model that represents the EML 2.1.1 Text module
+   * @classcategory Models/Metadata/EML211
+   * @extends Backbone.Model
+   */
   var EMLText = Backbone.Model.extend(
-    /** @lends EMLText211.prototype */{
-
-    type: "EMLText",
-
-    defaults: function(){
-      return {
-        objectXML: null,
-        objectDOM: null,
-        parentModel: null,
-        originalText: [],
-        text: [] //The text content
-      }
-    },
-
-    initialize: function(attributes){
-      var attributes = attributes || {}
-
-      if(attributes.objectDOM)
-        this.set(this.parse(attributes.objectDOM));
-
-      if(attributes.text) {
-        if (_.isArray(attributes.text)) {
-          this.text = attributes.text
-        } else {
-          this.text = [attributes.text]
+    /** @lends EMLText211.prototype */ {
+      type: "EMLText",
+
+      defaults: function () {
+        return {
+          objectXML: null,
+          objectDOM: null,
+          parentModel: null,
+          originalText: [],
+          text: [], //The text content
+        };
+      },
+
+      initialize: function (attributes) {
+        var attributes = attributes || {};
+
+        if (attributes.objectDOM) this.set(this.parse(attributes.objectDOM));
+
+        if (attributes.text) {
+          if (_.isArray(attributes.text)) {
+            this.text = attributes.text;
+          } else {
+            this.text = [attributes.text];
+          }
         }
-      }
-
-      this.on("change:text", this.trickleUpChange);
-    },
-
-    /**
-         * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
-         * Used during parse() and serialize()
-         */
-    nodeNameMap: function(){
-      return{
-
-      }
-    },
-
-    /**
-    * function setText
-    *
-    * @param text {string} - The text, usually taken directly from an HTML textarea
-    * value, to parse and set on this model
-    */
-    setText: function(text){
-
-      if( typeof text !== "string" )
-        return "";
-
-      let model = this;
-
-      require(["models/metadata/eml211/EML211"], function(EMLModel){
-        //Get the EML model and use the cleanXMLText function to clean up the text
-        text = EMLModel.prototype.cleanXMLText(text);
-
-        //Get the list of paragraphs - checking for carriage returns and line feeds
-        var paragraphsCR = text.split(String.fromCharCode(13));
-        var paragraphsLF = text.split(String.fromCharCode(10));
-
-        //Use the paragraph list that has the most
-        var paragraphs = (paragraphsCR > paragraphsLF)? paragraphsCR : paragraphsLF;
 
-        paragraphs = _.map(paragraphs, function(p){ return p.trim() });
-
-        model.set("text", paragraphs);
-      });
-
-    },
-
-    parse: function(objectDOM){
-      if(!objectDOM)
-        var objectDOM = this.get("objectDOM").cloneNode(true);
-
-      //Start a list of paragraphs
-      var paragraphs = [];
-
-      //Get all the child nodes of this text element
-      var $objectDOM = $(objectDOM);
-
-      // Save all the contained nodes as paragraphs
-      // ignore any nested formatting elements for now
-      //TODO: Support more detailed text formatting
-      if( $objectDOM.children().length ){
-
-        paragraphs = this.parseNestedElements($objectDOM);
-
-      }
-      else if( objectDOM.textContent ){
-        paragraphs[0] = objectDOM.textContent;
-      }
+        this.on("change:text", this.trickleUpChange);
+      },
+
+      /**
+       * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
+       * Used during parse() and serialize()
+       */
+      nodeNameMap: function () {
+        return {};
+      },
+
+      /**
+       * function setText
+       *
+       * @param text {string} - The text, usually taken directly from an HTML textarea
+       * value, to parse and set on this model
+       */
+      setText: function (text) {
+        if (typeof text !== "string") return "";
+
+        let model = this;
+
+        require(["models/metadata/eml211/EML211"], function (EMLModel) {
+          //Get the EML model and use the cleanXMLText function to clean up the text
+          text = EMLModel.prototype.cleanXMLText(text);
+
+          //Get the list of paragraphs - checking for carriage returns and line feeds
+          var paragraphsCR = text.split(String.fromCharCode(13));
+          var paragraphsLF = text.split(String.fromCharCode(10));
+
+          //Use the paragraph list that has the most
+          var paragraphs =
+            paragraphsCR > paragraphsLF ? paragraphsCR : paragraphsLF;
+
+          paragraphs = _.map(paragraphs, function (p) {
+            return p.trim();
+          });
+
+          model.set("text", paragraphs);
+        });
+      },
 
-      return {
-        text: paragraphs,
-        originalText: paragraphs.slice(0) //The slice function will effectively clone the array
-      }
-    },
+      parse: function (objectDOM) {
+        if (!objectDOM) var objectDOM = this.get("objectDOM").cloneNode(true);
 
-    parseNestedElements: function(nodeEl){
+        //Start a list of paragraphs
+        var paragraphs = [];
 
-      let children = $(nodeEl).children(),
-          paragraphs = [];
+        //Get all the child nodes of this text element
+        var $objectDOM = $(objectDOM);
 
-      children.each((i, childNode) => {
-        if( $(childNode).children().length ){
-          paragraphs = paragraphs.concat(this.parseNestedElements(childNode));
+        // Save all the contained nodes as paragraphs
+        // ignore any nested formatting elements for now
+        //TODO: Support more detailed text formatting
+        if ($objectDOM.children().length) {
+          paragraphs = this.parseNestedElements($objectDOM);
+        } else if (objectDOM.textContent) {
+          paragraphs[0] = objectDOM.textContent;
         }
-        else{
-          paragraphs = paragraphs.concat(this.parseParagraphs(childNode));
-        }
-      })
-
-      return paragraphs;
-    },
 
-    parseParagraphs: function(nodeEl){
-      if( nodeEl.textContent ){
+        return {
+          text: paragraphs,
+          originalText: paragraphs.slice(0), //The slice function will effectively clone the array
+        };
+      },
 
-        //Get the list of paragraphs - checking for carriage returns and line feeds
-        var paragraphsCR = nodeEl.textContent.split(String.fromCharCode(13));
-        var paragraphsLF = nodeEl.textContent.split(String.fromCharCode(10));
-
-        //Use the paragraph list that has the most
-        var paragraphs = (paragraphsCR > paragraphsLF)? paragraphsCR : paragraphsLF;
+      parseNestedElements: function (nodeEl) {
+        let children = $(nodeEl).children(),
+          paragraphs = [];
 
-        //Trim extra whitespace off each paragraph to get rid of the line break characters
-        paragraphs = _.map(paragraphs, function(text){
-          if(typeof text == "string")
-            return text.trim();
-          else
-            return text;
+        children.each((i, childNode) => {
+          if ($(childNode).children().length) {
+            paragraphs = paragraphs.concat(this.parseNestedElements(childNode));
+          } else {
+            paragraphs = paragraphs.concat(this.parseParagraphs(childNode));
+          }
         });
 
-        //Remove all falsey values - primarily empty strings
-        paragraphs = _.compact(paragraphs);
-
         return paragraphs;
+      },
 
-      }
-    },
-
-    serialize: function(){
-      var objectDOM = this.updateDOM(),
-        xmlString = objectDOM.outerHTML;
+      parseParagraphs: function (nodeEl) {
+        if (nodeEl.textContent) {
+          //Get the list of paragraphs - checking for carriage returns and line feeds
+          var paragraphsCR = nodeEl.textContent.split(String.fromCharCode(13));
+          var paragraphsLF = nodeEl.textContent.split(String.fromCharCode(10));
 
-      //Camel-case the XML
-        xmlString = this.formatXML(xmlString);
+          //Use the paragraph list that has the most
+          var paragraphs =
+            paragraphsCR > paragraphsLF ? paragraphsCR : paragraphsLF;
 
-        return xmlString;
-    },
+          //Trim extra whitespace off each paragraph to get rid of the line break characters
+          paragraphs = _.map(paragraphs, function (text) {
+            if (typeof text == "string") return text.trim();
+            else return text;
+          });
 
-    /**
-     * Makes a copy of the original XML DOM and updates it with the new values from the model.
-     */
-    updateDOM: function(){
-      var type = this.get("type") || this.get("parentAttribute") || 'text',
-          objectDOM = this.get("objectDOM") ? this.get("objectDOM").cloneNode(true) : document.createElement(type);
+          //Remove all falsey values - primarily empty strings
+          paragraphs = _.compact(paragraphs);
 
-      //FIrst check if any of the text in this model has changed since it was originally parsed
-      if( _.intersection(this.get("text"), this.get("originalText")).length == this.get("text").length
-       && this.get("objectDOM")){
-        return objectDOM;
-      }
+          return paragraphs;
+        }
+      },
 
-      //If there is no text, return an empty string
-      if( this.isEmpty() ){
-        return "";
-      }
+      serialize: function () {
+        var objectDOM = this.updateDOM(),
+          xmlString = objectDOM.outerHTML;
 
-      //Empty the DOM
-      $(objectDOM).empty();
+        //Camel-case the XML
+        xmlString = this.formatXML(xmlString);
 
-      //Format the text
-      var paragraphs = this.get("text");
-      _.each(paragraphs, function(p){
+        return xmlString;
+      },
+
+      /**
+       * Makes a copy of the original XML DOM and updates it with the new values from the model.
+       */
+      updateDOM: function () {
+        var type = this.get("type") || this.get("parentAttribute") || "text",
+          objectDOM = this.get("objectDOM")
+            ? this.get("objectDOM").cloneNode(true)
+            : document.createElement(type);
+
+        //FIrst check if any of the text in this model has changed since it was originally parsed
+        if (
+          _.intersection(this.get("text"), this.get("originalText")).length ==
+            this.get("text").length &&
+          this.get("objectDOM")
+        ) {
+          return objectDOM;
+        }
 
-        //If this paragraph text is a string, add a <para> node with that text
-        if( typeof p == "string" && p.trim().length )
-          $(objectDOM).append("<para>" + p + "</para>");
+        //If there is no text, return an empty string
+        if (this.isEmpty()) {
+          return "";
+        }
 
-      });
+        //Empty the DOM
+        $(objectDOM).empty();
 
-      return objectDOM;
-    },
+        //Format the text
+        var paragraphs = this.get("text");
+        _.each(paragraphs, function (p) {
+          //If this paragraph text is a string, add a <para> node with that text
+          if (typeof p == "string" && p.trim().length)
+            $(objectDOM).append("<para>" + p + "</para>");
+        });
 
-    /**
-    * Climbs up the model heirarchy until it finds the EML model
-    *
-    * @return {EML211|false} - Returns the EML 211 Model or false if not found
-    */
-    getParentEML: function(){
-      var emlModel = this.get("parentModel"),
+        return objectDOM;
+      },
+
+      /**
+       * Climbs up the model heirarchy until it finds the EML model
+       *
+       * @return {EML211|false} - Returns the EML 211 Model or false if not found
+       */
+      getParentEML: function () {
+        var emlModel = this.get("parentModel"),
           tries = 0;
 
-      while (emlModel.type !== "EML" && tries < 6){
-        emlModel = emlModel.get("parentModel");
-        tries++;
-      }
+        while (emlModel.type !== "EML" && tries < 6) {
+          emlModel = emlModel.get("parentModel");
+          tries++;
+        }
 
-      if( emlModel && emlModel.type == "EML")
-        return emlModel;
-      else
-        return false;
+        if (emlModel && emlModel.type == "EML") return emlModel;
+        else return false;
+      },
 
-    },
+      trickleUpChange: function () {
+        if (
+          MetacatUI.rootDataPackage &&
+          MetacatUI.rootDataPackage.packageModel
+        ) {
+          MetacatUI.rootDataPackage.packageModel.set("changed", true);
+        }
+      },
 
-    trickleUpChange: function(){
-      if( MetacatUI.rootDataPackage && MetacatUI.rootDataPackage.packageModel ){
-        MetacatUI.rootDataPackage.packageModel.set("changed", true);
-      }
-    },
+      formatXML: function (xmlString) {
+        return DataONEObject.prototype.formatXML.call(this, xmlString);
+      },
 
-    formatXML: function(xmlString){
-      return DataONEObject.prototype.formatXML.call(this, xmlString);
-    },
+      isEmpty: function () {
+        //If the text is an empty array, this is empty
+        if (Array.isArray(this.get("text")) && this.get("text").length == 0) {
+          return true;
+        }
+        //If the text is a falsey value, it is empty
+        else if (!this.get("text")) {
+          return true;
+        }
 
-    isEmpty: function() {
+        //Iterate over each paragraph in the text array and check if it's an empty string
+        for (var i = 0; i < this.get("text").length; i++) {
+          if (this.get("text")[i].trim().length > 0) return false;
+        }
 
-      //If the text is an empty array, this is empty
-      if( Array.isArray(this.get("text")) && this.get("text").length == 0 ){
-        return true;
-      }
-      //If the text is a falsey value, it is empty
-      else if( !this.get("text") ){
         return true;
-      }
+      },
 
-      //Iterate over each paragraph in the text array and check if it's an empty string
-      for (var i = 0; i < this.get('text').length; i++) {
-        if (this.get('text')[i].trim().length > 0)
-          return false;
-      }
-
-      return true;
-    },
+      /**
+       * Returns the EML Text paragraphs as a string, with each paragraph on a new line.
+       * @returns {string}
+       */
+      toString: function () {
+        var value = [];
 
-    /**
-    * Returns the EML Text paragraphs as a string, with each paragraph on a new line.
-    * @returns {string}
-    */
-    toString: function() {
-      var value = [];
-
-      if (_.isArray(this.get('text'))) {
-        value = this.get('text');
-      }
+        if (_.isArray(this.get("text"))) {
+          value = this.get("text");
+        }
 
-      return value.join('\n\n');
-    }
-  });
+        return value.join("\n\n");
+      },
+    },
+  );
 
   return EMLText;
 });
diff --git a/docs/docs/src_js_models_metadata_eml211_EMLUnit.js.html b/docs/docs/src_js_models_metadata_eml211_EMLUnit.js.html
index 1320433cb..b7048dfeb 100644
--- a/docs/docs/src_js_models_metadata_eml211_EMLUnit.js.html
+++ b/docs/docs/src_js_models_metadata_eml211_EMLUnit.js.html
@@ -46,45 +46,43 @@ 

Source: src/js/models/metadata/eml211/EMLUnit.js

"use strict";
 
-define(["jquery", "underscore", "backbone"], function($, _, Backbone) {
-
-    /**
-     * @class EMLUnit
-     * @classdesc An EMLUnit represents a single unit defined in the EML Unit Dictionary
-     * @classcategory Models/Metadata/EML211
-     * @extends Backbone.Model
-     */
-    var EMLUnit = Backbone.Model.extend(
-      /** @lends EMLUnit.prototype */{
-
-        /* The default unit fields */
-        defaults: function() {
-            return {
-                /* With X2JS, attributes are prefixed with _ */
-                _id: null,
-                _name: null,
-                _parentSI: null,
-                _multiplierToSI: null,
-                _abbreviation: null,
-                _unitType: null,
-                /* Child elements are not */
-                description: null,
-            };
-        },
-
-        /* Constructs a new instance */
-        initialize: function(attrs, options) {
-        },
-
-        /* No op - Units are read only */
-        save: function() {
-            console.log("EMLUnit is read only. Not implemented.");
-
-            return false;
-        }
-    });
-
-    return EMLUnit;
+define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @class EMLUnit
+   * @classdesc An EMLUnit represents a single unit defined in the EML Unit Dictionary
+   * @classcategory Models/Metadata/EML211
+   * @extends Backbone.Model
+   */
+  var EMLUnit = Backbone.Model.extend(
+    /** @lends EMLUnit.prototype */ {
+      /* The default unit fields */
+      defaults: function () {
+        return {
+          /* With X2JS, attributes are prefixed with _ */
+          _id: null,
+          _name: null,
+          _parentSI: null,
+          _multiplierToSI: null,
+          _abbreviation: null,
+          _unitType: null,
+          /* Child elements are not */
+          description: null,
+        };
+      },
+
+      /* Constructs a new instance */
+      initialize: function (attrs, options) {},
+
+      /* No op - Units are read only */
+      save: function () {
+        console.log("EMLUnit is read only. Not implemented.");
+
+        return false;
+      },
+    },
+  );
+
+  return EMLUnit;
 });
 
diff --git a/docs/docs/src_js_models_metadata_eml220_EMLText.js.html b/docs/docs/src_js_models_metadata_eml220_EMLText.js.html index f1f4ff96e..16499d8bf 100644 --- a/docs/docs/src_js_models_metadata_eml220_EMLText.js.html +++ b/docs/docs/src_js_models_metadata_eml220_EMLText.js.html @@ -44,150 +44,148 @@

Source: src/js/models/metadata/eml220/EMLText.js

-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/metadata/eml211/EMLText',
-        "text!templates/portals/editor/MarkdownExample.md"],
-    function($, _, Backbone, EMLText211, MarkdownExample) {
-
-      /**
-      * @class EMLText
-      * @classdesc A model that represents the EML 2.2.0 Text module
-      * @classcategory Models/Metadata/EML220
-      * @extends EMLText211
-      */
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/metadata/eml211/EMLText",
+  "text!templates/portals/editor/MarkdownExample.md",
+], function ($, _, Backbone, EMLText211, MarkdownExample) {
+  /**
+   * @class EMLText
+   * @classdesc A model that represents the EML 2.2.0 Text module
+   * @classcategory Models/Metadata/EML220
+   * @extends EMLText211
+   */
   var EMLText = EMLText211.extend(
-    /** @lends EMLText.prototype */{
-
-    defaults: function(){
-      return _.extend(EMLText211.prototype.defaults(), {
-        markdown: null,
-        markdownExample: MarkdownExample
-      });
-    },
+    /** @lends EMLText.prototype */ {
+      defaults: function () {
+        return _.extend(EMLText211.prototype.defaults(), {
+          markdown: null,
+          markdownExample: MarkdownExample,
+        });
+      },
 
-    /**
-    * Parses the XML objectDOM into a JSON object to be set on the model.
-    * If this EMLText element contains markdown, then parse it. Otherwise, use
-    * the EMLText 211 parse() method.
-    *
-    * @param {Element} objectDOM - XML Element to parse
-    * @return {JSON} The literal object to be set on the model later
-    */
-    parse: function(objectDOM){
-      if(!objectDOM)
-        var objectDOM = this.get("objectDOM").cloneNode(true);
-
-      // Get the markdown elements inside this EMLText element
-      var markdownElements = $(objectDOM).children("markdown"),
+      /**
+       * Parses the XML objectDOM into a JSON object to be set on the model.
+       * If this EMLText element contains markdown, then parse it. Otherwise, use
+       * the EMLText 211 parse() method.
+       *
+       * @param {Element} objectDOM - XML Element to parse
+       * @return {JSON} The literal object to be set on the model later
+       */
+      parse: function (objectDOM) {
+        if (!objectDOM) var objectDOM = this.get("objectDOM").cloneNode(true);
+
+        // Get the markdown elements inside this EMLText element
+        var markdownElements = $(objectDOM).children("markdown"),
           modelJSON = {};
 
-      //Grab the contents of each markdown element and add it to the JSON
-      if( markdownElements.length ){
+        //Grab the contents of each markdown element and add it to the JSON
+        if (markdownElements.length) {
+          modelJSON.markdown = "";
+
+          //Get the text content of the markdown element
+          _.each(markdownElements, function (markdownElement) {
+            // Concatenate markdown elements with a space.
+            if (modelJSON.markdown === "") {
+              modelJSON.markdown = markdownElement.textContent;
+            } else {
+              modelJSON.markdown += " " + markdownElement.textContent;
+            }
+          });
+
+          //Return the JSON
+          return modelJSON;
+        }
+        //If there is no markdown, parse as the same as EML 2.1.1
+        else {
+          return EMLText211.prototype.parse(objectDOM);
+        }
+      },
 
-        modelJSON.markdown = "";
+      /**
+       * Makes a copy of the original XML DOM and updates it with the new values from the model
+       *
+       * @param {string} textType - a string indicating the name for the outer xml element (i.e. content). Used in case there is no exisiting xmlDOM.
+       * @return {XMLElement}
+       */
+      updateDOM: function (textType) {
+        var markdown = this.get("markdown");
+
+        //If there is no markdown, parse as the same as EML 2.1.1
+        if (!markdown) {
+          return EMLText211.prototype.updateDOM.call(this);
+        } else {
+          var objectDOM = this.get("objectDOM");
 
-        //Get the text content of the markdown element
-        _.each(markdownElements, function(markdownElement){
-          // Concatenate markdown elements with a space.
-          if(modelJSON.markdown === ""){
-            modelJSON.markdown = markdownElement.textContent;
+          if (objectDOM) {
+            objectDOM = objectDOM.cloneNode(true);
+            $(objectDOM).empty();
           } else {
-            modelJSON.markdown += " " + markdownElement.textContent;
+            // create an XML section element from scratch
+            var xmlText = "<" + textType + "></" + textType + ">",
+              objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
+              objectDOM = $(objectDOM).children()[0];
           }
-        });
-
-        //Return the JSON
-        return modelJSON;
-      }
-      //If there is no markdown, parse as the same as EML 2.1.1
-      else{
-        return EMLText211.prototype.parse(objectDOM);
-      }
-
-    },
-
-    /**
-     * Makes a copy of the original XML DOM and updates it with the new values from the model
-     *
-     * @param {string} textType - a string indicating the name for the outer xml element (i.e. content). Used in case there is no exisiting xmlDOM.
-     * @return {XMLElement}
-     */
-    updateDOM: function(textType){
 
-      var markdown = this.get("markdown");
-
-      //If there is no markdown, parse as the same as EML 2.1.1
-      if(!markdown){
-        return EMLText211.prototype.updateDOM.call(this);
-      }
-      else{
+          // There could be multiple markdown elements, or markdown could be a string
+          if (typeof markdown == "string") {
+            markdown = [markdown];
+          }
 
-        var objectDOM = this.get("objectDOM");
+          _.each(
+            markdown,
+            function (markdownElement) {
+              // Create markdown element with content wrapped in CDATA tags
+              var markdownSerialized =
+                objectDOM.ownerDocument.createElement("markdown");
+              var cdataMarkdown =
+                objectDOM.ownerDocument.createCDATASection(markdownElement);
+              $(markdownSerialized).append(cdataMarkdown);
+              $(objectDOM).append(markdownSerialized);
+            },
+            this,
+          );
+
+          return objectDOM;
+        }
+      },
 
-        if (objectDOM) {
-          objectDOM = objectDOM.cloneNode(true);
-          $(objectDOM).empty();
+      /**
+       * @overrides EML211.setText
+       * If there is markdown, then the markdown gets updated. Otherwise, we default to the EML211.setText() functionality
+       */
+      setText: function (text) {
+        if (this.get("markdown")) {
+          this.set("markdown", text);
         } else {
-          // create an XML section element from scratch
-          var xmlText = "<" + textType + "></" + textType + ">",
-              objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
-              objectDOM = $(objectDOM).children()[0];
+          return EMLText211.prototype.setText.call(this, text);
         }
+      },
 
-        // There could be multiple markdown elements, or markdown could be a string
-        if(typeof markdown == "string"){
-          markdown = [markdown]
+      /**
+       * @overrides EML211.toString
+       * Returns the markdown string or EML Text paragraphs as a string, if there  is no  markdown. (For
+       * compatability with EML 2.1.1). Returns an empty string if neither are set on the model.
+       * @returns {string}
+       */
+      toString: function () {
+        try {
+          return (
+            this.get("markdown") ||
+            EMLText211.prototype.toString.call(this) ||
+            ""
+          );
+        } catch (e) {
+          console.error("Failed to convert EMLText toString(): ", e);
+          return "";
         }
-
-        _.each(markdown, function(markdownElement){
-          // Create markdown element with content wrapped in CDATA tags
-          var markdownSerialized = objectDOM.ownerDocument.createElement("markdown");
-          var cdataMarkdown = objectDOM.ownerDocument.createCDATASection(markdownElement);
-          $(markdownSerialized).append(cdataMarkdown);
-          $(objectDOM).append(markdownSerialized)
-        }, this);
-
-        return objectDOM
-
-      }
-
-    },
-
-    /**
-    * @overrides EML211.setText
-    * If there is markdown, then the markdown gets updated. Otherwise, we default to the EML211.setText() functionality
-    */
-    setText: function(text){
-
-      if( this.get("markdown") ){
-        this.set("markdown", text);
-      }
-      else{
-        return EMLText211.prototype.setText.call(this, text);
-      }
-
+      },
     },
-
-    /**
-    * @overrides EML211.toString
-    * Returns the markdown string or EML Text paragraphs as a string, if there  is no  markdown. (For
-    * compatability with EML 2.1.1). Returns an empty string if neither are set on the model.
-    * @returns {string}
-    */
-    toString: function(){
-      try{
-        return this.get("markdown") || EMLText211.prototype.toString.call(this) || "";
-      }
-      catch(e){
-        console.error("Failed to convert EMLText toString(): ", e);
-        return "";
-      }
-    }
-
-  });
+  );
 
   return EMLText;
-
 });
 
diff --git a/docs/docs/src_js_models_metadata_eml_EMLMethodStep.js.html b/docs/docs/src_js_models_metadata_eml_EMLMethodStep.js.html index e946b3c57..d34e7264b 100644 --- a/docs/docs/src_js_models_metadata_eml_EMLMethodStep.js.html +++ b/docs/docs/src_js_models_metadata_eml_EMLMethodStep.js.html @@ -44,21 +44,22 @@

Source: src/js/models/metadata/eml/EMLMethodStep.js

-
/* global define */
-var required = ['jquery',
-    'underscore',
-    'backbone',
-    'models/DataONEObject',
-    'models/metadata/eml220/EMLText']
-
-if( MetacatUI.appModel.get("customEMLMethods").length ){
+            
var required = [
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/DataONEObject",
+  "models/metadata/eml220/EMLText",
+];
+
+if (MetacatUI.appModel.get("customEMLMethods").length) {
   required.push("models/metadata/eml/EMLSpecializedText");
 }
 
-define(required,
-    function($, _, Backbone, DataONEObject, EMLText, EMLSpecializedText) {
-
-  /**
+define(
+  required,
+  function ($, _, Backbone, DataONEObject, EMLText, EMLSpecializedText) {
+    /**
   * @class EMLMethodStep
   * @classdesc Represents the EML Method Steps. The methodStep field allows for repeated sets of
             elements that document a series of procedures followed to produce a
@@ -69,299 +70,311 @@ 

Source: src/js/models/metadata/eml/EMLMethodStep.js

* @extends Backbone.Model * @since 2.19.0 */ - var EMLMethodStep = Backbone.Model.extend( - /** @lends EMLMethodStep.prototype */{ + var EMLMethodStep = Backbone.Model.extend( + /** @lends EMLMethodStep.prototype */ { + /** + * Default attributes for EMLMethodSteps + * @returns {object} + * @property {string} objectXML The original XML snippet string from the EML XML + * @property {Element} objectDOM The original XML snippet as an Element + * @property {EMLText|EMLSpecializedText} description A textual description of this method step + * @property {string[]} instrumentation One or more instruments used for measurement and recording data + * @property {EMLMethodStep[]} subStep Nested additional method steps within this step. This is useful for hierarchical method descriptions. This is *not* fully supported in MetacatUI yet + * @property {string[]} customMethodID A unique identifier for this Custom Method Step type, which is defined in {@link AppConfig#customEMLMethods} + * @property {boolean} required If true, this method step is required in it's parent EML + */ + defaults: function () { + return { + objectXML: null, + objectDOM: null, + description: null, + instrumentation: [], + subStep: [], + customMethodID: "", + required: false, + }; + }, + + initialize: function (attributes) { + attributes = attributes || {}; + + if (attributes.objectDOM) { + this.set(this.parse(attributes.objectDOM)); + } else if (attributes.customMethodID) { + try { + let customMethodConfig = MetacatUI.appModel + .get("customEMLMethods") + .find((config) => config.id == attributes.customMethodID); + + this.set( + "description", + new EMLSpecializedText({ + type: "description", + title: customMethodConfig.titleOptions[0], + titleOptions: customMethodConfig.titleOptions, + }), + ); + } catch (e) { + console.error(e); + } + } else { + this.set( + "description", + new EMLText({ + type: "description", + }), + ); + } - /** - * Default attributes for EMLMethodSteps - * @returns {object} - * @property {string} objectXML The original XML snippet string from the EML XML - * @property {Element} objectDOM The original XML snippet as an Element - * @property {EMLText|EMLSpecializedText} description A textual description of this method step - * @property {string[]} instrumentation One or more instruments used for measurement and recording data - * @property {EMLMethodStep[]} subStep Nested additional method steps within this step. This is useful for hierarchical method descriptions. This is *not* fully supported in MetacatUI yet - * @property {string[]} customMethodID A unique identifier for this Custom Method Step type, which is defined in {@link AppConfig#customEMLMethods} - * @property {boolean} required If true, this method step is required in it's parent EML - */ - defaults: function(){ - return { - objectXML: null, - objectDOM: null, - description: null, - instrumentation: [], - subStep: [], - customMethodID: "", - required: false - } - }, - - initialize: function(attributes){ - attributes = attributes || {}; - - if(attributes.objectDOM){ - this.set(this.parse(attributes.objectDOM)); - } - else if(attributes.customMethodID){ - - try{ - let customMethodConfig = MetacatUI.appModel.get("customEMLMethods").find(config => config.id == attributes.customMethodID); - - this.set("description", new EMLSpecializedText({ - type: "description", - title: customMethodConfig.titleOptions[0], - titleOptions: customMethodConfig.titleOptions - })); - } - catch(e){ - console.error(e); - } - } - else{ - this.set("description", new EMLText({ - type: "description" - })) - } - - //Set the required attribute - if( typeof attributes.required == "boolean" ){ - this.set("required", attributes.required); - } - - //specific attributes to listen to - this.on("change:instrumentation", this.trickleUpChange); - - }, + //Set the required attribute + if (typeof attributes.required == "boolean") { + this.set("required", attributes.required); + } - /** - * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML). - * Used during parse() and serialize() - * @returns {object} - */ - nodeNameMap: function(){ - return { - "alternateidentifier" : "alternateIdentifier", - "methodstep" : "methodStep", - "substep" : "subStep", - "datasource" : "dataSource", - "referencedentityid" : "referencedEntityId", - "qualitycontrol" : "qualityControl", - "shortname" : "shortName" - } - }, - - parse: function(objectDOM) { - var modelJSON = {}; - - if (!objectDOM) var objectDOM = this.get("objectDOM"); - - let $objectDOM = $(objectDOM), - description = $objectDOM.children("description"); - - //Get the titles of all the custom method steps from the App Config - let customMethodOptions = MetacatUI.appModel.get("customEMLMethods"), - customMethodTitles = _.flatten(_.pluck(customMethodOptions, "titleOptions")), - isCustom = false; - - try{ - //If there is at least one custom method configured, check if this description is one - if( customMethodOptions && customMethodOptions.length ){ - let specializedTextAttr = EMLSpecializedText.prototype.parse(description[0]), - matchingCustomMethod = customMethodOptions.find(options => options.titleOptions.includes(specializedTextAttr.title)); - - if( matchingCustomMethod ){ - isCustom = true; - - //Use the EMLSpecializedText model for custom methods - modelJSON.description = new EMLSpecializedText({ + //specific attributes to listen to + this.on("change:instrumentation", this.trickleUpChange); + }, + + /** + * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML). + * Used during parse() and serialize() + * @returns {object} + */ + nodeNameMap: function () { + return { + alternateidentifier: "alternateIdentifier", + methodstep: "methodStep", + substep: "subStep", + datasource: "dataSource", + referencedentityid: "referencedEntityId", + qualitycontrol: "qualityControl", + shortname: "shortName", + }; + }, + + parse: function (objectDOM) { + var modelJSON = {}; + + if (!objectDOM) var objectDOM = this.get("objectDOM"); + + let $objectDOM = $(objectDOM), + description = $objectDOM.children("description"); + + //Get the titles of all the custom method steps from the App Config + let customMethodOptions = MetacatUI.appModel.get("customEMLMethods"), + customMethodTitles = _.flatten( + _.pluck(customMethodOptions, "titleOptions"), + ), + isCustom = false; + + try { + //If there is at least one custom method configured, check if this description is one + if (customMethodOptions && customMethodOptions.length) { + let specializedTextAttr = EMLSpecializedText.prototype.parse( + description[0], + ), + matchingCustomMethod = customMethodOptions.find((options) => + options.titleOptions.includes(specializedTextAttr.title), + ); + + if (matchingCustomMethod) { + isCustom = true; + + //Use the EMLSpecializedText model for custom methods + modelJSON.description = new EMLSpecializedText({ + objectDOM: description[0], + type: "description", + titleOptions: matchingCustomMethod.titleOptions, + parentModel: this, + }); + //Save the other configurations of this custom method to this EMLMethodStep + modelJSON.customMethodID = matchingCustomMethod.id; + modelJSON.required = matchingCustomMethod.required; + } + } + } catch (e) { + console.error(e); + } + + //Create a regular EMLText description for non-custom methods + if (!isCustom) { + modelJSON.description = new EMLText({ objectDOM: description[0], type: "description", - titleOptions: matchingCustomMethod.titleOptions, - parentModel: this - }); - //Save the other configurations of this custom method to this EMLMethodStep - modelJSON.customMethodID = matchingCustomMethod.id; - modelJSON.required = matchingCustomMethod.required; + parentModel: this, + }); } - } - } - catch(e){ - console.error(e); - } - - //Create a regular EMLText description for non-custom methods - if( !isCustom ){ - modelJSON.description = new EMLText({ objectDOM: description[0], type: "description", parentModel: this }); - } - - //Parse the instrumentation - modelJSON.instrumentation = []; - $objectDOM.children("instrumentation").each((i, el) => { - modelJSON.instrumentation.push(el.textContent); - }); - /** @todo: Support parsing subSteps */ + //Parse the instrumentation + modelJSON.instrumentation = []; + $objectDOM.children("instrumentation").each((i, el) => { + modelJSON.instrumentation.push(el.textContent); + }); - return modelJSON; - }, + /** @todo: Support parsing subSteps */ - serialize: function(){ - var objectDOM = this.updateDOM(); + return modelJSON; + }, - if(!objectDOM) - return ""; + serialize: function () { + var objectDOM = this.updateDOM(); - var xmlString = objectDOM.outerHTML; + if (!objectDOM) return ""; - //Camel-case the XML - xmlString = this.formatXML(xmlString); + var xmlString = objectDOM.outerHTML; - return xmlString; - }, + //Camel-case the XML + xmlString = this.formatXML(xmlString); - /** - * Makes a copy of the original XML DOM and updates it with the new values from the model. - */ - updateDOM: function(){ - - //Return nothing if this model has only the default values - if( this.isEmpty() ){ - return; - } - - try{ - var objectDOM; - - if (this.get("objectDOM")) { - objectDOM = this.get("objectDOM").cloneNode(true); - } else { - objectDOM = $(document.createElement("methodstep")); - } - - let $objectDOM = $(objectDOM); + return xmlString; + }, - //Update the description - let description = this.get("description"); - if(description){ - let updatedDescription = description.updateDOM(); - - //Descriptions are required for method steps, so if updating the DOM didn't work, don't serialize this method step. - if( !updatedDescription ){ + /** + * Makes a copy of the original XML DOM and updates it with the new values from the model. + */ + updateDOM: function () { + //Return nothing if this model has only the default values + if (this.isEmpty()) { return; } - //Add the description to the method step - let existingDesc = $objectDOM.children("description"); - if( existingDesc.length ){ - existingDesc.replaceWith(updatedDescription); - } - else{ - $objectDOM.append(updatedDescription); - } - } - - try{ - //Update the instrumentation - let instrumentation = this.get("instrumentation"); - $objectDOM.children("instrumentation").remove(); - - if(instrumentation && instrumentation.length){ - instrumentation.reverse().each(i => { - let updatedI = document.createElement("instrumentation"); - updatedI.textContent = i; - $objectDOM.children("description").after(updatedI); - }) - } - } - catch(e){ - console.error("Failed to serialize method step instrumentation, skipping. ", e); - } - - /** @todo: Update software and subSteps */ - - // Remove empty (zero-length or whitespace-only) nodes - $objectDOM.find("*").filter(function() { return $.trim(this.innerHTML) === ""; } ).remove(); - - return objectDOM; - } - catch(e){ - console.error("Failed to update the EMLMethodStep. Won't serialize. ", e); - return; - } - }, - - /** - * function isEmpty() - Will check if there are any values set on this model - * that are different than the default values and would be serialized to the EML. - * - * @return {boolean} - Returns true is this model is empty, false if not - */ - isEmpty: function(){ - - if( !this.get("description") || this.get("description").isEmpty() ){ - return true; - } - - }, - - /** - * Returns whether or not this Method Step is a custom one, which currently only applies to the description - * @returns {boolean} - */ - isCustom: function(){ - return this.get("description")? this.get("description").type == "EMLSpecializedText" : false; - }, - - /** - * Overloads Backbone.Model.validate() to check if this model has valid values set on it - * @extends Backbone.Model.validate - * @returns {object} - */ - validate: function(){ - - try{ - - let validationErrors = {} - - if( this.isCustom() && this.get("required") ){ - let desc = this.get("description"), - isMissing = false; - - //If there is a missing description, we need to show the required error - if( !desc ){ - isMissing = true; - } - else if( !desc.get("text") ){ - isMissing = true; + try { + var objectDOM; + + if (this.get("objectDOM")) { + objectDOM = this.get("objectDOM").cloneNode(true); + } else { + objectDOM = $(document.createElement("methodstep")); + } + + let $objectDOM = $(objectDOM); + + //Update the description + let description = this.get("description"); + if (description) { + let updatedDescription = description.updateDOM(); + + //Descriptions are required for method steps, so if updating the DOM didn't work, don't serialize this method step. + if (!updatedDescription) { + return; + } + + //Add the description to the method step + let existingDesc = $objectDOM.children("description"); + if (existingDesc.length) { + existingDesc.replaceWith(updatedDescription); + } else { + $objectDOM.append(updatedDescription); + } + } + + try { + //Update the instrumentation + let instrumentation = this.get("instrumentation"); + $objectDOM.children("instrumentation").remove(); + + if (instrumentation && instrumentation.length) { + instrumentation.reverse().each((i) => { + let updatedI = document.createElement("instrumentation"); + updatedI.textContent = i; + $objectDOM.children("description").after(updatedI); + }); + } + } catch (e) { + console.error( + "Failed to serialize method step instrumentation, skipping. ", + e, + ); + } + + /** @todo: Update software and subSteps */ + + // Remove empty (zero-length or whitespace-only) nodes + $objectDOM + .find("*") + .filter(function () { + return $.trim(this.innerHTML) === ""; + }) + .remove(); + + return objectDOM; + } catch (e) { + console.error( + "Failed to update the EMLMethodStep. Won't serialize. ", + e, + ); + return; } - else if( !_.compact(desc.get("text")).length ){ - isMissing = true; + }, + + /** + * function isEmpty() - Will check if there are any values set on this model + * that are different than the default values and would be serialized to the EML. + * + * @return {boolean} - Returns true is this model is empty, false if not + */ + isEmpty: function () { + if (!this.get("description") || this.get("description").isEmpty()) { + return true; } - - if( isMissing ){ - validationErrors.description = `${desc.get("title")} is required.` - return validationErrors; + }, + + /** + * Returns whether or not this Method Step is a custom one, which currently only applies to the description + * @returns {boolean} + */ + isCustom: function () { + return this.get("description") + ? this.get("description").type == "EMLSpecializedText" + : false; + }, + + /** + * Overloads Backbone.Model.validate() to check if this model has valid values set on it + * @extends Backbone.Model.validate + * @returns {object} + */ + validate: function () { + try { + let validationErrors = {}; + + if (this.isCustom() && this.get("required")) { + let desc = this.get("description"), + isMissing = false; + + //If there is a missing description, we need to show the required error + if (!desc) { + isMissing = true; + } else if (!desc.get("text")) { + isMissing = true; + } else if (!_.compact(desc.get("text")).length) { + isMissing = true; + } + + if (isMissing) { + validationErrors.description = `${desc.get("title")} is required.`; + return validationErrors; + } + } + } catch (e) { + console.error("Error while validating the Methods: ", e); + return false; } - } - - } - catch(e){ - console.error("Error while validating the Methods: ", e); - return false; - } - - }, + }, - trickleUpChange: function(){ - MetacatUI.rootDataPackage.packageModel.set("changed", true); - }, + trickleUpChange: function () { + MetacatUI.rootDataPackage.packageModel.set("changed", true); + }, - formatXML: function(xmlString){ - return DataONEObject.prototype.formatXML.call(this, xmlString); - } - }); + formatXML: function (xmlString) { + return DataONEObject.prototype.formatXML.call(this, xmlString); + }, + }, + ); - return EMLMethodStep; -}); + return EMLMethodStep; + }, +);
diff --git a/docs/docs/src_js_models_metadata_eml_EMLSpecializedText.js.html b/docs/docs/src_js_models_metadata_eml_EMLSpecializedText.js.html index ee4797465..268fa6e5c 100644 --- a/docs/docs/src_js_models_metadata_eml_EMLSpecializedText.js.html +++ b/docs/docs/src_js_models_metadata_eml_EMLSpecializedText.js.html @@ -44,148 +44,147 @@

Source: src/js/models/metadata/eml/EMLSpecializedText.js<
-
/* global define */
-define(['jquery', 'underscore', 'backbone', 'models/metadata/eml220/EMLText'],
-    function($, _, Backbone, EMLText) {
-
-      /**
-      * @class EMLSpecializedText
-      * @classdesc An EMLSpecializedText is an EML 2.2.0 Text module that is treated differently in
-      * the UI display. It is identified as "Specialized", by the title (either a `title` element in EMLText
-      * or a Markdown Header 1). For example, you may want to display a custom EML Method Step specifically about
-      * "Ethical Research Practices". An EMLSpecializedText would have a title of `Ethical Research Practices`, which
-      * would be serialized in the EML XML as a section title or markdown header.
-      * @classcategory Models/Metadata/EML
-      * @since 2.19.0
-      * @extends EMLText
-      */
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/metadata/eml220/EMLText",
+], function ($, _, Backbone, EMLText) {
+  /**
+   * @class EMLSpecializedText
+   * @classdesc An EMLSpecializedText is an EML 2.2.0 Text module that is treated differently in
+   * the UI display. It is identified as "Specialized", by the title (either a `title` element in EMLText
+   * or a Markdown Header 1). For example, you may want to display a custom EML Method Step specifically about
+   * "Ethical Research Practices". An EMLSpecializedText would have a title of `Ethical Research Practices`, which
+   * would be serialized in the EML XML as a section title or markdown header.
+   * @classcategory Models/Metadata/EML
+   * @since 2.19.0
+   * @extends EMLText
+   */
   var EMLSpecializedText = EMLText.extend(
-    /** @lends EMLSpecializedText.prototype */{
+    /** @lends EMLSpecializedText.prototype */ {
+      type: "EMLSpecializedText",
 
-    type: "EMLSpecializedText",
-
-    defaults: function(){
-      return _.extend(EMLText.prototype.defaults(), {
-        id: null,
-        title: "",
-        titleOptions: []
-      });
-    },
+      defaults: function () {
+        return _.extend(EMLText.prototype.defaults(), {
+          id: null,
+          title: "",
+          titleOptions: [],
+        });
+      },
 
-    /**
-    * Parses the XML objectDOM into a literal object to be set on the model.
-    * It uses the EMLText 220 parse() method first and then performs additional
-    * parsing for Specialized Texts. In particular, the first title in the text is
-    * sorted out and used to identify as a Specialized text.
-    *
-    * @param {Element} objectDOM - XML Element to parse
-    * @return {object} The literal object to be set on the model later
-    */
-    parse: function(objectDOM){
-
-      try{
-        if(!objectDOM)
-          var objectDOM = this.get("objectDOM").cloneNode(true);
-
-        //Use the parent EMLText model parse() method
-        let parsedAttributes = EMLText.prototype.parse.call(this, objectDOM);
-
-        try{
-          //Find all of the title nodes inside each section
-          let titleNodes = $(objectDOM).children("section").children("title");
-
-          //Get the title text from the first title node
-          if( titleNodes.length ){
-            let firstTitleNode = titleNodes[0];
-            if( firstTitleNode ){
-              //Save the title to the model attributes
-              parsedAttributes.title = firstTitleNode.text;
-              //Remove the title from the text attribute so that it doesn't get serialized twice
-              parsedAttributes.text = _.without(parsedAttributes.text, parsedAttributes.title);
-              parsedAttributes.originalText = _.without(parsedAttributes.originalText, parsedAttributes.title);
+      /**
+       * Parses the XML objectDOM into a literal object to be set on the model.
+       * It uses the EMLText 220 parse() method first and then performs additional
+       * parsing for Specialized Texts. In particular, the first title in the text is
+       * sorted out and used to identify as a Specialized text.
+       *
+       * @param {Element} objectDOM - XML Element to parse
+       * @return {object} The literal object to be set on the model later
+       */
+      parse: function (objectDOM) {
+        try {
+          if (!objectDOM) var objectDOM = this.get("objectDOM").cloneNode(true);
+
+          //Use the parent EMLText model parse() method
+          let parsedAttributes = EMLText.prototype.parse.call(this, objectDOM);
+
+          try {
+            //Find all of the title nodes inside each section
+            let titleNodes = $(objectDOM).children("section").children("title");
+
+            //Get the title text from the first title node
+            if (titleNodes.length) {
+              let firstTitleNode = titleNodes[0];
+              if (firstTitleNode) {
+                //Save the title to the model attributes
+                parsedAttributes.title = firstTitleNode.text;
+                //Remove the title from the text attribute so that it doesn't get serialized twice
+                parsedAttributes.text = _.without(
+                  parsedAttributes.text,
+                  parsedAttributes.title,
+                );
+                parsedAttributes.originalText = _.without(
+                  parsedAttributes.originalText,
+                  parsedAttributes.title,
+                );
+              }
+            } else if (parsedAttributes.markdown) {
+              /** @TODO: Support Markdown by looking for the first Header Level 1 (starting with #) */
             }
+          } catch (e) {
+            console.error(
+              "Failed to parse the Specialized part of the EMLSpecializedText: ",
+              e,
+            );
+          } finally {
+            return parsedAttributes;
           }
-          else if( parsedAttributes.markdown ){
-            /** @TODO: Support Markdown by looking for the first Header Level 1 (starting with #) */
-          }
-        }
-        catch(e){
-          console.error("Failed to parse the Specialized part of the EMLSpecializedText: ", e);
-        }
-        finally{
-          return parsedAttributes;
-        }
-
-      }
-      catch(e){
-        console.error("Failed to parse EMLSpecializedText: ", e);
-        return {}
-      }
-
-    },
-
-    /**
-     * Makes a copy of the original XML DOM and updates it with the new values from the model
-     *
-     * @param {string} textType - a string indicating the name for the outer xml element (i.e. content). Used in case there is no exisiting xmlDOM.
-     * @return {XMLElement}
-     */
-    updateDOM: function(textType){
-
-      try{
-
-        //First update the DOM using the inherited updateDOM() method
-        let updatedDOM = EMLText.prototype.updateDOM.call(this);
-        let title = this.get("title");
-
-        if(this.get("markdown")){
-          /** @todo Support EMLSpecializedText for Markdown */
+        } catch (e) {
+          console.error("Failed to parse EMLSpecializedText: ", e);
+          return {};
         }
-        else if( updatedDOM && title ){
+      },
 
-          //Get the section element
-          let sectionEl = $(updatedDOM).children("section");
+      /**
+       * Makes a copy of the original XML DOM and updates it with the new values from the model
+       *
+       * @param {string} textType - a string indicating the name for the outer xml element (i.e. content). Used in case there is no exisiting xmlDOM.
+       * @return {XMLElement}
+       */
+      updateDOM: function (textType) {
+        try {
+          //First update the DOM using the inherited updateDOM() method
+          let updatedDOM = EMLText.prototype.updateDOM.call(this);
+          let title = this.get("title");
+
+          if (this.get("markdown")) {
+            /** @todo Support EMLSpecializedText for Markdown */
+          } else if (updatedDOM && title) {
+            //Get the section element
+            let sectionEl = $(updatedDOM).children("section");
+
+            //If there isn't a selection Element, create one and wrap it around the paras
+            if (!sectionEl.length) {
+              let allParas = $(updatedDOM).find("para");
+              allParas.wrapAll("<section />");
+              sectionEl = $(updatedDOM).children("section");
+            }
 
-          //If there isn't a selection Element, create one and wrap it around the paras
-          if( !sectionEl.length ){
-            let allParas = $(updatedDOM).find("para");
-            allParas.wrapAll("<section />");
-            sectionEl = $(updatedDOM).children("section");
+            //Find the most up-to-date title from the AppConfig.
+            //The first title in the list gets used. All other titles in the list are
+            // considered legacy/alternative titles that may have been used in the past.
+            let titleOptions = this.get("titleOptions");
+            title = titleOptions.length ? titleOptions[0] : title;
+
+            //Get the title of the first section
+            let titleEl = sectionEl.children("title");
+            //If there isn't a title, create one
+            if (!titleEl.length) {
+              titleEl = $(document.createElement("title")).text(title);
+              sectionEl.prepend(titleEl);
+            }
+            //Otherwise update the title text content
+            else {
+              titleEl.text(title);
+            }
           }
 
-          //Find the most up-to-date title from the AppConfig.
-          //The first title in the list gets used. All other titles in the list are
-          // considered legacy/alternative titles that may have been used in the past.
-          let titleOptions = this.get("titleOptions");
-          title = titleOptions.length? titleOptions[0] : title;
-
-          //Get the title of the first section
-          let titleEl = sectionEl.children("title");
-          //If there isn't a title, create one
-          if( !titleEl.length ){
-            titleEl = $(document.createElement("title")).text(title);
-            sectionEl.prepend(titleEl);
-          }
-          //Otherwise update the title text content
-          else{
-            titleEl.text(title);
-          }
+          return updatedDOM;
+        } catch (e) {
+          console.error(
+            "Failed to serialize the EMLSpecializedText. Will proceed using the original EML snippet. ",
+            e,
+          );
+          return this.get("objectDOM")
+            ? this.get("objectDOM").cloneNode(true)
+            : "";
         }
-
-        return updatedDOM;
-
-      }
-      catch(e){
-        console.error("Failed to serialize the EMLSpecializedText. Will proceed using the original EML snippet. ", e);
-        return this.get("objectDOM")? this.get("objectDOM").cloneNode(true) : "";
-      }
-
-    }
-
-  });
+      },
+    },
+  );
 
   return EMLSpecializedText;
-
 });
 
diff --git a/docs/docs/src_js_models_portals_PortalImage.js.html b/docs/docs/src_js_models_portals_PortalImage.js.html index 66873a62e..df3271a84 100644 --- a/docs/docs/src_js_models_portals_PortalImage.js.html +++ b/docs/docs/src_js_models_portals_PortalImage.js.html @@ -44,316 +44,322 @@

Source: src/js/models/portals/PortalImage.js

-
/* global define */
-define(["jquery",
-        "underscore",
-        "backbone",
-        "models/DataONEObject"
-    ],
-    function($, _, Backbone, DataONEObject) {
-
+            
define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
+  $,
+  _,
+  Backbone,
+  DataONEObject,
+) {
+  /**
+   * @class PortalImage
+   * @classdesc A Portal Image model represents a single image used in a Portal
+   * @classcategory Models/Portals
+   * @extends Backbone.Model
+   */
+  var PortalImageModel = DataONEObject.extend(
+    /** @lends PortalImage.prototype */ {
       /**
-       * @class PortalImage
-       * @classdesc A Portal Image model represents a single image used in a Portal
-       * @classcategory Models/Portals
-       * @extends Backbone.Model
+       * @inheritdoc
        */
-      var PortalImageModel = DataONEObject.extend(
-        /** @lends PortalImage.prototype */{
-
-        /**
-        * @inheritdoc
-        */
-        type: "PortalImage",
-
-        defaults: function(){
-          return _.extend(DataONEObject.prototype.defaults(), {
-            identifier: "",
-            imageURL: "",
-            label: "",
-            associatedURL: "",
-            objectDOM: null,
-            nodeName: "image",
-            portalModel: null
-          });
-        },
-
-        initialize: function(attrs){
-
-          // Call the super class initialize function
-          DataONEObject.prototype.initialize.call(this, attrs);
-
-          // If the image model is initialized with an identifier but no image URL,
-          // create the full image URL
-          if(this.get("identifier") && !this.get("imageURL")){
-
-            var baseURL = this.getBaseURL(),
-                imageURL = baseURL + this.get("identifier");
-                this.set("imageURL", imageURL);
-          }
-
-        },
-
-        /**
-         * Parses an ImageType XML element from a portal document
-         *
-         *  @param {XMLElement} objectDOM - An ImageType XML element from a portal document
-         *  @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
-        */
-        parse: function(objectDOM){
+      type: "PortalImage",
+
+      defaults: function () {
+        return _.extend(DataONEObject.prototype.defaults(), {
+          identifier: "",
+          imageURL: "",
+          label: "",
+          associatedURL: "",
+          objectDOM: null,
+          nodeName: "image",
+          portalModel: null,
+        });
+      },
+
+      initialize: function (attrs) {
+        // Call the super class initialize function
+        DataONEObject.prototype.initialize.call(this, attrs);
+
+        // If the image model is initialized with an identifier but no image URL,
+        // create the full image URL
+        if (this.get("identifier") && !this.get("imageURL")) {
+          var baseURL = this.getBaseURL(),
+            imageURL = baseURL + this.get("identifier");
+          this.set("imageURL", imageURL);
+        }
+      },
 
-          if(!objectDOM){
-            objectDOM = this.get("objectDOM");
+      /**
+       * Parses an ImageType XML element from a portal document
+       *
+       *  @param {XMLElement} objectDOM - An ImageType XML element from a portal document
+       *  @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
+       */
+      parse: function (objectDOM) {
+        if (!objectDOM) {
+          objectDOM = this.get("objectDOM");
 
-            if(!objectDOM){
-              return {};
-            }
+          if (!objectDOM) {
+            return {};
           }
+        }
 
-          var portalModel = this.get("portalModel"),
-              $objectDOM = $(objectDOM),
-              modelJSON = {};
+        var portalModel = this.get("portalModel"),
+          $objectDOM = $(objectDOM),
+          modelJSON = {};
+
+        if (portalModel) {
+          modelJSON.datasource = portalModel.get("datasource");
+          modelJSON.submitter = portalModel.get("submitter");
+          modelJSON.rightsHolder = portalModel.get("rightsHolder");
+          modelJSON.originMemberNode = portalModel.get("originMemberNode");
+          modelJSON.authoritativeMemberNode = portalModel.get(
+            "authoritativeMemberNode",
+          );
+        }
 
-          if( portalModel ){
-            modelJSON.datasource = portalModel.get("datasource");
-            modelJSON.submitter  = portalModel.get("submitter");
-            modelJSON.rightsHolder = portalModel.get("rightsHolder");
-            modelJSON.originMemberNode = portalModel.get("originMemberNode");
-            modelJSON.authoritativeMemberNode = portalModel.get("authoritativeMemberNode");
-          }
+        modelJSON.nodeName = objectDOM.nodeName;
 
-          modelJSON.nodeName = objectDOM.nodeName;
+        //Parse all the simple string elements
+        modelJSON.label = $objectDOM.children("label").text();
+        modelJSON.associatedURL = $objectDOM.children("associatedURL").text();
 
-          //Parse all the simple string elements
-          modelJSON.label = $objectDOM.children("label").text();
-          modelJSON.associatedURL = $objectDOM.children("associatedURL").text();
+        // Parse the image URL or identifier
+        modelJSON.identifier = $objectDOM.children("identifier").text();
+        if (modelJSON.identifier) {
+          if (modelJSON.identifier.substring(0, 4) !== "http") {
+            var baseURL = this.getBaseURL();
+            modelJSON.imageURL = baseURL + modelJSON.identifier;
+          } else {
+            modelJSON.imageURL = modelJSON.identifier;
+          }
+        }
 
-          // Parse the image URL or identifier
-          modelJSON.identifier = $objectDOM.children("identifier").text();
-          if( modelJSON.identifier ){
-            if( modelJSON.identifier.substring(0, 4) !== "http" ){
+        return modelJSON;
+      },
 
-              var baseURL = this.getBaseURL();
-              modelJSON.imageURL = baseURL + modelJSON.identifier;
+      /**
+       * imageExists - Check if an image exists with the given
+       * url, or if no url provided, with the baseURL + identifier
+       *
+       * @param  {string} imageURL  The image URL to check
+       * @return {boolean}          Returns true if an HTTP request returns anything but 404
+       */
+      imageExists: function (imageURL) {
+        if (!imageURL) {
+          this.get("imageURL");
+        }
 
-            }
-            else{
-              modelJSON.imageURL = modelJSON.identifier;
-            }
-          }
+        if (!imageURL && this.get("identifier")) {
+          var baseURL = this.getBaseURL(),
+            imageURL = baseURL + this.get("identifier");
+        }
 
-          return modelJSON;
+        if (!imageURL) {
+          return false;
+        }
 
-        },
+        var http = new XMLHttpRequest();
+        http.open("HEAD", imageURL, false);
+        http.send();
 
-        /**
-         * imageExists - Check if an image exists with the given
-         * url, or if no url provided, with the baseURL + identifier
-         *
-         * @param  {string} imageURL  The image URL to check
-         * @return {boolean}          Returns true if an HTTP request returns anything but 404
-         */
-        imageExists: function (imageURL){
+        return http.status != 404;
+      },
 
-          if(!imageURL){
-            this.get("imageURL")
+      /**
+       * getBaseURL - Get the base URL to use with an image identifier
+       *
+       * @return {string}  The image base URL, or an empty string if not found
+       */
+      getBaseURL: function () {
+        var url = "",
+          portalModel = this.get("portalModel"),
+          // datasource = portalModel ? portalModel.get("datasource") : false;
+          datasource = this.get("datasource"),
+          datasource =
+            portalModel && !datasource
+              ? portalModel.get("datasource")
+              : datasource;
+
+        if (MetacatUI.appModel.get("isCN")) {
+          var sourceRepo;
+
+          //Use the object service URL from the origin MN/datasource
+          if (datasource) {
+            sourceRepo = MetacatUI.nodeModel.getMember(datasource);
           }
-
-          if(!imageURL && this.get("identifier")){
-            var baseURL = this.getBaseURL(),
-                imageURL = baseURL + this.get("identifier");
+          // Use the object service URL from the alt repo
+          if (!sourceRepo) {
+            sourceRepo = MetacatUI.appModel.getActiveAltRepo();
           }
-
-          if(!imageURL){
-            return false
+          if (sourceRepo) {
+            url = sourceRepo.objectServiceUrl;
           }
+        }
 
-          var http = new XMLHttpRequest();
-          http.open('HEAD', imageURL, false);
-          http.send();
-
-          return http.status != 404;
-
-        },
-
-        /**
-         * getBaseURL - Get the base URL to use with an image identifier
-         *
-         * @return {string}  The image base URL, or an empty string if not found
-         */
-        getBaseURL: function(){
-
-          var url = "",
-              portalModel = this.get("portalModel"),
-              // datasource = portalModel ? portalModel.get("datasource") : false;
-              datasource = this.get("datasource"),
-              datasource = (portalModel && !datasource) ? portalModel.get("datasource") : datasource;
-
-          if( MetacatUI.appModel.get("isCN") ){
-            var sourceRepo;
-
-            //Use the object service URL from the origin MN/datasource
-            if( datasource ){
-              sourceRepo = MetacatUI.nodeModel.getMember(datasource);
-            }
-            // Use the object service URL from the alt repo
-            if( !sourceRepo ){
-              sourceRepo = MetacatUI.appModel.getActiveAltRepo();
-            }
-            if( sourceRepo ){
-              url = sourceRepo.objectServiceUrl;
-            }
+        if (!url && datasource) {
+          var imageMN = _.findWhere(
+            MetacatUI.appModel.get("alternateRepositories"),
+            { identifier: datasource },
+          );
+          if (imageMN) {
+            url = imageMN.objectServiceUrl;
           }
+        }
 
-          if(!url && datasource){
-            var imageMN = _.findWhere(MetacatUI.appModel.get("alternateRepositories"), { identifier: datasource });
-            if(imageMN){
-              url = imageMN.objectServiceUrl;
-            }
-          }
+        //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel
+        if (!url) {
+          url =
+            MetacatUI.appModel.get("objectServiceUrl") ||
+            MetacatUI.appModel.get("resolveServiceUrl");
+        }
+        return url;
+      },
 
-          //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel
-          if( !url ){
-            url = MetacatUI.appModel.get("objectServiceUrl") || MetacatUI.appModel.get("resolveServiceUrl");
-          }
-          return url;
-        },
-
-        /**
-    		 * Makes a copy of the original XML DOM and updates it with the new values from the model
-         *
-         *  @return {XMLElement} An updated ImageType XML element from a portal document
-    		 */
-        updateDOM: function() {
-
-          //If there is no identifier, don't serialize anything
-          if( !this.get("identifier") ){
-            return "";
-          }
+      /**
+       * Makes a copy of the original XML DOM and updates it with the new values from the model
+       *
+       *  @return {XMLElement} An updated ImageType XML element from a portal document
+       */
+      updateDOM: function () {
+        //If there is no identifier, don't serialize anything
+        if (!this.get("identifier")) {
+          return "";
+        }
 
-          var objectDOM = this.get("objectDOM");
+        var objectDOM = this.get("objectDOM");
+
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+          $(objectDOM).empty();
+        } else {
+          // create an XML image element from scratch
+          var xmlText =
+              "<" + this.get("nodeName") + "></" + this.get("nodeName") + ">",
+            objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
+            objectDOM = $(objectDOM).children()[0];
+        }
 
-          if (objectDOM) {
-            objectDOM = objectDOM.cloneNode(true);
-            $(objectDOM).empty();
-          } else {
-            // create an XML image element from scratch
-            var xmlText = "<" + this.get("nodeName") + "></" + this.get("nodeName") + ">",
-                objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
-                objectDOM = $(objectDOM).children()[0];
-         }
-
-          // get new image data
-          var imageData = {
-            identifier: this.get("identifier"),
-            label: this.get("label"),
-            associatedURL: this.get("associatedURL")
+        // get new image data
+        var imageData = {
+          identifier: this.get("identifier"),
+          label: this.get("label"),
+          associatedURL: this.get("associatedURL"),
+        };
+
+        _.map(imageData, function (value, nodeName) {
+          // Don't serialize falsey values
+          if (value) {
+            // Make new sub-node
+            var imageSubnodeSerialized =
+              objectDOM.ownerDocument.createElement(nodeName);
+            $(imageSubnodeSerialized).text(value);
+            // Append new sub-node to objectDOM
+            $(objectDOM).append(imageSubnodeSerialized);
           }
+        });
 
-          _.map(imageData, function(value, nodeName){
-
-            // Don't serialize falsey values
-            if(value){
-              // Make new sub-node
-              var imageSubnodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
-              $(imageSubnodeSerialized).text(value);
-              // Append new sub-node to objectDOM
-              $(objectDOM).append(imageSubnodeSerialized);
-
-            }
-
-          });
-
-          return objectDOM;
-        },
-
-        /**
-         * Overrides the default Backbone.Model.validate.function() to
-         * check if this PortalImage model has all the required values necessary
-         * to save to the server.
-         *
-         * @return {Object} If there are errors, an object comprising error
-         *                   messages. If no errors, returns nothing.
-        */
-        validate: function(){
-          try {
-
-            var errors          = {},
-                requiredFields = MetacatUI.appModel.get("portalEditorRequiredFields"),
-                label           = this.get("label"),
-                url             = this.get("associatedURL"),
-                id              = this.get("identifier"),
-                genericLabels   = ["logo", "image"], // not set by the user
-                hasLabel        = (label && typeof label == "string" && !genericLabels.includes(label)) ? true : false,
-                hasURL          = (url && typeof url == "string") ? true : false,
-                hasId           = (id && typeof id == "string") ? true : false,
-                urlRegex        = new RegExp(/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/);
-
-            // If it's a logo, check whether it's a required image
-            if(this.get("nodeName") === "logo" && requiredFields.logo && !hasId){
-              errors["identifier"] = "An image is required."
-              return errors
-            }
-            // If it's a section image, check whether it's a required image
-            else if(this.get("nodeName") === "image" && requiredFields.sectionImage && !hasId){
-              errors["identifier"] = "An image is required."
-              return errors
-            }
-            // If none of the fields have values, the portalImage won't be serialized
-            else if(!hasId && !hasURL && !hasLabel){
-              return
-            }
-
-            // If the URL isn't a valid format, add an error message
-            if(hasURL && !urlRegex.test(url)){
-              errors["associatedURL"] = "Enter a valid URL."
-            }
-            //If the URL is valid, check if there is an http or https protocol
-            else if(hasURL && url.substring(0,4) != "http"){
-              //If not, add the https protocol
-              this.set("associatedURL", "https://" + url);
-            }
+        return objectDOM;
+      },
 
+      /**
+       * Overrides the default Backbone.Model.validate.function() to
+       * check if this PortalImage model has all the required values necessary
+       * to save to the server.
+       *
+       * @return {Object} If there are errors, an object comprising error
+       *                   messages. If no errors, returns nothing.
+       */
+      validate: function () {
+        try {
+          var errors = {},
+            requiredFields = MetacatUI.appModel.get(
+              "portalEditorRequiredFields",
+            ),
+            label = this.get("label"),
+            url = this.get("associatedURL"),
+            id = this.get("identifier"),
+            genericLabels = ["logo", "image"], // not set by the user
+            hasLabel =
+              label &&
+              typeof label == "string" &&
+              !genericLabels.includes(label)
+                ? true
+                : false,
+            hasURL = url && typeof url == "string" ? true : false,
+            hasId = id && typeof id == "string" ? true : false,
+            urlRegex = new RegExp(
+              /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/,
+            );
+
+          // If it's a logo, check whether it's a required image
+          if (
+            this.get("nodeName") === "logo" &&
+            requiredFields.logo &&
+            !hasId
+          ) {
+            errors["identifier"] = "An image is required.";
+            return errors;
+          }
+          // If it's a section image, check whether it's a required image
+          else if (
+            this.get("nodeName") === "image" &&
+            requiredFields.sectionImage &&
+            !hasId
+          ) {
+            errors["identifier"] = "An image is required.";
             return errors;
-
           }
-          catch(e){
-            console.error("Error validating a portal image. Error message:" + e);
+          // If none of the fields have values, the portalImage won't be serialized
+          else if (!hasId && !hasURL && !hasLabel) {
             return;
           }
-        },
-
-
-        /**
-         * isEmpty - Returns true if the PortalImage model has no label, no associatedURL, and no identifier
-         *
-         * @return {boolean}  true if the model is empty, false if it has at least a label, url, or id
-         */
-        isEmpty: function(){
-          return (
-            !this.get("label")          &&
-            !this.get("associatedURL")  &&
-            !this.get("identifier")
-          ) ;
-        },
-
-        /**
-        * Returns true if this PortalImage hasn't been saved to a Portal yet, so it is a new object.
-        * For now, all PortalImages will be considered new objects since we will not be performing updates on them.
-        * @return {boolean}
-        */
-        isNew: function(){
-          if( this.get("identifier") ){
-            return false;
+
+          // If the URL isn't a valid format, add an error message
+          if (hasURL && !urlRegex.test(url)) {
+            errors["associatedURL"] = "Enter a valid URL.";
           }
-          else{
-            return true;
+          //If the URL is valid, check if there is an http or https protocol
+          else if (hasURL && url.substring(0, 4) != "http") {
+            //If not, add the https protocol
+            this.set("associatedURL", "https://" + url);
           }
+
+          return errors;
+        } catch (e) {
+          console.error("Error validating a portal image. Error message:" + e);
+          return;
         }
+      },
 
-      });
+      /**
+       * isEmpty - Returns true if the PortalImage model has no label, no associatedURL, and no identifier
+       *
+       * @return {boolean}  true if the model is empty, false if it has at least a label, url, or id
+       */
+      isEmpty: function () {
+        return (
+          !this.get("label") &&
+          !this.get("associatedURL") &&
+          !this.get("identifier")
+        );
+      },
+
+      /**
+       * Returns true if this PortalImage hasn't been saved to a Portal yet, so it is a new object.
+       * For now, all PortalImages will be considered new objects since we will not be performing updates on them.
+       * @return {boolean}
+       */
+      isNew: function () {
+        if (this.get("identifier")) {
+          return false;
+        } else {
+          return true;
+        }
+      },
+    },
+  );
 
-      return PortalImageModel;
+  return PortalImageModel;
 });
 
diff --git a/docs/docs/src_js_models_portals_PortalModel.js.html b/docs/docs/src_js_models_portals_PortalModel.js.html index 12052f5e2..79bbd9c85 100644 --- a/docs/docs/src_js_models_portals_PortalModel.js.html +++ b/docs/docs/src_js_models_portals_PortalModel.js.html @@ -47,2175 +47,2282 @@

Source: src/js/models/portals/PortalModel.js

/**
  * @exports PortalModel
  */
-/* global define */
-define(["jquery",
-        "underscore",
-        "backbone",
-        "gmaps",
-        "uuid",
-        "collections/Filters",
-        "collections/SolrResults",
-        "models/filters/Filter",
-        "models/portals/PortalSectionModel",
-        "models/portals/PortalVizSectionModel",
-        "models/portals/PortalImage",
-        "models/metadata/eml211/EMLParty",
-        "models/metadata/eml220/EMLText",
-        "models/CollectionModel",
-        "models/Search",
-        "models/filters/FilterGroup",
-        "models/Map",
-    ],
-    function($, _, Backbone, gmaps, uuid, Filters, SolrResults, FilterModel,
-        PortalSectionModel, PortalVizSectionModel, PortalImage,
-        EMLParty, EMLText, CollectionModel, SearchModel, FilterGroup, MapModel ) {
-        /**
-         * @classdesc A PortalModel is a specialized collection that represents a portal,
-         * including the associated data, people, portal descriptions, results and
-         * visualizations.  It also includes settings for customized filtering of the
-         * associated data, and properties used to customized the map display and the
-         * overall branding of the portal.
-         *
-         * @class PortalModel
-         * @classcategory Models/Portals
-         * @extends CollectionModel
-         * @module models/PortalModel
-         * @name PortalModel
-         * @constructor
-        */
-        var PortalModel = CollectionModel.extend(
-            /** @lends PortalModel.prototype */{
-
-            /**
-            * The name of this type of model
-            * @type {string}
-            */
-            type: "Portal",
-
-            /**
-             * Overrides the default Backbone.Model.defaults() function to
-             * specify default attributes for the portal model
-            * @type {object}
-            */
-            defaults: function() {
-                return _.extend(CollectionModel.prototype.defaults(), {
-                    id: null,
-                    objectXML: null,
-                    formatId: MetacatUI.appModel.get("portalEditorSerializationFormat"),
-                    formatType: "METADATA",
-                    type: "portal",
-                    //Is true if the last fetch was sent with user credentials. False if not.
-                    fetchedWithAuth: null,
-                    logo: null,
-                    sections: [],
-                    associatedParties: [],
-                    acknowledgments: null,
-                    acknowledgmentsLogos: [],
-                    awards: [],
-                    checkedNodeLabels: false,
-                    labelDoubleChecked: false,
-                    literatureCited: [],
-                    filterGroups: [],
-                    createSeriesId: true, //If true, a seriesId will be created when this object is saved.
-                    // The portal document options may specify section to hide
-                    edit: false, // Set to true if this model is being used in a portal editor view
-                    hideMetrics: null,
-                    hideData: null,
-                    hideMembers: null,
-                    hideMap: null,
-                    // List of section labels indicating the order in which to display the sections.
-                    // Labels must exactly match the labels set on sections, or the values set on the
-                    // metricsLabel, dataLabel, and membersLabel options.
-                    pageOrder: null,
-                    //Options for the custom section labels
-                    //NOTE: This are not fully supported yet.
-                    metricsLabel: "Metrics",
-                    dataLabel: "Data",
-                    membersLabel: "Members",
-                    // Map options, as specified in the portal document options
-                    mapZoomLevel: 3,
-                    mapCenterLatitude: null,
-                    mapCenterLongitude: null,
-                    mapShapeHue: 200,
-                    // The MapModel
-                    mapModel: gmaps ? new MapModel() : null,
-                    optionNames: ["primaryColor", "secondaryColor", "accentColor",
-                            "mapZoomLevel", "mapCenterLatitude", "mapCenterLongitude",
-                            "mapShapeHue", "hideData", "hideMetrics", "hideMembers",
-                            "pageOrder", "layout", "theme"
-                    ],
-                    // Portal view colors, as specified in the portal document options
-                    primaryColor: MetacatUI.appModel.get("portalDefaults").primaryColor || "#006699",
-                    secondaryColor: MetacatUI.appModel.get("portalDefaults").secondaryColor || "#009299",
-                    accentColor: MetacatUI.appModel.get("portalDefaults").accentColor || "#f89406",
-                    primaryColorRGB: null,
-                    secondaryColorRGB: null,
-                    accentColorRGB: null,
-                    primaryColorTransparent: MetacatUI.appModel.get("portalDefaults").primaryColorTransparent || "rgba(0, 102, 153, .7)",
-                    secondaryColorTransparent: MetacatUI.appModel.get("portalDefaults").secondaryColorTransparent || "rgba(0, 146, 153, .7)",
-                    accentColorTransparent: MetacatUI.appModel.get("portalDefaults").accentColorTransparent || "rgba(248, 148, 6, .7)",
-                    theme: null,
-                    layout: null
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "gmaps",
+  "uuid",
+  "collections/Filters",
+  "collections/SolrResults",
+  "models/filters/Filter",
+  "models/portals/PortalSectionModel",
+  "models/portals/PortalVizSectionModel",
+  "models/portals/PortalImage",
+  "models/metadata/eml211/EMLParty",
+  "models/metadata/eml220/EMLText",
+  "models/CollectionModel",
+  "models/Search",
+  "models/filters/FilterGroup",
+  "models/Map",
+], function (
+  $,
+  _,
+  Backbone,
+  gmaps,
+  uuid,
+  Filters,
+  SolrResults,
+  FilterModel,
+  PortalSectionModel,
+  PortalVizSectionModel,
+  PortalImage,
+  EMLParty,
+  EMLText,
+  CollectionModel,
+  SearchModel,
+  FilterGroup,
+  MapModel,
+) {
+  /**
+   * @classdesc A PortalModel is a specialized collection that represents a portal,
+   * including the associated data, people, portal descriptions, results and
+   * visualizations.  It also includes settings for customized filtering of the
+   * associated data, and properties used to customized the map display and the
+   * overall branding of the portal.
+   *
+   * @class PortalModel
+   * @classcategory Models/Portals
+   * @extends CollectionModel
+   * @module models/PortalModel
+   * @name PortalModel
+   * @constructor
+   */
+  var PortalModel = CollectionModel.extend(
+    /** @lends PortalModel.prototype */ {
+      /**
+       * The name of this type of model
+       * @type {string}
+       */
+      type: "Portal",
+
+      /**
+       * Overrides the default Backbone.Model.defaults() function to
+       * specify default attributes for the portal model
+       * @type {object}
+       */
+      defaults: function () {
+        return _.extend(CollectionModel.prototype.defaults(), {
+          id: null,
+          objectXML: null,
+          formatId: MetacatUI.appModel.get("portalEditorSerializationFormat"),
+          formatType: "METADATA",
+          type: "portal",
+          //Is true if the last fetch was sent with user credentials. False if not.
+          fetchedWithAuth: null,
+          logo: null,
+          sections: [],
+          associatedParties: [],
+          acknowledgments: null,
+          acknowledgmentsLogos: [],
+          awards: [],
+          checkedNodeLabels: false,
+          labelDoubleChecked: false,
+          literatureCited: [],
+          filterGroups: [],
+          createSeriesId: true, //If true, a seriesId will be created when this object is saved.
+          // The portal document options may specify section to hide
+          edit: false, // Set to true if this model is being used in a portal editor view
+          hideMetrics: null,
+          hideData: null,
+          hideMembers: null,
+          hideMap: null,
+          // List of section labels indicating the order in which to display the sections.
+          // Labels must exactly match the labels set on sections, or the values set on the
+          // metricsLabel, dataLabel, and membersLabel options.
+          pageOrder: null,
+          //Options for the custom section labels
+          //NOTE: This are not fully supported yet.
+          metricsLabel: "Metrics",
+          dataLabel: "Data",
+          membersLabel: "Members",
+          // Map options, as specified in the portal document options
+          mapZoomLevel: 3,
+          mapCenterLatitude: null,
+          mapCenterLongitude: null,
+          mapShapeHue: 200,
+          // The MapModel
+          mapModel: gmaps ? new MapModel() : null,
+          optionNames: [
+            "primaryColor",
+            "secondaryColor",
+            "accentColor",
+            "mapZoomLevel",
+            "mapCenterLatitude",
+            "mapCenterLongitude",
+            "mapShapeHue",
+            "hideData",
+            "hideMetrics",
+            "hideMembers",
+            "pageOrder",
+            "layout",
+            "theme",
+          ],
+          // Portal view colors, as specified in the portal document options
+          primaryColor:
+            MetacatUI.appModel.get("portalDefaults").primaryColor || "#006699",
+          secondaryColor:
+            MetacatUI.appModel.get("portalDefaults").secondaryColor ||
+            "#009299",
+          accentColor:
+            MetacatUI.appModel.get("portalDefaults").accentColor || "#f89406",
+          primaryColorRGB: null,
+          secondaryColorRGB: null,
+          accentColorRGB: null,
+          primaryColorTransparent:
+            MetacatUI.appModel.get("portalDefaults").primaryColorTransparent ||
+            "rgba(0, 102, 153, .7)",
+          secondaryColorTransparent:
+            MetacatUI.appModel.get("portalDefaults")
+              .secondaryColorTransparent || "rgba(0, 146, 153, .7)",
+          accentColorTransparent:
+            MetacatUI.appModel.get("portalDefaults").accentColorTransparent ||
+            "rgba(248, 148, 6, .7)",
+          theme: null,
+          layout: null,
+        });
+      },
+
+      /**
+       * The default text to use for a new section label added by the user
+       * @type {string}
+       */
+      newSectionLabel: "Untitled",
+
+      /**
+       * Overrides the default Backbone.Model.initialize() function to
+       * provide some custom initialize options
+       *
+       * @param {} options -
+       */
+      initialize: function (attrs) {
+        //Call the super class initialize function
+        CollectionModel.prototype.initialize.call(this, attrs);
+
+        // Generate transparent colours from the primary, secondary, and accent colors
+        // TODO
+
+        if (attrs.isNew) {
+          this.set("synced", true);
+          //Create an isPartOf filter for this new Portal
+          this.addIsPartOfFilter();
+
+          var model = this;
+
+          // Insert new sections if any are set in the appModel
+
+          var portalDefaults = MetacatUI.appModel.get("portalDefaults"),
+            defaultSections = portalDefaults ? portalDefaults.sections : [];
+
+          if (
+            defaultSections &&
+            defaultSections.length &&
+            Array.isArray(defaultSections)
+          ) {
+            defaultSections.forEach(function (section, index) {
+              // If there is at least one section default set...
+              if (section.title || section.label) {
+                var newDefaultSection = new PortalSectionModel({
+                  title: section.title || "",
+                  label: section.label || this.newSectionLabel,
+                  // Set a default image on new markdown sections
+                  image: model.getRandomSectionImage(),
+                  portalModel: model,
                 });
-            },
-
-            /**
-             * The default text to use for a new section label added by the user
-             * @type {string}
-            */
-            newSectionLabel: "Untitled",
-
-            /**
-             * Overrides the default Backbone.Model.initialize() function to
-             * provide some custom initialize options
-             *
-             * @param {} options -
-            */
-            initialize: function(attrs) {
-
-              //Call the super class initialize function
-              CollectionModel.prototype.initialize.call(this, attrs);
-
-              // Generate transparent colours from the primary, secondary, and accent colors
-              // TODO
-
-              if( attrs.isNew ){
-                this.set("synced", true);
-                //Create an isPartOf filter for this new Portal
-                this.addIsPartOfFilter();
-
-                var model = this;
-
-                // Insert new sections if any are set in the appModel
-
-                var portalDefaults = MetacatUI.appModel.get("portalDefaults"),
-                    defaultSections = portalDefaults ? portalDefaults.sections : [];
-
-                if(defaultSections && defaultSections.length && Array.isArray(defaultSections)){
-                  defaultSections.forEach(function(section, index){
-                    // If there is at least one section default set...
-                    if(section.title || section.label){
-                      var newDefaultSection = new PortalSectionModel({
-                        title: section.title || "",
-                        label: section.label || this.newSectionLabel,
-                        // Set a default image on new markdown sections
-                        image: model.getRandomSectionImage(),
-                        portalModel: model
-                      });
-                      model.addSection(newDefaultSection);
-                    }
-                  });
-                }
+                model.addSection(newDefaultSection);
               }
-
-              // check for info received from Bookkeeper
-              if( MetacatUI.appModel.get("enableBookkeeperServices") ){
-
-                this.listenTo( MetacatUI.appUserModel, "change:dataoneSubscription", function(){
-                  if(MetacatUI.appUserModel.get("dataoneSubscription").isTrialing()) {
-                    this.setRandomLabel();
-                  }
-                });
-
-                //Fetch the user subscription info
-                MetacatUI.appUserModel.fetchSubscription();
+            });
+          }
+        }
+
+        // check for info received from Bookkeeper
+        if (MetacatUI.appModel.get("enableBookkeeperServices")) {
+          this.listenTo(
+            MetacatUI.appUserModel,
+            "change:dataoneSubscription",
+            function () {
+              if (
+                MetacatUI.appUserModel.get("dataoneSubscription").isTrialing()
+              ) {
+                this.setRandomLabel();
               }
-
-              // Cache this model for later use
-              this.cachePortal();
-
             },
+          );
+
+          //Fetch the user subscription info
+          MetacatUI.appUserModel.fetchSubscription();
+        }
+
+        // Cache this model for later use
+        this.cachePortal();
+      },
+
+      /**
+       * getRandomSectionImage - Using the list of image identifiers set
+       * in the app config, select an image to use for a portal section.
+       * The function will not return the same image until all the images
+       * have been returned at least once. If an image would return a 404
+       * error, it is skipped. If all images give 404s, an empty string
+       * is returned.
+       *
+       * @return {PortalImage}  A portal image model to use in a section model
+       */
+      getRandomSectionImage: function () {
+        // This variable will hold the section image to return, if any
+        var newSectionImage = "",
+          // The default portal values set in the config
+          portalDefaults = MetacatUI.appModel.get("portalDefaults"),
+          // Check if default images are set on the model already
+          defaultImageIds = this.get("defaultSectionImageIds"),
+          // Keep track of where we are in the list of default images,
+          // so there's not too much repetition
+          runningNumber = this.get("defaultImageRunningNumber") || 0;
+
+        // If none are set, get the configured default image IDs,
+        // shuffle them, and set them on the model.
+        if (!defaultImageIds || !defaultImageIds.length) {
+          // Get the list of default section image IDs from the appModel
+          defaultImageIds = portalDefaults
+            ? portalDefaults.sectionImageIdentifiers
+            : false;
+
+          // If some are configured...
+          if (defaultImageIds && defaultImageIds.length) {
+            // ...Shuffle the images...
+            for (let i = defaultImageIds.length - 1; i > 0; i--) {
+              let j = Math.floor(Math.random() * (i + 1));
+              [defaultImageIds[i], defaultImageIds[j]] = [
+                defaultImageIds[j],
+                defaultImageIds[i],
+              ];
+            }
+            // ... and save the shuffled list to the portal model
+            this.set("defaultSectionImageIds", defaultImageIds);
+          }
+        }
+
+        // Can't get a random image if none are configured
+        if (!defaultImageIds) {
+          console.log(
+            "Can't set a default image on new markdown sections because there are no default image IDs set. Check portalDefaults.sectionImageIdentifiers in the config file.",
+          );
+          return;
+        }
+
+        // Select one of the image IDs
+        if (defaultImageIds && defaultImageIds.length > 0) {
+          if (runningNumber >= defaultImageIds.length) {
+            runningNumber = 0;
+          }
+
+          // Go through the shuffled array of image IDs in order
+          for (i = runningNumber; i < defaultImageIds.length; i++) {
+            // Skip images that have already returned 404 errors
+            if (defaultImageIds[i] == "NOT FOUND") {
+              continue;
+            }
 
-            /**
-             * getRandomSectionImage - Using the list of image identifiers set
-             * in the app config, select an image to use for a portal section.
-             * The function will not return the same image until all the images
-             * have been returned at least once. If an image would return a 404
-             * error, it is skipped. If all images give 404s, an empty string
-             * is returned.
-             *
-             * @return {PortalImage}  A portal image model to use in a section model
-             */
-            getRandomSectionImage: function(){
-
-              // This variable will hold the section image to return, if any
-              var newSectionImage = "",
-                  // The default portal values set in the config
-                  portalDefaults = MetacatUI.appModel.get("portalDefaults"),
-                  // Check if default images are set on the model already
-                  defaultImageIds = this.get("defaultSectionImageIds"),
-                  // Keep track of where we are in the list of default images,
-                  // so there's not too much repetition
-                  runningNumber = this.get("defaultImageRunningNumber") || 0;
-
-              // If none are set, get the configured default image IDs,
-              // shuffle them, and set them on the model.
-              if(!defaultImageIds || !defaultImageIds.length){
-
-                // Get the list of default section image IDs from the appModel
-                defaultImageIds = portalDefaults ? portalDefaults.sectionImageIdentifiers : false;
-
-                // If some are configured...
-                if(defaultImageIds && defaultImageIds.length){
-                  // ...Shuffle the images...
-                  for (let i = defaultImageIds.length - 1; i > 0; i--) {
-                    let j = Math.floor(Math.random() * (i + 1));
-                    [defaultImageIds[i], defaultImageIds[j]] = [defaultImageIds[j], defaultImageIds[i]];
-                  }
-                  // ... and save the shuffled list to the portal model
-                  this.set("defaultSectionImageIds", defaultImageIds);
-                }
+            // Section images are PortalImage models
+            var newSectionImage = new PortalImage({
+              identifier: defaultImageIds[i],
+              portalModel: this.get("portalModel"),
+            });
+
+            // Skip adding an image if it doesn't exist given the identifer and baseUrl found in the image model
+            if (newSectionImage.imageExists()) {
+              break;
+              // If the image doesn't exist, mark it so we don't have to
+              // check again next time
+            } else {
+              defaultImageIds[i] = "NOT FOUND";
+              newSectionImage = "";
+            }
+          }
+        }
+
+        this.set("defaultImageRunningNumber", i + 1);
+        this.set("defaultSectionImageIds", defaultImageIds);
+
+        return newSectionImage;
+      },
+
+      /**
+       * Returns the portal URL
+       *
+       * @return {string} The portal URL
+       */
+      url: function () {
+        //Start the base URL string
+        // use the resolve service if there is no object service url
+        // (e.g. in DataONE theme)
+        var urlBase =
+          MetacatUI.appModel.get("objectServiceUrl") ||
+          MetacatUI.appModel.get("resolveServiceUrl");
+
+        //Get the active alternative repository, if one is configured
+        var activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
+
+        if (activeAltRepo) {
+          urlBase = activeAltRepo.objectServiceUrl;
+        }
+
+        //If this object is being updated, use the old pid in the URL
+        if (!this.isNew() && this.get("oldPid")) {
+          return urlBase + encodeURIComponent(this.get("oldPid"));
+        }
+        //If this object is new, use the new pid in the URL
+        else {
+          return (
+            urlBase + encodeURIComponent(this.get("seriesId") || this.get("id"))
+          );
+        }
+      },
+
+      /**
+       * Overrides the default Backbone.Model.fetch() function to provide some custom
+       * fetch options
+       * @param [options] {object} - Options for this fetch
+       * @property [options.objectOnly] {Boolean} - If true, only the object will be retrieved and not the system metadata
+       * @property [options.systemMetadataOnly] {Boolean} - If true, only the system metadata will be retrieved
+       * @return {XMLDocument} The XMLDocument returned from the fetch() AJAX call
+       */
+      fetch: function (options) {
+        if (!options) var options = {};
+        else var options = _.clone(options);
+
+        //If the seriesId has not been found yet, get it from Solr
+        if (!this.get("id") && !this.get("seriesId") && this.get("label")) {
+          this.once("change:seriesId", function () {
+            this.fetch(options);
+          });
+          this.once("latestVersionFound", function () {
+            this.fetch(options);
+          });
+
+          //Get the series ID of this object
+          this.getSeriesIdByLabel();
+
+          return;
+        }
+        //If we found the latest version in this pid version chain,
+        else if (this.get("id") && this.get("latestVersion")) {
+          //Set it as the id of this model
+          this.set("id", this.get("latestVersion"));
+
+          //Stop listening to the change of seriesId and the latest version found
+          this.stopListening("change:seriesId", this.fetch);
+          this.stopListening("latestVersionFound", this.fetch);
+        }
+
+        //If this MetacatUI instance is pointing to a CN, use the origin MN
+        // to fetch the Portal, if available as an alt repo.
+        if (MetacatUI.appModel.get("isCN") && this.get("datasource")) {
+          //Check if the origin MN (datasource) is an alt repo option
+          var altRepo = _.findWhere(
+            MetacatUI.appModel.get("alternateRepositories"),
+            { identifier: this.get("datasource") },
+          );
+
+          if (altRepo) {
+            //Set the origin MN (datasource) as the active alt repo
+            MetacatUI.appModel.set(
+              "activeAlternateRepositoryId",
+              this.get("datasource"),
+            );
+          }
+        }
+
+        //Fetch the system metadata
+        if (!options.objectOnly || options.systemMetadataOnly) {
+          this.fetchSystemMetadata();
+
+          if (options.systemMetadataOnly) {
+            return;
+          }
+        }
+
+        var requestSettings = {
+          dataType: "xml",
+          error: function (model, response) {
+            model.trigger("error", model, response);
+
+            if (response && response.status == 404) {
+              model.trigger("notFound");
+            }
+          },
+        };
+
+        //Save a boolean flag for whether or not this fetch was done with user authentication.
+        //This is helpful when the app is dealing with potentially private data
+        this.set("fetchedWithAuth", MetacatUI.appUserModel.get("loggedIn"));
+
+        // Add the user settings to the fetch settings
+        requestSettings = _.extend(
+          requestSettings,
+          MetacatUI.appUserModel.createAjaxSettings(),
+        );
+
+        // Call Backbone.Model.fetch()
+        return Backbone.Model.prototype.fetch.call(this, requestSettings);
+      },
+
+      /**
+       * Get the portal seriesId by searching for the portal by its label in Solr
+       */
+      getSeriesIdByLabel: function () {
+        //Exit if there is no portal name set
+        if (!this.get("label")) return;
+
+        var model = this;
+
+        //Start the base URL for the query service
+        var baseUrl = "";
+
+        try {
+          //If this app instance is pointing to the CN, find the Portal series ID on the MN
+          if (MetacatUI.appModel.get("alternateRepositories").length) {
+            //Get the array of possible authoritative MNs
+            var possibleAuthMNs = this.get("possibleAuthMNs");
+
+            //If there are no possible authoritative MNs, use the CN query service
+            if (!possibleAuthMNs.length) {
+              baseUrl = MetacatUI.appModel.get("queryServiceUrl");
+            } else {
+              baseUrl = possibleAuthMNs[0].queryServiceUrl;
+            }
+          } else {
+            //Get the query service URL
+            baseUrl = MetacatUI.appModel.get("queryServiceUrl");
+          }
+        } catch (e) {
+          console.error(
+            "Error in trying to determine the query service URL. Going to try to use the AppModel setting. ",
+            e,
+          );
+        } finally {
+          //Default to the query service URL configured in the AppModel, if one wasn't set earlier
+          if (!baseUrl) {
+            baseUrl = MetacatUI.appModel.get("queryServiceUrl");
+            //If there isn't a query service URL, trigger a "not found" error and exit
+            if (!baseUrl) {
+              this.trigger("notFound");
+              return;
+            }
+          }
+        }
+
+        var requestSettings = {
+          url:
+            baseUrl +
+            'q=label:"' +
+            this.get("label") +
+            '" OR ' +
+            'seriesId:"' +
+            this.get("label") +
+            '"' +
+            "&fl=seriesId,id,label,datasource" +
+            "&sort=dateUploaded%20asc" +
+            "&rows=1" +
+            "&wt=json",
+          dataType: "json",
+          error: function (response) {
+            model.trigger("error", model, response);
+
+            if (response.status == 404) {
+              model.trigger("notFound");
+            }
+          },
+          success: function (response) {
+            if (response.response.numFound > 0) {
+              //Set the label and datasource
+              model.set("label", response.response.docs[0].label);
+              model.set("datasource", response.response.docs[0].datasource);
+
+              //Save the seriesId, if one is found
+              if (response.response.docs[0].seriesId) {
+                model.set("seriesId", response.response.docs[0].seriesId);
               }
-
-              // Can't get a random image if none are configured
-              if(!defaultImageIds){
-                console.log("Can't set a default image on new markdown sections because there are no default image IDs set. Check portalDefaults.sectionImageIdentifiers in the config file.");
-                return
+              //If this portal doesn't have a seriesId,
+              //but id has been found
+              else if (response.response.docs[0].id) {
+                //Save the id
+                model.set("id", response.response.docs[0].id);
+
+                //Find the latest version in this version chain
+                model.findLatestVersion(response.response.docs[0].id);
               }
-
-              // Select one of the image IDs
-              if(defaultImageIds && defaultImageIds.length > 0){
-
-                if(runningNumber >= defaultImageIds.length){
-                  runningNumber = 0
-                }
-
-                // Go through the shuffled array of image IDs in order
-                for (i = runningNumber; i < defaultImageIds.length; i++) {
-
-                  // Skip images that have already returned 404 errors
-                  if(defaultImageIds[i] == "NOT FOUND"){
-                    continue;
-                  }
-
-                  // Section images are PortalImage models
-                  var newSectionImage = new PortalImage({
-                    identifier: defaultImageIds[i],
-                    portalModel: this.get("portalModel")
-                  });
-
-                  // Skip adding an image if it doesn't exist given the identifer and baseUrl found in the image model
-                  if(newSectionImage.imageExists()){
-                    break;
-                  // If the image doesn't exist, mark it so we don't have to
-                  // check again next time
-                  } else {
-                    defaultImageIds[i] = "NOT FOUND";
-                    newSectionImage = "";
-                  }
-                }
+              // if we don't have Id or SeriesId
+              else {
+                model.trigger("notFound");
               }
-
-              this.set("defaultImageRunningNumber", i + 1);
-              this.set("defaultSectionImageIds", defaultImageIds);
-
-              return newSectionImage
-            },
-
-            /**
-             * Returns the portal URL
-             *
-             * @return {string} The portal URL
-            */
-            url: function() {
-
-              //Start the base URL string
-              // use the resolve service if there is no object service url
-              // (e.g. in DataONE theme)
-              var urlBase = MetacatUI.appModel.get("objectServiceUrl") ||
-                MetacatUI.appModel.get("resolveServiceUrl");
-
-              //Get the active alternative repository, if one is configured
-              var activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
-
-              if( activeAltRepo ){
-                urlBase = activeAltRepo.objectServiceUrl;
+            } else {
+              var possibleAuthMNs = model.get("possibleAuthMNs");
+              if (possibleAuthMNs.length) {
+                //Remove the first MN from the array, since it didn't contain the Portal, so it's not the auth MN
+                possibleAuthMNs.shift();
               }
 
-              //If this object is being updated, use the old pid in the URL
-              if ( !this.isNew() && this.get("oldPid") ) {
-                return urlBase +
-                    encodeURIComponent(this.get("oldPid"));
+              //If there are no other possible auth MNs to check, trigger this Portal as Not Found.
+              if (possibleAuthMNs.length == 0 || !possibleAuthMNs) {
+                model.trigger("notFound");
               }
-              //If this object is new, use the new pid in the URL
+              //If there's more MNs to check, try again
               else {
-                return urlBase +
-                    encodeURIComponent(this.get("seriesId") || this.get("id"));
-              }
-            },
-
-            /**
-             * Overrides the default Backbone.Model.fetch() function to provide some custom
-             * fetch options
-             * @param [options] {object} - Options for this fetch
-             * @property [options.objectOnly] {Boolean} - If true, only the object will be retrieved and not the system metadata
-             * @property [options.systemMetadataOnly] {Boolean} - If true, only the system metadata will be retrieved
-             * @return {XMLDocument} The XMLDocument returned from the fetch() AJAX call
-            */
-            fetch: function(options) {
-
-              if ( ! options ) var options = {};
-              else var options = _.clone(options);
-
-              //If the seriesId has not been found yet, get it from Solr
-              if( !this.get("id") && !this.get("seriesId") && this.get("label") ){
-
-                this.once("change:seriesId", function(){
-                  this.fetch(options)
-                });
-                this.once("latestVersionFound", function(){
-                  this.fetch(options)
-                });
-
-                //Get the series ID of this object
-                this.getSeriesIdByLabel();
-
-                return;
+                model.getSeriesIdByLabel();
               }
-              //If we found the latest version in this pid version chain,
-              else if( this.get("id") && this.get("latestVersion") ){
-                //Set it as the id of this model
-                this.set("id", this.get("latestVersion"));
-
-                //Stop listening to the change of seriesId and the latest version found
-                this.stopListening("change:seriesId", this.fetch);
-                this.stopListening("latestVersionFound", this.fetch);
+            }
+          },
+        };
+
+        //Save a boolean flag for whether or not this fetch was done with user authentication.
+        //This is helpful when the app is dealing with potentially private data
+        this.set("fetchedWithAuth", MetacatUI.appUserModel.get("loggedIn"));
+
+        requestSettings = _.extend(
+          requestSettings,
+          MetacatUI.appUserModel.createAjaxSettings(),
+        );
+
+        $.ajax(requestSettings);
+      },
+
+      /**
+       * This function has been renamed `getSeriesIdByLabel` and may be removed in future releases.
+       * @deprecated This function has been renamed `getSeriesIdByLabel` and may be removed in future releases.
+       * @see PortalModel#getSeriesIdByLabel
+       */
+      getSeriesIdByName: function () {
+        this.getSeriesIdByLabel();
+      },
+
+      /**
+       * Overrides the default Backbone.Model.parse() function to parse the custom
+       * portal XML document
+       *
+       * @param {XMLDocument} response - The XMLDocument returned from the fetch() AJAX call
+       * @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
+       */
+      parse: function (response) {
+        //Start the empty JSON object
+        var modelJSON = {},
+          modelRef = this,
+          portalNode;
+
+        // Iterate over each root XML node to find the portal node
+        $(response)
+          .children()
+          .each(function (i, el) {
+            if (el.tagName.indexOf("portal") > -1) {
+              portalNode = el;
+              return false;
+            }
+          });
+
+        // If a portal XML node wasn't found, return an empty JSON object
+        if (typeof portalNode == "undefined" || !portalNode) {
+          return {};
+        }
+
+        // Parse the collection elements
+        modelJSON = this.parseCollectionXML(portalNode);
+
+        // Save the xml for serialize
+        modelJSON.objectXML = response;
+
+        // Parse the portal logo
+        var portLogo = $(portalNode).children("logo")[0];
+        if (portLogo) {
+          var portImageModel = new PortalImage({
+            objectDOM: portLogo,
+            portalModel: this,
+          });
+          portImageModel.set(portImageModel.parse());
+          modelJSON.logo = portImageModel;
+        }
+
+        // Parse acknowledgement logos into urls
+        var logos = $(portalNode).children("acknowledgmentsLogo");
+        modelJSON.acknowledgmentsLogos = [];
+        _.each(
+          logos,
+          function (logo, i) {
+            if (!logo) return;
+
+            var imageModel = new PortalImage({
+              objectDOM: logo,
+              portalModel: this,
+            });
+            imageModel.set(imageModel.parse());
+
+            if (imageModel.get("imageURL")) {
+              modelJSON.acknowledgmentsLogos.push(imageModel);
+            }
+          },
+          this,
+        );
+
+        // Parse the literature cited
+        // This will only work for bibtex at the moment
+        var bibtex = $(portalNode)
+          .children("literatureCited")
+          .children("bibtex");
+        if (bibtex.length > 0) {
+          modelJSON.literatureCited = this.parseTextNode(
+            portalNode,
+            "literatureCited",
+          );
+        }
+
+        // Parse the portal content sections
+        modelJSON.sections = [];
+        $(portalNode)
+          .children("section")
+          .each(function (i, section) {
+            //Get the section type, if there is one
+            var sectionTypeNode = $(section).find(
+                "optionName:contains(sectionType)",
+              ),
+              sectionType = "";
+
+            if (sectionTypeNode.length) {
+              var optionValueNode = sectionTypeNode
+                .first()
+                .siblings("optionValue");
+              if (optionValueNode.length) {
+                sectionType = optionValueNode[0].textContent;
               }
+            }
 
-              //If this MetacatUI instance is pointing to a CN, use the origin MN
-              // to fetch the Portal, if available as an alt repo.
-              if( MetacatUI.appModel.get("isCN") && this.get("datasource") ){
-                //Check if the origin MN (datasource) is an alt repo option
-                var altRepo = _.findWhere(MetacatUI.appModel.get("alternateRepositories"), { identifier: this.get("datasource") });
+            if (sectionType == "visualization") {
+              // Create a new PortalVizSectionModel
+              modelJSON.sections.push(
+                new PortalVizSectionModel({
+                  objectDOM: section,
+                  literatureCited: modelJSON.literatureCited,
+                }),
+              );
+            } else {
+              // Create a new PortalSectionModel
+              modelJSON.sections.push(
+                new PortalSectionModel({
+                  objectDOM: section,
+                  literatureCited: modelJSON.literatureCited,
+                  portalModel: modelRef,
+                }),
+              );
+            }
 
-                if( altRepo ){
-                  //Set the origin MN (datasource) as the active alt repo
-                  MetacatUI.appModel.set("activeAlternateRepositoryId", this.get("datasource"));
+            //Parse the PortalSectionModel
+            modelJSON.sections[i].set(modelJSON.sections[i].parse(section));
+          });
+
+        // Parse the EMLText elements
+        modelJSON.acknowledgments = this.parseEMLTextNode(
+          portalNode,
+          "acknowledgments",
+        );
+
+        // Parse the awards
+        modelJSON.awards = [];
+        var parse_it = this.parseTextNode;
+        $(portalNode)
+          .children("award")
+          .each(function (i, award) {
+            var award_parsed = {};
+            $(award)
+              .children()
+              .each(function (i, award_attr) {
+                if (award_attr.nodeName != "funderLogo") {
+                  // parse the text nodes
+                  award_parsed[award_attr.nodeName] = parse_it(
+                    award,
+                    award_attr.nodeName,
+                  );
+                } else {
+                  // parse funderLogo which is type ImageType
+                  var imageModel = new PortalImage({ objectDOM: award_attr });
+                  imageModel.set(imageModel.parse());
+                  award_parsed[award_attr.nodeName] = imageModel;
                 }
+              });
+            modelJSON.awards.push(award_parsed);
+          });
+
+        // Parse the associatedParties
+        modelJSON.associatedParties = [];
+        $(portalNode)
+          .children("associatedParty")
+          .each(function (i, associatedParty) {
+            modelJSON.associatedParties.push(
+              new EMLParty({
+                objectDOM: associatedParty,
+              }),
+            );
+          });
+
+        // Parse the options. Use children() and not find() because we only want
+        // option nodes that are direct children of the portal node. Option nodes
+        // can also be found within section nodes.
+        $(portalNode)
+          .children("option")
+          .each(function (i, option) {
+            var optionName = $(option).find("optionName")[0].textContent,
+              optionValue = $(option).find("optionValue")[0].textContent;
+
+            if (optionValue === "true") {
+              optionValue = true;
+            } else if (optionValue === "false") {
+              optionValue = false;
+            }
 
-              }
-
-              //Fetch the system metadata
-              if( !options.objectOnly || options.systemMetadataOnly ){
-                this.fetchSystemMetadata();
+            // TODO: keep a list of optionNames so that in the case of
+            // custom options, we can serialize them in serialize()
+            // otherwise it's not saved in the model which attributes
+            // are <option></option>s
+
+            // Convert the comma separated list of pages into an array
+            if (
+              optionName === "pageOrder" &&
+              optionValue &&
+              optionValue.length
+            ) {
+              optionValue = optionValue.split(",");
+            }
 
-                if( options.systemMetadataOnly ){
-                  return;
+            if (!_.has(modelJSON, optionName)) {
+              modelJSON[optionName] = optionValue;
+            }
+          });
+
+        // Convert all the hex colors to rgb
+        if (modelJSON.primaryColor) {
+          modelJSON.primaryColorRGB = this.hexToRGB(modelJSON.primaryColor);
+          modelJSON.primaryColorTransparent =
+            "rgba(" +
+            modelJSON.primaryColorRGB.r +
+            "," +
+            modelJSON.primaryColorRGB.g +
+            "," +
+            modelJSON.primaryColorRGB.b +
+            ", .7)";
+        }
+        if (modelJSON.secondaryColor) {
+          modelJSON.secondaryColorRGB = this.hexToRGB(modelJSON.secondaryColor);
+          modelJSON.secondaryColorTransparent =
+            "rgba(" +
+            modelJSON.secondaryColorRGB.r +
+            "," +
+            modelJSON.secondaryColorRGB.g +
+            "," +
+            modelJSON.secondaryColorRGB.b +
+            ", .5)";
+        }
+        if (modelJSON.accentColor) {
+          modelJSON.accentColorRGB = this.hexToRGB(modelJSON.accentColor);
+          modelJSON.accentColorTransparent =
+            "rgba(" +
+            modelJSON.accentColorRGB.r +
+            "," +
+            modelJSON.accentColorRGB.g +
+            "," +
+            modelJSON.accentColorRGB.b +
+            ", .5)";
+        }
+
+        if (gmaps) {
+          // Create a MapModel with all the map options
+          modelJSON.mapModel = new MapModel();
+          var mapOptions = modelJSON.mapModel.get("mapOptions");
+
+          if (modelJSON.mapZoomLevel) {
+            mapOptions.zoom = parseInt(modelJSON.mapZoomLevel);
+            mapOptions.minZoom = parseInt(modelJSON.mapZoomLevel);
+          }
+          if (
+            (modelJSON.mapCenterLatitude ||
+              modelJSON.mapCenterLatitude === 0) &&
+            (modelJSON.mapCenterLongitude || modelJSON.mapCenterLongitude === 0)
+          ) {
+            mapOptions.center = modelJSON.mapModel.createLatLng(
+              modelJSON.mapCenterLatitude,
+              modelJSON.mapCenterLongitude,
+            );
+          }
+          if (modelJSON.mapShapeHue) {
+            modelJSON.mapModel.set("tileHue", modelJSON.mapShapeHue);
+          }
+        }
+
+        // Parse the UIFilterGroups
+        modelJSON.filterGroups = [];
+        var allFilters = modelJSON.searchModel.get("filters");
+        $(portalNode)
+          .children("filterGroup")
+          .each(function (i, filterGroup) {
+            // Create a FilterGroup model
+            var filterGroupModel = new FilterGroup({
+              objectDOM: filterGroup,
+              isUIFilterType: true,
+            });
+            modelJSON.filterGroups.push(filterGroupModel);
+
+            // Add the Filters from this FilterGroup to the portal's Search model,
+            // unless this portal model is being edited. Then we only want the
+            // definition filters to be included in the search model.
+            if (!modelRef.get("edit")) {
+              allFilters.add(filterGroupModel.get("filters").models);
+            }
+          });
+
+        return modelJSON;
+      },
+
+      /**
+       * Parses the XML nodes that are of type EMLText
+       *
+       * @param {Element} parentNode - The XML Element that contains all the EMLText nodes
+       * @param {string} nodeName - The name of the XML node to parse
+       * @param {boolean} isMultiple - If true, parses the nodes into an array
+       * @return {(string|Array)} A string or array of strings comprising the text content
+       */
+      parseEMLTextNode: function (parentNode, nodeName, isMultiple) {
+        var node = $(parentNode).children(nodeName);
+
+        // If no matching nodes were found, return falsey values
+        if (!node || !node.length) {
+          // Return an empty array if the isMultiple flag is true
+          if (isMultiple) return [];
+          // Return null if the isMultiple flag is false
+          else return null;
+        }
+        // If exactly one node is found and we are only expecting one, return the text content
+        else if (node.length == 1 && !isMultiple) {
+          return new EMLText({
+            objectDOM: node[0],
+          });
+        } else {
+          // If more than one node is found, parse into an array
+          return _.map(node, function (node) {
+            return new EMLText({
+              objectDOM: node,
+            });
+          });
+        }
+      },
+
+      /**
+       * Sets the fileName attribute on this model using the portal label
+       * @override
+       */
+      setMissingFileName: function () {
+        var fileName = this.get("label");
+
+        if (!fileName) {
+          fileName = "portal.xml";
+        } else {
+          fileName = fileName.replace(/[^a-zA-Z0-9]/g, "_") + ".xml";
+        }
+
+        this.set("fileName", fileName);
+      },
+
+      /**
+       * @typedef {Object} PortalModel#rgb - An RGB color value
+       * @property {number} r - A value between 0 and 255 defining the intensity of red
+       * @property {number} g - A value between 0 and 255 defining the intensity of green
+       * @property {number} b - A value between 0 and 255 defining the intensity of blue
+       */
+
+      /**
+       * Converts hex color values to RGB
+       *
+       * @param {string} hex - a color in hexadecimal format
+       * @return {rgb} a color in RGB format
+       */
+      hexToRGB: function (hex) {
+        var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+        return result
+          ? {
+              r: parseInt(result[1], 16),
+              g: parseInt(result[2], 16),
+              b: parseInt(result[3], 16),
+            }
+          : null;
+      },
+
+      /**
+       * Finds the node in the given portal XML document afterwhich the
+       * given node type should be inserted
+       *
+       * @param {Element} portalNode - The portal element of an XML document
+       * @param {string} nodeName - The name of the node to be inserted
+       *                             into xml
+       * @return {(jQuery|boolean)} A jQuery object indicating a position,
+       *                            or false when nodeName is not in the
+       *                            portal schema
+       */
+      getXMLPosition: function (portalNode, nodeName) {
+        var nodeOrder = [
+          "label",
+          "name",
+          "description",
+          "definition",
+          "logo",
+          "section",
+          "associatedParty",
+          "acknowledgments",
+          "acknowledgmentsLogo",
+          "award",
+          "literatureCited",
+          "filterGroup",
+          "option",
+        ];
+
+        var position = _.indexOf(nodeOrder, nodeName);
+
+        // First check that nodeName is in the list of nodes
+        if (position == -1) {
+          return false;
+        }
+
+        // If there's already an occurence of nodeName...
+        if ($(portalNode).children(nodeName).length > 0) {
+          // ...insert it after the last occurence
+          return $(portalNode).children(nodeName).last();
+        } else {
+          // Go through each node in the node list and find the position
+          // after which this node will be inserted
+          for (var i = position - 1; i >= 0; i--) {
+            if ($(portalNode).children(nodeOrder[i]).length) {
+              return $(portalNode).children(nodeOrder[i]).last();
+            }
+          }
+        }
+
+        return false;
+      },
+
+      /**
+       * Retrieves the model attributes and serializes into portal XML,
+       * to produce the new or modified portal document.
+       *
+       * @return {string} - Returns the portal XML as a string.
+       */
+      serialize: function () {
+        try {
+          // So we can call getXMLPosition() from within if{}
+          var model = this;
+
+          var xmlDoc, portalNode, xmlString;
+
+          xmlDoc = this.get("objectXML");
+
+          // Check if there is a portal doc already
+          if (xmlDoc == null) {
+            // If not create one
+            xmlDoc = this.createXML();
+          } else {
+            // If yes, clone it
+            xmlDoc = xmlDoc.cloneNode(true);
+          }
+
+          // Iterate over each root XML node to find the portal node
+          $(xmlDoc)
+            .children()
+            .each(function (i, el) {
+              if (el.tagName.indexOf("portal") > -1) {
+                portalNode = el;
+              }
+            });
+
+          // Serialize the collection elements
+          // ("name", "label", "description", "definition")
+          portalNode = this.updateCollectionDOM(portalNode);
+          xmlDoc = portalNode.getRootNode();
+          var $portalNode = $(portalNode);
+
+          // Set formatID
+          this.set(
+            "formatId",
+            MetacatUI.appModel.get("portalEditorSerializationFormat") ||
+              "https://purl.dataone.org/portals-1.1.0",
+          );
+
+          /* ==== Serialize portal logo ==== */
+
+          // Remove node if it exists already
+          $(xmlDoc).find("logo").remove();
+
+          // Get new values
+          var logo = this.get("logo");
+
+          // Don't serialize falsey values or empty logos
+          if (logo && logo.get("identifier")) {
+            // Make new node
+            var logoSerialized = logo.updateDOM("logo");
+
+            //Add the logo node to the XMLDocument
+            xmlDoc.adoptNode(logoSerialized);
+
+            // Insert new node at correct position
+            var insertAfter = this.getXMLPosition(portalNode, "logo");
+            if (insertAfter) {
+              insertAfter.after(logoSerialized);
+            } else {
+              portalNode.appendChild(logoSerialized);
+            }
+          }
+
+          /* ==== Serialize acknowledgment logos ==== */
+
+          // Remove element if it exists already
+          $(xmlDoc).find("acknowledgmentsLogo").remove();
+
+          var acknowledgmentsLogos = this.get("acknowledgmentsLogos");
+
+          // Don't serialize falsey values
+          if (acknowledgmentsLogos) {
+            _.each(acknowledgmentsLogos, function (imageModel) {
+              // Don't serialize empty imageModels
+              if (
+                imageModel.get("identifier") ||
+                imageModel.get("label") ||
+                imageModel.get("associatedURL")
+              ) {
+                var ackLogosSerialized = imageModel.updateDOM();
+
+                //Add the logo node to the XMLDocument
+                xmlDoc.adoptNode(ackLogosSerialized);
+
+                // Insert new node at correct position
+                var insertAfter = model.getXMLPosition(
+                  portalNode,
+                  "acknowledgmentsLogo",
+                );
+                if (insertAfter) {
+                  insertAfter.after(ackLogosSerialized);
+                } else {
+                  portalNode.appendChild(ackLogosSerialized);
                 }
               }
+            });
+          }
 
-              var requestSettings = {
-                  dataType: "xml",
-                  error: function(model, response) {
+          /* ==== Serialize literature cited ==== */
+          // Assumes the value of literatureCited is a block of bibtex text
 
-                      model.trigger("error", model, response);
+          // Remove node if it exists already
+          $(xmlDoc).find("literatureCited").remove();
 
-                      if( response && response.status == 404 ){
-                        model.trigger("notFound");
-                      }
-                  }
-              };
-
-              //Save a boolean flag for whether or not this fetch was done with user authentication.
-              //This is helpful when the app is dealing with potentially private data
-              this.set("fetchedWithAuth", MetacatUI.appUserModel.get("loggedIn"));
-
-              // Add the user settings to the fetch settings
-              requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());
+          // Get new values
+          var litCit = this.get("literatureCited");
 
-              // Call Backbone.Model.fetch()
-              return Backbone.Model.prototype.fetch.call(this, requestSettings);
+          // Don't serialize falsey values
+          if (litCit.length) {
+            // If there's only one element in litCited, it will be a string
+            // turn it into an array so that we can use _.each
+            if (typeof litCit == "string") {
+              litCit = [litCit];
+            }
 
-            },
+            // Make new <literatureCited> element
+            var litCitSerialized = xmlDoc.createElement("literatureCited");
+
+            _.each(litCit, function (bibtex) {
+              // Wrap in literature cited in cdata tags
+              var cdataLitCit = xmlDoc.createCDATASection(bibtex);
+              var bibtexSerialized = xmlDoc.createElement("bibtex");
+              // wrap in CDATA tags so that bibtex characters aren't escaped
+              bibtexSerialized.appendChild(cdataLitCit);
+              // <bibxtex> is a subelement of <literatureCited>
+              litCitSerialized.appendChild(bibtexSerialized);
+            });
+
+            // Insert new element at correct position
+            var insertAfter = this.getXMLPosition(
+              portalNode,
+              "literatureCited",
+            );
+            if (insertAfter) {
+              insertAfter.after(litCitSerialized);
+            } else {
+              portalNode.appendChild(litCitSerialized);
+            }
+          }
 
-            /**
-            * Get the portal seriesId by searching for the portal by its label in Solr
-            */
-            getSeriesIdByLabel: function(){
+          /* ==== Serialize portal content sections ==== */
 
-              //Exit if there is no portal name set
-              if( !this.get("label") )
-                return;
+          // Remove node if it exists already
+          $portalNode.children("section").remove();
 
-              var model = this;
+          var sections = this.get("sections");
 
-              //Start the base URL for the query service
-              var baseUrl = "";
+          // Don't serialize falsey values
+          if (sections) {
+            _.each(
+              sections,
+              function (sectionModel) {
+                // Don't serialize sections with default values
+                if (!this.sectionIsDefault(sectionModel)) {
+                  var sectionSerialized = sectionModel.updateDOM();
 
-              try{
-                //If this app instance is pointing to the CN, find the Portal series ID on the MN
-                if( MetacatUI.appModel.get("alternateRepositories").length ){
+                  //If there was an error serializing this section, or if
+                  // nothing was returned, don't do anythiing further
+                  if (!sectionSerialized) {
+                    return;
+                  }
 
-                  //Get the array of possible authoritative MNs
-                  var possibleAuthMNs = this.get("possibleAuthMNs");
+                  //Add the section node to the XMLDocument
+                  xmlDoc.adoptNode(sectionSerialized);
 
-                  //If there are no possible authoritative MNs, use the CN query service
-                  if( !possibleAuthMNs.length ){
-                    baseUrl = MetacatUI.appModel.get("queryServiceUrl");
+                  // Remove sections entirely if the content is blank
+                  var newMD = $(sectionSerialized).find("markdown")[0];
+                  if (!newMD || newMD.textContent == "") {
+                    $(sectionSerialized).find("markdown").remove();
                   }
-                  else{
-                    baseUrl = possibleAuthMNs[0].queryServiceUrl;
+
+                  // Remove the <content> element if it's empty.
+                  // This will trigger a validation error, prompting user to
+                  // enter content.
+                  if ($(sectionSerialized).find("content").is(":empty")) {
+                    $(sectionSerialized).find("content").remove();
                   }
 
-                }
-                else{
-                  //Get the query service URL
-                  baseUrl = MetacatUI.appModel.get("queryServiceUrl");
-                }
-              }
-              catch(e){
-                console.error("Error in trying to determine the query service URL. Going to try to use the AppModel setting. ", e);
-              }
-              finally{
-                //Default to the query service URL configured in the AppModel, if one wasn't set earlier
-                if( !baseUrl ){
-                  baseUrl = MetacatUI.appModel.get("queryServiceUrl");
-                  //If there isn't a query service URL, trigger a "not found" error and exit
-                  if( !baseUrl ){
-                    this.trigger("notFound");
-                    return;
+                  // Insert new node at correct position
+                  var insertAfter = model.getXMLPosition(portalNode, "section");
+                  if (insertAfter) {
+                    insertAfter.after(sectionSerialized);
+                  } else {
+                    portalNode.appendChild(sectionSerialized);
                   }
                 }
-              }
+              },
+              this,
+            );
+          }
 
-              var requestSettings = {
-                  url: baseUrl +
-                       "q=label:\"" + this.get("label") + "\" OR " +
-                       "seriesId:\"" + this.get("label") + "\"" +
-                       "&fl=seriesId,id,label,datasource" +
-                       "&sort=dateUploaded%20asc" +
-                       "&rows=1" +
-                       "&wt=json",
-                  dataType: "json",
-                  error: function(response) {
-                      model.trigger("error", model, response);
-
-                      if( response.status == 404 ){
-                        model.trigger("notFound");
-                      }
-                  },
-                  success: function(response){
-                    if( response.response.numFound > 0 ){
+          /* ====  Serialize the EMLText elements ("acknowledgments") ==== */
 
-                      //Set the label and datasource
-                      model.set("label", response.response.docs[0].label);
-                      model.set("datasource", response.response.docs[0].datasource);
+          var textFields = ["acknowledgments"];
 
-                      //Save the seriesId, if one is found
-                      if( response.response.docs[0].seriesId ){
-                        model.set("seriesId", response.response.docs[0].seriesId);
-                      }
-                      //If this portal doesn't have a seriesId,
-                      //but id has been found
-                      else if ( response.response.docs[0].id ){
-                        //Save the id
-                        model.set("id", response.response.docs[0].id);
-
-                        //Find the latest version in this version chain
-                        model.findLatestVersion(response.response.docs[0].id);
-                      }
-                      // if we don't have Id or SeriesId
-                      else {
-                        model.trigger("notFound");
-                      }
+          _.each(
+            textFields,
+            function (field) {
+              var fieldName = field;
 
-                    }
-                    else{
+              // Get the EMLText model
+              var emlTextModels = Array.isArray(this.get(field))
+                ? this.get(field)
+                : [this.get(field)];
+              if (!emlTextModels.length) return;
 
-                      var possibleAuthMNs = model.get("possibleAuthMNs");
-                      if( possibleAuthMNs.length ){
-                        //Remove the first MN from the array, since it didn't contain the Portal, so it's not the auth MN
-                        possibleAuthMNs.shift();
-                      }
+              // Get the node from the XML doc
+              var nodes = $portalNode.children(fieldName);
 
-                      //If there are no other possible auth MNs to check, trigger this Portal as Not Found.
-                      if( possibleAuthMNs.length == 0 || !possibleAuthMNs ){
-                        model.trigger("notFound");
-                      }
-                      //If there's more MNs to check, try again
-                      else{
-                        model.getSeriesIdByLabel();
-                      }
+              // Update the DOMs for each model
+              _.each(
+                emlTextModels,
+                function (thisTextModel, i) {
+                  //Don't serialize falsey values
+                  if (!thisTextModel) return;
 
-                    }
-                  }
-              }
+                  var node;
 
-              //Save a boolean flag for whether or not this fetch was done with user authentication.
-              //This is helpful when the app is dealing with potentially private data
-              this.set("fetchedWithAuth", MetacatUI.appUserModel.get("loggedIn"));
+                  //Get the existing node or create a new one
+                  if (nodes.length < i + 1) {
+                    node = xmlDoc.createElement(fieldName);
+                    this.getXMLPosition(portalNode, fieldName).after(node);
+                  } else {
+                    node = nodes[i];
+                  }
 
-              requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());
+                  var textModelSerialized = thisTextModel.updateDOM();
 
-              $.ajax(requestSettings);
+                  //If the text model wasn't serialized correctly or resulted in nothing
+                  if (
+                    typeof textModelSerialized == "undefined" ||
+                    !textModelSerialized
+                  ) {
+                    //Remove the existing node
+                    $(node).remove();
+                  } else {
+                    xmlDoc.adoptNode(textModelSerialized);
+                    $(node).replaceWith(textModelSerialized);
+                  }
+                },
+                this,
+              );
 
+              // Remove the extra nodes
+              this.removeExtraNodes(nodes, emlTextModels);
             },
-
-            /**
-            * This function has been renamed `getSeriesIdByLabel` and may be removed in future releases.
-            * @deprecated This function has been renamed `getSeriesIdByLabel` and may be removed in future releases.
-            * @see PortalModel#getSeriesIdByLabel
-            */
-            getSeriesIdByName: function(){ this.getSeriesIdByLabel() },
-
-            /**
-             * Overrides the default Backbone.Model.parse() function to parse the custom
-             * portal XML document
-             *
-             * @param {XMLDocument} response - The XMLDocument returned from the fetch() AJAX call
-             * @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
-            */
-            parse: function(response) {
-
-                //Start the empty JSON object
-                var modelJSON = {},
-                    modelRef = this,
-                    portalNode;
-
-                // Iterate over each root XML node to find the portal node
-                $(response).children().each(function(i, el) {
-                    if (el.tagName.indexOf("portal") > -1) {
-                        portalNode = el;
-                        return false;
-                    }
-                });
-
-                // If a portal XML node wasn't found, return an empty JSON object
-                if (typeof portalNode == "undefined" || !portalNode) {
-                    return {};
+            this,
+          );
+
+          /* ====  Serialize awards ==== */
+
+          // Remove award node if it exists already
+          $portalNode.children("award").remove();
+
+          // Get new values
+          var awards = this.get("awards");
+
+          // Don't serialize falsey values
+          if (awards && awards.length > 0) {
+            _.each(awards, function (award) {
+              // Make new node
+              var awardSerialized = xmlDoc.createElement("award");
+
+              // create the <award> subnodes
+              _.map(award, function (value, nodeName) {
+                // serialize the simple text nodes
+                if (nodeName != "funderLogo") {
+                  // Don't serialize falsey values
+                  if (value) {
+                    // Make new sub-nodes
+                    var awardSubnodeSerialized = xmlDoc.createElement(nodeName);
+                    $(awardSubnodeSerialized).text(value);
+                    $(awardSerialized).append(awardSubnodeSerialized);
+                  }
+                } else {
+                  // serialize "funderLogo" which is ImageType
+                  var funderLogoSerialized = value.updateDOM();
+                  xmlDoc.adoptNode(funderLogoSerialized);
+                  $(awardSerialized).append(funderLogoSerialized);
                 }
+              });
 
-                // Parse the collection elements
-                modelJSON = this.parseCollectionXML(portalNode);
-
-                // Save the xml for serialize
-                modelJSON.objectXML = response;
-
-                // Parse the portal logo
-                var portLogo = $(portalNode).children("logo")[0];
-                if (portLogo) {
-                  var portImageModel = new PortalImage({
-                    objectDOM: portLogo,
-                    portalModel: this
-                  });
-                  portImageModel.set(portImageModel.parse());
-                  modelJSON.logo = portImageModel
-                };
-
-                // Parse acknowledgement logos into urls
-                var logos = $(portalNode).children("acknowledgmentsLogo");
-                modelJSON.acknowledgmentsLogos = [];
-                _.each(logos, function(logo, i) {
-                    if ( !logo ) return;
-
-                    var imageModel = new PortalImage({
-                      objectDOM: logo,
-                      portalModel: this
-                    });
-                    imageModel.set(imageModel.parse());
-
-                    if( imageModel.get("imageURL") ){
-                      modelJSON.acknowledgmentsLogos.push( imageModel );
-                    }
-                }, this);
-
-                // Parse the literature cited
-                // This will only work for bibtex at the moment
-                var bibtex = $(portalNode).children("literatureCited").children("bibtex");
-                if (bibtex.length > 0) {
-                    modelJSON.literatureCited = this.parseTextNode(portalNode, "literatureCited");
+              // Insert new node at correct position
+              var insertAfter = model.getXMLPosition(portalNode, "award");
+              if (insertAfter) {
+                insertAfter.after(awardSerialized);
+              } else {
+                portalNode.appendChild(awardSerialized);
+              }
+            });
+          }
+
+          /* ====  Serialize associatedParties ==== */
+
+          // Remove element if it exists already
+          $portalNode.children("associatedParty").remove();
+
+          // Get new values
+          var parties = this.get("associatedParties");
+
+          // Don't serialize falsey values
+          if (parties) {
+            // Serialize each associatedParty
+            _.each(parties, function (party) {
+              // Update the DOM of the EMLParty
+              var partyEl = party.updateDOM();
+              partyDoc = $.parseXML(party.formatXML($(partyEl)[0]));
+
+              // Make sure we don't insert empty EMLParty nodes into the EML
+              if (partyDoc.childNodes.length) {
+                //Save a reference to the associated party element in the NodeList
+                var assocPartyEl = partyDoc.childNodes[0];
+                //Add the associated part element to the portal XML doc
+                xmlDoc.adoptNode(assocPartyEl);
+
+                // Get the last node of this type to insert after
+                var insertAfter = $portalNode
+                  .children("associatedParty")
+                  .last();
+
+                // If there isn't a node found, find the EML position to insert after
+                if (!insertAfter.length) {
+                  insertAfter = model.getXMLPosition(
+                    portalNode,
+                    "associatedParty",
+                  );
                 }
 
-                // Parse the portal content sections
-                modelJSON.sections = [];
-                $(portalNode).children("section").each(function(i, section){
-
-                  //Get the section type, if there is one
-                  var sectionTypeNode = $(section).find("optionName:contains(sectionType)"),
-                      sectionType = "";
-
-                  if( sectionTypeNode.length ){
-                    var optionValueNode = sectionTypeNode.first().siblings("optionValue");
-                    if( optionValueNode.length ){
-                      sectionType = optionValueNode[0].textContent;
-                    }
-                  }
-
-                  if( sectionType == "visualization" ){
-                    // Create a new PortalVizSectionModel
-                    modelJSON.sections.push( new PortalVizSectionModel({
-                      objectDOM: section,
-                      literatureCited: modelJSON.literatureCited
-                    }) );
-                  }
-                  else{
-                    // Create a new PortalSectionModel
-                    modelJSON.sections.push( new PortalSectionModel({
-                      objectDOM: section,
-                      literatureCited: modelJSON.literatureCited,
-                      portalModel: modelRef
-                    }) );
-                  }
-
-                  //Parse the PortalSectionModel
-                  modelJSON.sections[i].set( modelJSON.sections[i].parse(section) );
-                });
-
-                // Parse the EMLText elements
-                modelJSON.acknowledgments = this.parseEMLTextNode(portalNode, "acknowledgments");
-
-                // Parse the awards
-                modelJSON.awards = [];
-                var parse_it = this.parseTextNode;
-                $(portalNode).children("award").each(function(i, award) {
-                    var award_parsed = {};
-                    $(award).children().each(function(i, award_attr) {
-                        if(award_attr.nodeName != "funderLogo"){
-                          // parse the text nodes
-                          award_parsed[award_attr.nodeName] = parse_it(award, award_attr.nodeName);
-                        } else {
-                          // parse funderLogo which is type ImageType
-                          var imageModel = new PortalImage({ objectDOM: award_attr });
-                          imageModel.set(imageModel.parse());
-                          award_parsed[award_attr.nodeName] = imageModel;
-                        }
-                    });
-                    modelJSON.awards.push(award_parsed);
-                });
-
-                // Parse the associatedParties
-                modelJSON.associatedParties = [];
-                $(portalNode).children("associatedParty").each(function(i, associatedParty) {
-
-                    modelJSON.associatedParties.push(new EMLParty({
-                        objectDOM: associatedParty
-                    }));
-
-                });
-
-                // Parse the options. Use children() and not find() because we only want
-                // option nodes that are direct children of the portal node. Option nodes
-                // can also be found within section nodes.
-                $(portalNode).children("option").each(function(i, option) {
-
-                    var optionName = $(option).find("optionName")[0].textContent,
-                        optionValue = $(option).find("optionValue")[0].textContent;
-
-                    if (optionValue === "true") {
-                        optionValue = true;
-                    } else if (optionValue === "false") {
-                        optionValue = false;
-                    }
+                //Insert the party DOM at the insert position
+                if (insertAfter && insertAfter.length) {
+                  insertAfter.after(assocPartyEl);
+                } else {
+                  portalNode.appendChild(assocPartyEl);
+                }
+              }
+            });
+          }
+
+          try {
+            /* ====  Serialize options (including map options) ==== */
+            // This will only serialize the options named in `optNames` (below)
+            // Functionality needed in order to serialize new or custom options
+
+            // The standard list of options used in portals
+            var optNames = this.get("optionNames");
+
+            _.each(optNames, function (optName) {
+              //Get the value on the model
+              var optValue = model.get(optName),
+                existingValue;
+
+              //Get the existing optionName element
+              var matchingOption = $portalNode
+                .children("option")
+                .find("optionName:contains('" + optName + "')");
+
+              //
+              if (
+                !matchingOption.length ||
+                matchingOption.first().text() != optName
+              ) {
+                matchingOption = false;
+              } else {
+                //Get the value for this option from the Portal doc
+                existingValue = matchingOption.siblings("optionValue").text();
+              }
 
-                    // TODO: keep a list of optionNames so that in the case of
-                    // custom options, we can serialize them in serialize()
-                    // otherwise it's not saved in the model which attributes
-                    // are <option></option>s
+              // Don't serialize null or undefined values. Also don't serialize values that match the default model value
+              if (
+                (optValue || optValue === 0 || optValue === false) &&
+                optValue != model.defaults()[optName]
+              ) {
+                //Replace the existing option, if it exists
+                if (matchingOption) {
+                  matchingOption.siblings("optionValue").text(optValue);
+                } else {
+                  // Make new node
+                  // <optionName> and <optionValue> are subelements of <option>
+                  var optionSerialized = xmlDoc.createElement("option"),
+                    optNameSerialized = xmlDoc.createElement("optionName"),
+                    optValueSerialized = xmlDoc.createElement("optionValue");
 
-                    // Convert the comma separated list of pages into an array
-                    if(optionName === "pageOrder" && optionValue && optionValue.length){
-                      optionValue = optionValue.split(',');
-                    }
+                  $(optNameSerialized).text(optName);
+                  $(optValueSerialized).text(optValue);
 
-                    if( !_.has(modelJSON, optionName) ){
-                      modelJSON[optionName] = optionValue;
-                    }
+                  $(optionSerialized).append(
+                    optNameSerialized,
+                    optValueSerialized,
+                  );
 
-                });
+                  // Insert new node at correct position
+                  var insertAfter = model.getXMLPosition(portalNode, "option");
 
-                // Convert all the hex colors to rgb
-                if(modelJSON.primaryColor){
-                  modelJSON.primaryColorRGB = this.hexToRGB(modelJSON.primaryColor);
-                  modelJSON.primaryColorTransparent = "rgba(" +  modelJSON.primaryColorRGB.r +
-                    "," + modelJSON.primaryColorRGB.g + "," + modelJSON.primaryColorRGB.b +
-                    ", .7)";
-                }
-                if(modelJSON.secondaryColor){
-                  modelJSON.secondaryColorRGB = this.hexToRGB(modelJSON.secondaryColor);
-                  modelJSON.secondaryColorTransparent = "rgba(" +  modelJSON.secondaryColorRGB.r +
-                    "," + modelJSON.secondaryColorRGB.g + "," + modelJSON.secondaryColorRGB.b +
-                    ", .5)";
+                  if (insertAfter) {
+                    insertAfter.after(optionSerialized);
+                  }
                 }
-                if(modelJSON.accentColor){
-                  modelJSON.accentColorRGB = this.hexToRGB(modelJSON.accentColor);
-                  modelJSON.accentColorTransparent = "rgba(" +  modelJSON.accentColorRGB.r +
-                    "," + modelJSON.accentColorRGB.g + "," + modelJSON.accentColorRGB.b +
-                    ", .5)";
+              } else {
+                //Remove the elements from the portal XML when the value is invalid
+                if (matchingOption) {
+                  matchingOption.parent("option").remove();
                 }
+              }
+            });
+          } catch (e) {
+            console.error(e);
+          }
 
-                if (gmaps) {
-                    // Create a MapModel with all the map options
-                    modelJSON.mapModel = new MapModel();
-                    var mapOptions = modelJSON.mapModel.get("mapOptions");
-
-                    if (modelJSON.mapZoomLevel) {
-                        mapOptions.zoom = parseInt(modelJSON.mapZoomLevel);
-                        mapOptions.minZoom = parseInt(modelJSON.mapZoomLevel);
-                    }
-                    if ((modelJSON.mapCenterLatitude || modelJSON.mapCenterLatitude === 0) &&
-                        (modelJSON.mapCenterLongitude || modelJSON.mapCenterLongitude === 0)) {
-                        mapOptions.center = modelJSON.mapModel.createLatLng(modelJSON.mapCenterLatitude, modelJSON.mapCenterLongitude);
-                    }
-                    if (modelJSON.mapShapeHue) {
-                        modelJSON.mapModel.set("tileHue", modelJSON.mapShapeHue);
-                    }
-                }
+          /* ====  Serialize UI FilterGroups (aka custom search filters) ==== */
 
-                // Parse the UIFilterGroups
-                modelJSON.filterGroups = [];
-                var allFilters = modelJSON.searchModel.get("filters");
-                $(portalNode).children("filterGroup").each(function(i, filterGroup) {
+          // Get new filter group values
+          var filterGroups = this.get("filterGroups");
 
-                  // Create a FilterGroup model
-                  var filterGroupModel = new FilterGroup({
-                      objectDOM: filterGroup,
-                      isUIFilterType: true
-                  });
-                  modelJSON.filterGroups.push(filterGroupModel);
+          // Remove filter groups in the current objectDOM that are at the portal
+          // level. (don't use .find("filterGroup") as that would remove
+          // filterGroups that are nested in the definition
+          $portalNode.children("filterGroup").remove();
 
-                  // Add the Filters from this FilterGroup to the portal's Search model,
-                  // unless this portal model is being edited. Then we only want the
-                  // definition filters to be included in the search model.
-                  if (!modelRef.get("edit")){
-                    allFilters.add(filterGroupModel.get("filters").models);
-                  }
-                  
+          // Make a new node for each filter group in the model
+          _.each(filterGroups, function (filterGroup) {
+            filterGroupSerialized = filterGroup.updateDOM();
 
-                });
+            if (filterGroupSerialized) {
+              //Add the new element to the XMLDocument
+              xmlDoc.adoptNode(filterGroupSerialized);
 
-                return modelJSON;
-            },
+              // Insert new node at correct position
+              var insertAfter = model.getXMLPosition(portalNode, "filterGroup");
 
-            /**
-             * Parses the XML nodes that are of type EMLText
-             *
-             * @param {Element} parentNode - The XML Element that contains all the EMLText nodes
-             * @param {string} nodeName - The name of the XML node to parse
-             * @param {boolean} isMultiple - If true, parses the nodes into an array
-             * @return {(string|Array)} A string or array of strings comprising the text content
-            */
-            parseEMLTextNode: function(parentNode, nodeName, isMultiple) {
-
-                var node = $(parentNode).children(nodeName);
-
-                // If no matching nodes were found, return falsey values
-                if (!node || !node.length) {
-
-                    // Return an empty array if the isMultiple flag is true
-                    if (isMultiple)
-                        return [];
-                    // Return null if the isMultiple flag is false
-                    else
-                        return null;
+              if (insertAfter) {
+                insertAfter.after(filterGroupSerialized);
+              } else {
+                portalNode.appendChild(filterGroupSerialized);
+              }
+            }
+          });
+
+          /* ====  Remove duplicates ==== */
+
+          //Do a final check to make sure there are no duplicate ids in the XML
+          var elementsWithIDs = $(xmlDoc).find("[id]"),
+            //Get an array of all the ids in this EML doc
+            allIDs = _.map(elementsWithIDs, function (el) {
+              return $(el).attr("id");
+            });
+
+          //If there is at least one id in the EML...
+          if (allIDs && allIDs.length) {
+            //Boil the array down to just the unique values
+            var uniqueIDs = _.uniq(allIDs);
+
+            //If the unique array is shorter than the array of all ids,
+            // then there is a duplicate somewhere
+            if (uniqueIDs.length < allIDs.length) {
+              //For each element in the EML that has an id,
+              _.each(elementsWithIDs, function (el) {
+                //Get the id for this element
+                var id = $(el).attr("id");
+
+                //If there is more than one element in the EML with this id,
+                if ($(xmlDoc).find("[id='" + id + "']").length > 1) {
+                  //And if it is not a unit node, which we don't want to change,
+                  if (!$(el).is("unit"))
+                    //Then change the id attribute to a random uuid
+                    $(el).attr("id", "urn-uuid-" + uuid.v4());
                 }
-                // If exactly one node is found and we are only expecting one, return the text content
-                else if (node.length == 1 && !isMultiple) {
-                    return new EMLText({
-                        objectDOM: node[0]
-                    });
-                } else {
-                // If more than one node is found, parse into an array
-                    return _.map(node, function(node) {
-                        return new EMLText({
-                            objectDOM: node
-                        });
-                    });
-
+              });
+            }
+          }
+
+          // Convert xml to xmlString and return xmlString
+          xmlString = new XMLSerializer().serializeToString(xmlDoc);
+
+          //If there isn't an XML declaration, add one
+          if (xmlString.indexOf("<?xml") == -1) {
+            xmlString = '<?xml version="1.0" encoding="UTF-8"?>' + xmlString;
+          }
+
+          return xmlString;
+        } catch (e) {
+          console.error("Error while serializing the Portal XML document: ", e);
+          this.set("errorMessage", e.stack);
+          this.trigger(
+            "errorSaving",
+            MetacatUI.appModel.get("portalEditSaveErrorMsg"),
+          );
+          return;
+        }
+      },
+
+      /**
+       * Checks whether the given sectionModel has been updated by the
+       * user, or whether all attributes match their default values.
+       * For a section's markdown, the default value is either an empty
+       * string or null. For a section's label, the default
+       * value is either an empty string or a string that begins with the
+       * value set to PortalModel.newSectionLabel. For all other attributes,
+       * the defaults are set in PortalSectionModel.defaults.
+       * @param {PortalSectionModel} sectionModel - The model to check against a default model
+       * @return {boolean} returns true if the sectionModel matches a default model, and false when at least one attribute differs
+       */
+      sectionIsDefault: function (sectionModel) {
+        try {
+          var defaults = sectionModel.defaults(),
+            currentMarkdown = sectionModel.get("content").get("markdown"),
+            labelRegex = new RegExp("^" + this.newSectionLabel, "i");
+
+          // For each attribute, check whether it matches the default
+          if (
+            // Check whether markdown matches the content that's
+            // auto-filled or whether it's empty
+            //currentMarkdown === this.markdownExample ||
+            (currentMarkdown == "" || currentMarkdown == null) &&
+            sectionModel.get("image") === defaults.image &&
+            sectionModel.get("introduction") === defaults.introduction &&
+            // Check whether label starts with the default new page name,
+            // or whether it's empty
+            (labelRegex.test(sectionModel.get("label")) ||
+              sectionModel.get("label") == "" ||
+              sectionModel.get("label") == null) &&
+            sectionModel.get("literatureCited") === defaults.literatureCited &&
+            sectionModel.get("title") === defaults.title
+          ) {
+            // All elements of the section match the default
+            return true;
+          } else {
+            // At least one attribute of the section has been updated
+            return false;
+          }
+        } catch (e) {
+          // If there's a problem with this function for some reason,
+          // return false so that the section is serialized to avoid
+          // losing information
+          console.log(
+            "Failed to check whether section model is default. Serializing it anyway. Error message:" +
+              e,
+          );
+          return false;
+        }
+      },
+
+      /**
+       * Initialize the object XML for a brand spankin' new portal
+       * @inheritdoc
+       *
+       */
+      createXML: function () {
+        var format =
+          MetacatUI.appModel.get("portalEditorSerializationFormat") ||
+          "https://purl.dataone.org/portals-1.1.0";
+        var xmlString = '<por:portal xmlns:por="' + format + '"></por:portal>';
+        var xmlNew = $.parseXML(xmlString);
+        var portalNode = xmlNew.getElementsByTagName("por:portal")[0];
+
+        this.set("ownerDocument", portalNode.ownerDocument);
+        return xmlNew;
+      },
+
+      /**
+       * Overrides the default Backbone.Model.validate.function() to
+       * check if this portal model has all the required values necessary
+       * to save to the server.
+       *
+       * @param {Object} [attrs] - A literal object of model attributes to validate.
+       * @param {Object} [options] - A literal object of options for this validation process
+       * @return {Object} If there are errors, an object comprising error
+       *                   messages. If no errors, returns nothing.
+       */
+      validate: function (attrs, options) {
+        try {
+          var errors = {},
+            requiredFields =
+              MetacatUI.appModel.get("portalEditorRequiredFields") || {};
+
+          //Execute the superclass validate() function
+          var collectionErrors = this.constructor.__super__.validate.call(this);
+          if (
+            typeof collectionErrors == "object" &&
+            Object.keys(collectionErrors).length
+          ) {
+            //Use the errors messages from the CollectionModel for this PortalModel
+            errors = collectionErrors;
+          }
+
+          // ---- Validate the description and name ----
+          //Map the model attributes to the user-facing attribute name
+          var textFields = {
+            description: "description",
+            name: "title",
+          };
+          //Iterate over each text field
+          _.each(
+            Object.keys(textFields),
+            function (field) {
+              //If this field is required, and it is a string
+              if (requiredFields[field] && typeof this.get(field) == "string") {
+                //If this is an empty string, set an error message
+                if (!this.get(field).trim().length) {
+                  errors[field] = "A " + textFields[field] + " is required.";
                 }
-
-            },
-
-            /**
-            * Sets the fileName attribute on this model using the portal label
-            * @override
-            */
-            setMissingFileName: function(){
-
-              var fileName = this.get("label");
-
-              if( !fileName ){
-                fileName = "portal.xml";
               }
-              else{
-                fileName = fileName.replace(/[^a-zA-Z0-9]/g, "_") + ".xml";
+              //If this field is required, and it's not a string at all, set an error message
+              else if (requiredFields[field]) {
+                errors[field] = "A " + textFields[field] + " is required.";
               }
-
-              this.set("fileName", fileName);
-
             },
-
-            /**
-             * @typedef {Object} PortalModel#rgb - An RGB color value
-             * @property {number} r - A value between 0 and 255 defining the intensity of red
-             * @property {number} g - A value between 0 and 255 defining the intensity of green
-             * @property {number} b - A value between 0 and 255 defining the intensity of blue
-            */
-
-            /**
-             * Converts hex color values to RGB
-             *
-             * @param {string} hex - a color in hexadecimal format
-             * @return {rgb} a color in RGB format
-            */
-            hexToRGB: function(hex){
-              var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
-              return result ? {
-                  r: parseInt(result[1], 16),
-                  g: parseInt(result[2], 16),
-                  b: parseInt(result[3], 16)
-              } : null;
-              },
-
-            /**
-             * Finds the node in the given portal XML document afterwhich the
-             * given node type should be inserted
-             *
-             * @param {Element} portalNode - The portal element of an XML document
-             * @param {string} nodeName - The name of the node to be inserted
-             *                             into xml
-             * @return {(jQuery|boolean)} A jQuery object indicating a position,
-             *                            or false when nodeName is not in the
-             *                            portal schema
-            */
-            getXMLPosition: function(portalNode, nodeName){
-
-              var nodeOrder = [ "label", "name", "description", "definition",
-                                "logo", "section", "associatedParty",
-                                "acknowledgments", "acknowledgmentsLogo",
-                                "award", "literatureCited", "filterGroup",
-                                "option"];
-
-              var position = _.indexOf(nodeOrder, nodeName);
-
-              // First check that nodeName is in the list of nodes
-              if ( position == -1 ) {
-                  return false;
-              };
-
-              // If there's already an occurence of nodeName...
-              if($(portalNode).children(nodeName).length > 0){
-                // ...insert it after the last occurence
-                return $(portalNode).children(nodeName).last();
-              } else {
-                // Go through each node in the node list and find the position
-                // after which this node will be inserted
-                for (var i = position - 1; i >= 0; i--) {
-                  if ( $(portalNode).children(nodeOrder[i]).length ) {
-                    return $(portalNode).children(nodeOrder[i]).last();
-                  }
-                }
+            this,
+          );
+
+          //---Validate the sections---
+          //Iterate over each section model
+          _.each(
+            this.get("sections"),
+            function (section) {
+              //Validate the section model
+              var sectionErrors = section.validate();
+
+              //If there is at least one error, then add an error to the PortalModel error list
+              if (sectionErrors && Object.keys(sectionErrors).length) {
+                errors.sections = "At least one section has an error";
               }
-
-              return false;
             },
+            this,
+          );
+
+          //----Validate the logo----
+          if (
+            requiredFields.logo &&
+            (!this.get("logo") || !this.get("logo").get("identifier"))
+          ) {
+            errors.logo = "A logo image is required";
+          } else if (this.get("logo")) {
+            logoErrors = this.get("logo").validate();
+            if (logoErrors && Object.keys(logoErrors).length) {
+              errors.logo = "A logo image is required";
+            }
+          }
 
-            /**
-             * Retrieves the model attributes and serializes into portal XML,
-             * to produce the new or modified portal document.
-             *
-             * @return {string} - Returns the portal XML as a string.
-            */
-            serialize: function(){
-
-              try{
-
-                // So we can call getXMLPosition() from within if{}
-                var model = this;
-
-                var xmlDoc,
-                    portalNode,
-                    xmlString;
-
-                xmlDoc = this.get("objectXML");
-
-                // Check if there is a portal doc already
-                if (xmlDoc == null){
-                  // If not create one
-                  xmlDoc = this.createXML();
-                } else {
-                  // If yes, clone it
-                  xmlDoc = xmlDoc.cloneNode(true);
-                };
-
-                // Iterate over each root XML node to find the portal node
-                $(xmlDoc).children().each(function(i, el) {
-                    if (el.tagName.indexOf("portal") > -1) {
-                        portalNode = el;
-                    }
-                });
-
-                // Serialize the collection elements
-                // ("name", "label", "description", "definition")
-                portalNode = this.updateCollectionDOM(portalNode);
-                xmlDoc = portalNode.getRootNode();
-                var $portalNode = $(portalNode);
-
-                // Set formatID
-                this.set("formatId",
-                  MetacatUI.appModel.get("portalEditorSerializationFormat") ||
-                  "https://purl.dataone.org/portals-1.1.0");
-
-                /* ==== Serialize portal logo ==== */
-
-                // Remove node if it exists already
-                $(xmlDoc).find("logo").remove();
-
-                // Get new values
-                var logo = this.get("logo");
-
-                // Don't serialize falsey values or empty logos
-                if(logo && logo.get("identifier")){
-
-                  // Make new node
-                  var logoSerialized = logo.updateDOM("logo");
-
-                  //Add the logo node to the XMLDocument
-                  xmlDoc.adoptNode(logoSerialized);
-
-                  // Insert new node at correct position
-                  var insertAfter = this.getXMLPosition(portalNode, "logo");
-                  if(insertAfter){
-                    insertAfter.after(logoSerialized);
-                  }
-                  else{
-                    portalNode.appendChild(logoSerialized);
-                  }
-
-                };
-
-                /* ==== Serialize acknowledgment logos ==== */
-
-                // Remove element if it exists already
-                $(xmlDoc).find("acknowledgmentsLogo").remove();
-
-                var acknowledgmentsLogos = this.get("acknowledgmentsLogos");
-
-                // Don't serialize falsey values
-                if(acknowledgmentsLogos){
-
-                  _.each(acknowledgmentsLogos, function(imageModel) {
+          //---Validate the acknowledgmentsLogo---
 
-                    // Don't serialize empty imageModels
-                    if(
-                      imageModel.get("identifier") ||
-                      imageModel.get("label") ||
-                      imageModel.get("associatedURL")
-                    ){
+          var nonEmptyAckLogos = this.get("acknowledgmentsLogos").filter(
+            function (portalImage) {
+              return !portalImage.isEmpty();
+            },
+          );
+
+          if (requiredFields.acknowledgmentsLogos && !nonEmptyAckLogos.length) {
+            errors.acknowledgmentsLogos =
+              "At least one partner logo image is required.";
+          } else if (nonEmptyAckLogos && nonEmptyAckLogos.length) {
+            _.each(
+              nonEmptyAckLogos,
+              function (ackLogo) {
+                // Validate the portal image model
+                var ackLogoErrors = ackLogo.validate();
+
+                // If there is at least one error, then add an error to the PortalModel error list
+                if (ackLogoErrors && Object.keys(ackLogoErrors).length) {
+                  errors.acknowledgmentsLogosImages =
+                    "At least one acknowledgment logo has an error";
+                }
+              },
+              this,
+            );
+          }
+
+          //TODO: Validate these other elements, listed below, as they are added to the portal editor
+
+          //---Validate the associatedParties---
+
+          //---Validate the acknowledgments---
+
+          //---Validate the award---
+
+          //---Validate the literatureCited---
+
+          //---Validate the filterGroups---
+
+          //Return the errors object
+          if (Object.keys(errors).length) return errors;
+          else {
+            return;
+          }
+        } catch (e) {
+          console.error(e);
+        }
+      },
+
+      /**
+       * Checks for the existing block list for repository labels
+       * If at least one other Portal has the same label, then it is not available.
+       * @param {string} label - The label to query for
+       */
+      checkLabelAvailability: function (label) {
+        //Validate the label set on the model if one isn't given
+        if (!label || typeof label != "string") {
+          var label = this.get("label");
+          if (!label || typeof label != "string") {
+            //Trigger an error event
+            this.trigger("errorValidatingLabel");
+            console.error("error validating label, no label provided");
+            return;
+          }
+        }
+
+        var model = this;
+
+        if (!this.get("checkedNodeLabels")) {
+          // query CN to fetch the latest node data
+          model.updateNodeBlockList();
+
+          this.listenTo(this, "change:checkedNodeLabels", function () {
+            this.checkPortalLabelAvailability(label);
+          });
+        } else {
+          this.checkPortalLabelAvailability(label);
+        }
+      },
+
+      /**
+       * Queries the Solr discovery index for other Portal objects with this same label.
+       * Also, checks for the existing block list for repository labels
+       * If at least one other Portal has the same label, then it is not available.
+       * @param {string} label - The label to query for
+       */
+      checkPortalLabelAvailability: function (label) {
+        var model = this;
+
+        // Stop Listening to the node model. We only need to retrieve this node label once.
+        this.stopListening(this, "change:checkedNodeLabels", function () {
+          this.checkPortalLabelAvailability(label);
+        });
 
-                      var ackLogosSerialized = imageModel.updateDOM();
+        // Convert the block list to lower case for case insensitive match
+        var lowerCaseBlockList = this.get("labelBlockList").map(
+          function (value) {
+            return value.toLowerCase();
+          },
+        );
+
+        // Check the existing blockList before making a Solr call
+        if (lowerCaseBlockList.indexOf(label.toLowerCase()) > -1) {
+          model.trigger("labelTaken");
+          return;
+        }
+
+        // Query solr to see if other portals already use this label
+        var requestSettings = {
+          url:
+            MetacatUI.appModel.get("queryServiceUrl") +
+            'q=label:"' +
+            label +
+            '"' +
+            ' AND formatId:"' +
+            this.get("formatId") +
+            '"' +
+            "&rows=0" +
+            "&wt=json",
+          error: function (response) {
+            model.trigger("errorValidatingLabel");
+          },
+          success: function (response) {
+            if (response.response.numFound > 0) {
+              //Add this label to the blockList so we don't have to query for it later
+              var blockList = model.get("labelBlockList");
+              if (Array.isArray(blockList)) {
+                blockList.push(label);
+              }
 
-                      //Add the logo node to the XMLDocument
-                      xmlDoc.adoptNode(ackLogosSerialized);
+              model.trigger("labelTaken");
+            } else {
+              if (MetacatUI.appModel.get("alternateRepositories").length) {
+                MetacatUI.appModel.setActiveAltRepo();
+                var activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
+                if (activeAltRepo) {
+                  var requestSettings = {
+                    url:
+                      activeAltRepo.queryServiceUrl +
+                      'q=label:"' +
+                      label +
+                      '"' +
+                      ' AND formatId:"' +
+                      model.get("formatId") +
+                      '"' +
+                      "&rows=0" +
+                      "&wt=json",
+                    error: function (response) {
+                      model.trigger("errorValidatingLabel");
+                    },
+                    success: function (response) {
+                      if (response.response.numFound > 0) {
+                        //Add this label to the blockList so we don't have to query for it later
+                        var blockList = model.get("labelBlockList");
+                        if (Array.isArray(blockList)) {
+                          blockList.push(label);
+                        }
 
-                      // Insert new node at correct position
-                      var insertAfter = model.getXMLPosition(portalNode, "acknowledgmentsLogo");
-                      if(insertAfter){
-                        insertAfter.after(ackLogosSerialized);
-                      }
-                      else {
-                        portalNode.appendChild(ackLogosSerialized);
+                        model.trigger("labelTaken");
+                      } else {
+                        model.trigger("labelAvailable");
                       }
-                    }
-                  })
-                };
-
-                /* ==== Serialize literature cited ==== */
-                // Assumes the value of literatureCited is a block of bibtex text
-
-                // Remove node if it exists already
-                $(xmlDoc).find("literatureCited").remove();
-
-                // Get new values
-                var litCit = this.get("literatureCited");
-
-                // Don't serialize falsey values
-                if( litCit.length ){
+                    },
+                  };
+                  //Attach the User auth info and send the request
+                  requestSettings = _.extend(
+                    requestSettings,
+                    MetacatUI.appUserModel.createAjaxSettings(),
+                  );
+                  $.ajax(requestSettings);
+                }
+              } else {
+                model.trigger("labelAvailable");
+              }
+            }
+          },
+        };
+        //Attach the User auth info and send the request
+        requestSettings = _.extend(
+          requestSettings,
+          MetacatUI.appUserModel.createAjaxSettings(),
+        );
+        $.ajax(requestSettings);
+      },
+
+      /**
+       * Queries the CN Solr to retrieve the updated BlockList
+       */
+      updateNodeBlockList: function () {
+        var model = this;
+
+        $.ajax({
+          url: MetacatUI.appModel.get("nodeServiceUrl"),
+          dataType: "text",
+          error: function (data, textStatus, xhr) {
+            // if there is an error in retrieving the node list;
+            // proceed with the existing node list to perform the checks
+            model.checkPortalLabelAvailability();
+          },
+          success: function (data, textStatus, xhr) {
+            var xmlResponse = $.parseXML(data) || null;
+            if (!xmlResponse) return;
+
+            // update the node block list on success
+            model.saveNodeBlockList(xmlResponse);
+          },
+        });
+      },
+
+      /**
+       * Parses the retrieved XML document and saves the node information to the BlockList
+       *
+       * @param {XMLDocument} The XMLDocument returned from the fetch() AJAX call
+       */
+      saveNodeBlockList: function (xml) {
+        var model = this,
+          children = xml.children || xml.childNodes;
+
+        //Traverse the XML response to get the MN info
+        _.each(children, function (d1NodeList) {
+          var d1NodeListChildren = d1NodeList.children || d1NodeList.childNodes;
+
+          //The first (and only) child should be the d1NodeList
+          _.each(d1NodeListChildren, function (thisNode) {
+            //Ignore parts of the XML that is not MN info
+            if (!thisNode.attributes) return;
+
+            //'node' will be a single node
+            var node = {},
+              nodeProperties = thisNode.children || thisNode.childNodes;
+
+            //Grab information about this node from XML nodes
+            _.each(nodeProperties, function (nodeProperty) {
+              if (nodeProperty.nodeName == "property")
+                node[$(nodeProperty).attr("key")] = nodeProperty.textContent;
+              else node[nodeProperty.nodeName] = nodeProperty.textContent;
+
+              //Check if this member node has v2 read capabilities - important for the Package service
+              if (
+                nodeProperty.nodeName == "services" &&
+                nodeProperty.childNodes.length
+              ) {
+                var v2 = $(nodeProperty).find(
+                  "service[name='MNRead'][version='v2'][available='true']",
+                ).length;
+                node["readv2"] = v2;
+              }
+            });
+
+            //Grab information about this node from XLM attributes
+            _.each(thisNode.attributes, function (attribute) {
+              node[attribute.nodeName] = attribute.nodeValue;
+            });
+
+            // Append Node name, node identifier and node short identifier to the array.
+            // node identifier
+            if (
+              Array.isArray(model.get("labelBlockList")) &&
+              model.get("labelBlockList").indexOf(node.identifier) < 0
+            ) {
+              model.get("labelBlockList").push(node.identifier);
+            }
 
-                  // If there's only one element in litCited, it will be a string
-                  // turn it into an array so that we can use _.each
-                  if(typeof litCit == "string"){
-                    litCit = [litCit]
-                  }
+            // node name
+            if (node.CN_node_name) {
+              node.name = node.CN_node_name;
+              if (
+                Array.isArray(model.get("labelBlockList")) &&
+                model.get("labelBlockList").indexOf(node.name) < 0
+              ) {
+                model.get("labelBlockList").push(node.name);
+              }
+            }
 
-                  // Make new <literatureCited> element
-                  var litCitSerialized = xmlDoc.createElement("literatureCited");
+            // node short identifier
+            node.shortIdentifier = node.identifier.substring(
+              node.identifier.lastIndexOf(":") + 1,
+            );
+            if (
+              Array.isArray(model.get("labelBlockList")) &&
+              model.get("labelBlockList").indexOf(node.shortIdentifier) < 0
+            ) {
+              model.get("labelBlockList").push(node.shortIdentifier);
+            }
+          });
+        });
 
-                  _.each(litCit, function(bibtex){
+        this.set("checkedNodeLabels", "true");
+      },
+
+      /**
+       * Removes nodes from the XML that do not have an accompanying model
+       * (i.e. nodes which were probably removed by the user during editing)
+       *
+       * @param {jQuery} nodes - The nodes to potentially remove
+       * @param {Model[]} models - The model to compare to
+       */
+      removeExtraNodes: function (nodes, models) {
+        // Remove the extra nodes
+        var extraNodes = nodes.length - models.length;
+        if (extraNodes > 0) {
+          for (var i = models.length; i < nodes.length; i++) {
+            $(nodes[i]).remove();
+          }
+        }
+      },
+
+      /**
+       * Saves the portal XML document to the server using the DataONE API
+       */
+      save: function () {
+        var model = this;
+
+        // Remove empty filters from the custom portal search filters.
+        this.get("filterGroups").forEach(function (filterGroupModel) {
+          filterGroupModel.get("filters").removeEmptyFilters();
+        }, this);
+
+        // Ensure empty filters (rule groups) are removed, including from
+        // within any nested filter groups
+        this.get("definitionFilters").removeEmptyFilters(true);
+
+        // Validate before we try anything else
+        if (!this.isValid()) {
+          //Trigger the invalid and cancelSave events
+          this.trigger("invalid");
+          this.trigger("cancelSave");
+          //Don't save the model since it's invalid
+          return false;
+        } else {
+          //Double-check that the label is available, if it was changed
+          if (
+            (this.isNew() || this.get("originalLabel") != this.get("label")) &&
+            !this.get("labelDoubleChecked")
+          ) {
+            //If the label is taken
+            this.once("labelTaken", function () {
+              //Stop listening to the label availability
+              this.stopListening("labelAvailable");
+
+              //Set that the label has been double-checked
+              this.set("labelDoubleChecked", true);
+
+              //If this portal is in a free trial of DataONE Plus, generate a new random label
+              // and start the save process again
+              if (MetacatUI.appModel.get("enableBookkeeperServices")) {
+                var subscription = MetacatUI.appUserModel.get(
+                  "dataoneSubscription",
+                );
+                if (subscription && subscription.isTrialing()) {
+                  this.setRandomLabel();
+
+                  this.set("labelDoubleChecked", true);
+
+                  // Start the save process again
+                  this.save();
 
-                    // Wrap in literature cited in cdata tags
-                    var cdataLitCit = xmlDoc.createCDATASection(bibtex);
-                    var bibtexSerialized = xmlDoc.createElement("bibtex");
-                    // wrap in CDATA tags so that bibtex characters aren't escaped
-                    bibtexSerialized.appendChild(cdataLitCit);
-                    // <bibxtex> is a subelement of <literatureCited>
-                    litCitSerialized.appendChild(bibtexSerialized);
+                  return;
+                }
+              } else {
+                //If the label is taken, trigger an invalid event
+                this.trigger("invalid");
+                //Trigger a cancellation of the save event
+                this.trigger("cancelSave");
+              }
+            });
+
+            this.once("labelAvailable", function () {
+              this.stopListening("labelTaken");
+              this.set("labelDoubleChecked", true);
+              this.save();
+            });
+
+            // Check label availability
+            this.checkLabelAvailability(this.get("label"));
+
+            // console.log("Double checking label");
+
+            //Don't proceed with the rest of the save
+            return;
+          } else {
+            this.trigger("valid");
+          }
+        }
+
+        //Check if the checksum has been calculated yet.
+        if (!this.get("checksum")) {
+          // Serialize the XML
+          var xml = this.serialize();
+
+          //If there is no xml returned from the serialize() function, then there
+          // was an error, so don't save.
+          if (typeof xml === "undefined" || !xml) {
+            //If no error message is set on the model, trigger an error now.
+            // If there is an error message already, it means the error has already
+            // been triggered inside the serialize() function.
+            if (!this.get("errorMessage")) {
+              this.trigger(
+                "errorSaving",
+                MetacatUI.appModel.get("portalEditSaveErrorMsg"),
+              );
+            }
 
+            return;
+          }
+
+          var xmlBlob = new Blob([xml], { type: "application/xml" });
+
+          //Set the Blob as the upload file
+          this.set("uploadFile", xmlBlob);
+
+          //When it is calculated, restart this function
+          this.off("checksumCalculated", this.save);
+          this.on("checksumCalculated", this.save);
+          //Calculate the checksum for this file
+          this.calculateChecksum();
+
+          //Exit this function until the checksum is done
+          return;
+        }
+
+        this.constructor.__super__.save.call(this);
+      },
+
+      /**
+       * Removes or hides the given section from this Portal
+       * @param {PortalSectionModel|string} section - Either the PortalSectionModel
+       * to remove, or the name of the section to remove. Some sections in the portals
+       * are not tied to PortalSectionModels, because they are created from other parts of the Portal
+       * document. For example, the Data, Metrics, and Members sections.
+       */
+      removeSection: function (section) {
+        try {
+          //If this section is a string, remove it by adding custom options
+          if (typeof section == "string") {
+            switch (section.toLowerCase()) {
+              case "data":
+                this.set("hideData", true);
+                break;
+              case "metrics":
+                this.set("hideMetrics", true);
+                break;
+              case "members":
+                this.set("hideMembers", true);
+                break;
+            }
+          }
+          //If this section is a section model, delete it from this Portal
+          else if (PortalSectionModel.prototype.isPrototypeOf(section)) {
+            // Remove the section from the model's sections array object.
+            // Use clone() to create new array reference and ensure change
+            // event is tirggered.
+            var sectionModels = _.clone(this.get("sections"));
+            sectionModels.splice($.inArray(section, sectionModels), 1);
+            this.set({ sections: sectionModels });
+          } else {
+            return;
+          }
+        } catch (e) {
+          console.error(e);
+        }
+      },
+
+      /**
+       * Adds the given section to this Portal
+       * @param {PortalSectionModel|string} section - Either the PortalSectionModel
+       * to add, or the name of the section to add. Some sections in the portals
+       * are not tied to PortalSectionModels, because they are created from other parts of the Portal
+       * document. For example, the Data, Metrics, and Members sections.
+       */
+      addSection: function (section) {
+        try {
+          //If this section is a string, add it by adding custom options
+          if (typeof section == "string") {
+            switch (section.toLowerCase()) {
+              case "data":
+                this.set("hideData", null);
+                break;
+              case "metrics":
+                this.set("hideMetrics", null);
+                break;
+              case "members":
+                this.set("hideMembers", null);
+                break;
+              case "freeform":
+                // Add a new, blank markdown section with a default image
+                var sectionModels = _.clone(this.get("sections")),
+                  newSection = new PortalSectionModel({
+                    portalModel: this,
+                    // Include a default image if some are configured.
+                    image: this.getRandomSectionImage(),
                   });
 
-                  // Insert new element at correct position
-                  var insertAfter = this.getXMLPosition(portalNode, "literatureCited");
-                  if(insertAfter){
-                    insertAfter.after(litCitSerialized);
-                  }
-                  else{
-                    portalNode.appendChild(litCitSerialized);
-                  }
+                sectionModels.push(newSection);
+                this.set("sections", sectionModels);
+                // Trigger event manually so we can just pass newSection
+                this.trigger("addSection", newSection);
+                break;
+            }
+          }
+          // If this section is a section model, add it to this Portal
+          else if (PortalSectionModel.prototype.isPrototypeOf(section)) {
+            var sectionModels = _.clone(this.get("sections"));
+            sectionModels.push(section);
+            this.set({ sections: sectionModels });
+            // trigger event manually so we can just pass newSection
+            this.trigger("addSection", section);
+          } else {
+            return;
+          }
+        } catch (e) {
+          console.error(e);
+        }
+      },
+
+      /**
+       * removePortalImage - remove a PortalImage model from either the
+       * logo, sections, or acknowledgmentsLogos node of the portal model.
+       *
+       * @param  {Image} portalImage the portalImage model to remove
+       */
+      removePortalImage: function (portalImage) {
+        try {
+          // find the portalImage to remove
+          switch (portalImage.get("nodeName")) {
+            case "logo":
+              if (portalImage === this.get("logo")) {
+                this.set("logo", this.defaults().logo);
+              }
+              break;
+            case "image":
+              _.each(this.get("sections"), function (section, i) {
+                if (portalImage === section.get("image")) {
+                  section.set("image", section.defaults().image);
                 }
+              });
+              break;
+            case "acknowledgmentsLogo":
+              var ackLogos = _.clone(this.get("acknowledgmentsLogos"));
+              ackLogos.splice($.inArray(portalImage, ackLogos), 1);
+              this.set({ acknowledgmentsLogos: ackLogos });
+              break;
+          }
+        } catch (e) {
+          console.log(
+            "Failed to remove a portalImage model, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Saves a reference to this Portal on the MetacatUI global object
+       */
+      cachePortal: function () {
+        if (this.get("id")) {
+          MetacatUI.portals = MetacatUI.portals || {};
+          MetacatUI.portals[this.get("id")] = this;
+        }
+
+        this.on("change:id", this.cachePortal);
+      },
+
+      /**
+       * Creates a URL for viewing more information about this object
+       * @return {string}
+       */
+      createViewURL: function () {
+        return (
+          MetacatUI.root +
+          "/" +
+          MetacatUI.appModel.get("portalTermPlural") +
+          "/" +
+          encodeURIComponent(
+            this.get("label") || this.get("seriesId") || this.get("id"),
+          )
+        );
+      },
+
+      /**
+       * Sets attributes on this Portal using the given Member Node data
+       * @param {object} nodeInfoObject - A literal object taken from the NodeModel 'members' array
+       */
+      createNodeAttributes: function (nodeInfoObject) {
+        var nodePortalModel = {};
+
+        if (nodeInfoObject === undefined) {
+          nodeInfoObject = {};
+        }
+
+        //TODO - check for undefined for each of the nodeInfo properties
+
+        // Setting basic properties from the node info object
+        this.set("name", nodeInfoObject.name);
+        this.set("logo", nodeInfoObject.logo);
+        this.set("description", nodeInfoObject.description);
+
+        // Creating repo specific Filters
+        var nodeFilterModel = new FilterModel({
+          fields: ["datasource"],
+          values: [nodeInfoObject.identifier],
+          label: "Datasets for a repository",
+          matchSubstring: false,
+          operator: "OR",
+        });
 
-                /* ==== Serialize portal content sections ==== */
-
-                // Remove node if it exists already
-                $portalNode.children("section").remove();
-
-                var sections = this.get("sections");
-
-                // Don't serialize falsey values
-                if(sections){
-
-                  _.each(sections, function(sectionModel) {
-
-                    // Don't serialize sections with default values
-                    if(!this.sectionIsDefault(sectionModel)){
-
-                      var sectionSerialized = sectionModel.updateDOM();
-
-                      //If there was an error serializing this section, or if
-                      // nothing was returned, don't do anythiing further
-                      if( !sectionSerialized ){
-                        return;
-                      }
-
-                      //Add the section node to the XMLDocument
-                      xmlDoc.adoptNode(sectionSerialized);
-
-                      // Remove sections entirely if the content is blank
-                      var newMD = $(sectionSerialized).find("markdown")[0];
-                      if( !newMD || newMD.textContent == "" ){
-                        $(sectionSerialized).find("markdown").remove();
-                      }
+        // adding the filter in the node model
+        this.get("definitionFilters").add(nodeFilterModel);
 
-                      // Remove the <content> element if it's empty.
-                      // This will trigger a validation error, prompting user to
-                      // enter content.
-                      if($(sectionSerialized).find("content").is(':empty')){
-                        $(sectionSerialized).find("content").remove();
-                      }
+        // Set up the search model
+        this.get("searchModel").get("filters").add(nodeFilterModel);
+      },
 
-                      // Insert new node at correct position
-                      var insertAfter = model.getXMLPosition(portalNode, "section");
-                      if(insertAfter){
-                        insertAfter.after(sectionSerialized);
-                      }
-                      else {
-                        portalNode.appendChild(sectionSerialized);
-                      }
+      /**
+       * Cleans up the given text so that it is XML-valid by escaping reserved characters, trimming white space, etc.
+       *
+       * @param {string} textString - The string to clean up
+       * @return {string} - The cleaned up string
+       */
+      cleanXMLText: function (textString) {
+        if (typeof textString != "string") return;
 
-                    }
+        textString = textString.trim();
 
-                  }, this)
-                };
-
-                /* ====  Serialize the EMLText elements ("acknowledgments") ==== */
-
-                var textFields = ["acknowledgments"];
-
-                _.each(textFields, function(field){
-
-                  var fieldName = field;
-
-                  // Get the EMLText model
-                  var emlTextModels = Array.isArray(this.get(field)) ? this.get(field) : [this.get(field)];
-                  if( ! emlTextModels.length ) return;
-
-                  // Get the node from the XML doc
-                  var nodes = $portalNode.children(fieldName);
-
-                  // Update the DOMs for each model
-                  _.each(emlTextModels, function(thisTextModel, i){
-                    //Don't serialize falsey values
-                    if(!thisTextModel) return;
-
-                    var node;
-
-                    //Get the existing node or create a new one
-                    if(nodes.length < i+1){
-                      node = xmlDoc.createElement(fieldName);
-                      this.getXMLPosition(portalNode, fieldName).after(node);
-                    }
-                    else {
-                       node = nodes[i];
-                    }
-
-                    var textModelSerialized = thisTextModel.updateDOM();
-
-                    //If the text model wasn't serialized correctly or resulted in nothing
-                    if(typeof textModelSerialized == "undefined" || !textModelSerialized){
-                      //Remove the existing node
-                      $(node).remove();
-                    }
-                    else{
-                      xmlDoc.adoptNode(textModelSerialized);
-                      $(node).replaceWith(textModelSerialized);
-                    }
-
-                  }, this);
-
-                  // Remove the extra nodes
-                  this.removeExtraNodes(nodes, emlTextModels);
-
-                }, this);
-
-                /* ====  Serialize awards ==== */
-
-                // Remove award node if it exists already
-                $portalNode.children("award").remove();
-
-                // Get new values
-                var awards = this.get("awards");
-
-                // Don't serialize falsey values
-                if(awards && awards.length>0){
-
-                  _.each(awards, function(award){
-
-                    // Make new node
-                    var awardSerialized = xmlDoc.createElement("award");
-
-                    // create the <award> subnodes
-                    _.map(award, function(value, nodeName){
-
-                      // serialize the simple text nodes
-                      if(nodeName != "funderLogo"){
-                        // Don't serialize falsey values
-                        if(value){
-                          // Make new sub-nodes
-                          var awardSubnodeSerialized = xmlDoc.createElement(nodeName);
-                          $(awardSubnodeSerialized).text(value);
-                          $(awardSerialized).append(awardSubnodeSerialized);
-                        }
-                      } else {
-                        // serialize "funderLogo" which is ImageType
-                        var funderLogoSerialized = value.updateDOM();
-                        xmlDoc.adoptNode(funderLogoSerialized);
-                        $(awardSerialized).append(funderLogoSerialized);
-                      }
-
-                    });
-
-                    // Insert new node at correct position
-                    var insertAfter = model.getXMLPosition(portalNode, "award");
-                    if(insertAfter){
-                      insertAfter.after(awardSerialized);
-                    }
-                    else{
-                      portalNode.appendChild(awardSerialized);
-                    }
-
-                  });
-
-                }
-
-                /* ====  Serialize associatedParties ==== */
-
-                // Remove element if it exists already
-                $portalNode.children("associatedParty").remove();
-
-                // Get new values
-                var parties = this.get("associatedParties");
-
-                // Don't serialize falsey values
-                if(parties){
-
-                  // Serialize each associatedParty
-                  _.each(parties, function(party){
-
-                    // Update the DOM of the EMLParty
-                    var partyEl  = party.updateDOM();
-                        partyDoc = $.parseXML(party.formatXML( $(partyEl)[0] ));
-
-                    // Make sure we don't insert empty EMLParty nodes into the EML
-                    if(partyDoc.childNodes.length){
-                      //Save a reference to the associated party element in the NodeList
-                      var assocPartyEl = partyDoc.childNodes[0];
-                      //Add the associated part element to the portal XML doc
-                      xmlDoc.adoptNode(assocPartyEl);
-
-                      // Get the last node of this type to insert after
-                      var insertAfter = $portalNode.children("associatedParty").last();
-
-                      // If there isn't a node found, find the EML position to insert after
-                      if( !insertAfter.length ) {
-                        insertAfter = model.getXMLPosition(portalNode, "associatedParty");
-                      }
-
-                      //Insert the party DOM at the insert position
-                      if ( insertAfter && insertAfter.length ){
-                        insertAfter.after(assocPartyEl);
-                      } else {
-                        portalNode.appendChild(assocPartyEl);
-                      }
-                    }
-                  });
-                }
-
-                try{
-                  /* ====  Serialize options (including map options) ==== */
-                  // This will only serialize the options named in `optNames` (below)
-                  // Functionality needed in order to serialize new or custom options
-
-                  // The standard list of options used in portals
-                  var optNames = this.get("optionNames");
-
-                  _.each(optNames, function(optName){
-
-                    //Get the value on the model
-                    var optValue = model.get(optName),
-                        existingValue;
-
-                    //Get the existing optionName element
-                    var matchingOption = $portalNode.children("option")
-                                                   .find("optionName:contains('" + optName + "')");
-
-                    //
-                    if( !matchingOption.length || matchingOption.first().text() != optName ){
-                      matchingOption = false;
-                    }
-                    else{
-                      //Get the value for this option from the Portal doc
-                      existingValue = matchingOption.siblings("optionValue").text();
-                    }
-
-                    // Don't serialize null or undefined values. Also don't serialize values that match the default model value
-                    if( (optValue || optValue === 0 || optValue === false) &&
-                        (optValue != model.defaults()[optName]) ){
-
-                      //Replace the existing option, if it exists
-                      if( matchingOption ){
-                        matchingOption.siblings("optionValue").text(optValue);
-                      }
-                      else{
-                        // Make new node
-                        // <optionName> and <optionValue> are subelements of <option>
-                        var optionSerialized   = xmlDoc.createElement("option"),
-                            optNameSerialized  = xmlDoc.createElement("optionName"),
-                            optValueSerialized = xmlDoc.createElement("optionValue");
-
-                        $(optNameSerialized).text(optName);
-                        $(optValueSerialized).text(optValue);
-
-                        $(optionSerialized).append(optNameSerialized, optValueSerialized);
-
-                        // Insert new node at correct position
-                        var insertAfter = model.getXMLPosition(portalNode, "option");
-
-                        if(insertAfter){
-                          insertAfter.after(optionSerialized);
-                        }
-
-                      }
-
-                    }
-                    else{
-                      //Remove the elements from the portal XML when the value is invalid
-                      if( matchingOption ){
-                        matchingOption.parent("option").remove();
-                      }
-                    }
-                  });
-                }
-                catch(e){
-                  console.error(e);
-                }
-
-                /* ====  Serialize UI FilterGroups (aka custom search filters) ==== */
-
-                // Get new filter group values
-                var filterGroups = this.get("filterGroups");
-
-                // Remove filter groups in the current objectDOM that are at the portal
-                // level. (don't use .find("filterGroup") as that would remove
-                // filterGroups that are nested in the definition
-                $portalNode.children("filterGroup").remove();
-
-                // Make a new node for each filter group in the model
-                _.each(filterGroups, function(filterGroup){
-
-                  filterGroupSerialized = filterGroup.updateDOM();
-
-                  if (filterGroupSerialized){
-                    //Add the new element to the XMLDocument
-                    xmlDoc.adoptNode(filterGroupSerialized);
-
-                    // Insert new node at correct position
-                    var insertAfter = model.getXMLPosition(portalNode, "filterGroup");
-
-                    if (insertAfter) {
-                      insertAfter.after(filterGroupSerialized);
-                    }
-                    else {
-                      portalNode.appendChild(filterGroupSerialized);
-                    }
-                  }
-
-                  
-                });
-
-                /* ====  Remove duplicates ==== */
-
-                //Do a final check to make sure there are no duplicate ids in the XML
-                var elementsWithIDs = $(xmlDoc).find("[id]"),
-                //Get an array of all the ids in this EML doc
-                    allIDs = _.map(elementsWithIDs, function(el){ return $(el).attr("id") });
-
-                //If there is at least one id in the EML...
-                if(allIDs && allIDs.length){
-                  //Boil the array down to just the unique values
-                  var uniqueIDs = _.uniq(allIDs);
-
-                  //If the unique array is shorter than the array of all ids,
-                  // then there is a duplicate somewhere
-                  if(uniqueIDs.length < allIDs.length){
-
-                    //For each element in the EML that has an id,
-                    _.each(elementsWithIDs, function(el){
-
-                      //Get the id for this element
-                      var id = $(el).attr("id");
-
-                      //If there is more than one element in the EML with this id,
-                      if( $(xmlDoc).find("[id='" + id + "']").length > 1 ){
-                        //And if it is not a unit node, which we don't want to change,
-                        if( !$(el).is("unit") )
-                          //Then change the id attribute to a random uuid
-                          $(el).attr("id", "urn-uuid-" + uuid.v4());
-                      }
-
-                    });
-
-                  }
-                }
-
-                // Convert xml to xmlString and return xmlString
-                xmlString = new XMLSerializer().serializeToString(xmlDoc);
-
-                //If there isn't an XML declaration, add one
-                if( xmlString.indexOf("<?xml") == -1 ){
-                  xmlString = '<?xml version="1.0" encoding="UTF-8"?>' + xmlString;
-                }
-
-                return xmlString;
-              }
-              catch(e){
-                console.error("Error while serializing the Portal XML document: ", e);
-                this.set("errorMessage", e.stack);
-                this.trigger("errorSaving", MetacatUI.appModel.get("portalEditSaveErrorMsg"));
-                return;
-              }
-            },
-
-            /**
-             * Checks whether the given sectionModel has been updated by the
-             * user, or whether all attributes match their default values.
-             * For a section's markdown, the default value is either an empty
-             * string or null. For a section's label, the default
-             * value is either an empty string or a string that begins with the
-             * value set to PortalModel.newSectionLabel. For all other attributes,
-             * the defaults are set in PortalSectionModel.defaults.
-             * @param {PortalSectionModel} sectionModel - The model to check against a default model
-             * @return {boolean} returns true if the sectionModel matches a default model, and false when at least one attribute differs
-            */
-            sectionIsDefault: function(sectionModel){
-
-              try{
-
-                var defaults = sectionModel.defaults(),
-                    currentMarkdown = sectionModel.get("content").get("markdown"),
-                    labelRegex = new RegExp("^" + this.newSectionLabel, "i");
-
-                // For each attribute, check whether it matches the default
-                if(
-                  // Check whether markdown matches the content that's
-                  // auto-filled or whether it's empty
-                  ( //currentMarkdown === this.markdownExample ||
-                    currentMarkdown == "" ||
-                    currentMarkdown == null
-                  ) &&
-                  ( sectionModel.get("image") === defaults.image ) &&
-                  ( sectionModel.get("introduction") === defaults.introduction ) &&
-                  // Check whether label starts with the default new page name,
-                  // or whether it's empty
-                  (
-                    labelRegex.test( sectionModel.get("label") ) ||
-                    sectionModel.get("label") == "" ||
-                    sectionModel.get("label") == null
-                  ) &&
-                  ( sectionModel.get("literatureCited") === defaults.literatureCited ) &&
-                  ( sectionModel.get("title") === defaults.title )
-                ){
-                  // All elements of the section match the default
-                  return true
-                } else {
-                  // At least one attribute of the section has been updated
-                  return false
-                }
-
-              }
-              catch(e){
-                // If there's a problem with this function for some reason,
-                // return false so that the section is serialized to avoid
-                // losing information
-                console.log("Failed to check whether section model is default. Serializing it anyway. Error message:" + e);
-                return false
-              }
-
-            },
-
-            /**
-             * Initialize the object XML for a brand spankin' new portal
-             * @inheritdoc
-             *
-            */
-            createXML: function() {
-              var format = MetacatUI.appModel.get("portalEditorSerializationFormat") ||
-                "https://purl.dataone.org/portals-1.1.0";
-              var xmlString = "<por:portal xmlns:por=\"" + format + "\"></por:portal>";
-              var xmlNew = $.parseXML(xmlString);
-              var portalNode = xmlNew.getElementsByTagName("por:portal")[0];
-
-              this.set("ownerDocument", portalNode.ownerDocument);
-              return(xmlNew);
-            },
-
-            /**
-             * Overrides the default Backbone.Model.validate.function() to
-             * check if this portal model has all the required values necessary
-             * to save to the server.
-             *
-             * @param {Object} [attrs] - A literal object of model attributes to validate.
-             * @param {Object} [options] - A literal object of options for this validation process
-             * @return {Object} If there are errors, an object comprising error
-             *                   messages. If no errors, returns nothing.
-            */
-            validate: function(attrs, options) {
-
-              try{
-
-                var errors = {},
-                    requiredFields = MetacatUI.appModel.get("portalEditorRequiredFields") || {};
-
-                //Execute the superclass validate() function
-                var collectionErrors = this.constructor.__super__.validate.call(this);
-                if( typeof collectionErrors == "object" && Object.keys(collectionErrors).length ){
-                  //Use the errors messages from the CollectionModel for this PortalModel
-                  errors = collectionErrors;
-                }
-
-                // ---- Validate the description and name ----
-                //Map the model attributes to the user-facing attribute name
-                var textFields = {
-                  description: "description",
-                  name: "title"
-                }
-                //Iterate over each text field
-                _.each( Object.keys(textFields), function(field){
-                  //If this field is required, and it is a string
-                  if( requiredFields[field] && typeof this.get(field) == "string" ){
-                    //If this is an empty string, set an error message
-                    if( !this.get(field).trim().length ){
-                      errors[field] = "A " + textFields[field] + " is required.";
-                    }
-                  }
-                  //If this field is required, and it's not a string at all, set an error message
-                  else if( requiredFields[field] ){
-                    errors[field] = "A " + textFields[field] + " is required.";
-                  }
-                }, this);
-
-                //---Validate the sections---
-                //Iterate over each section model
-                _.each( this.get("sections"), function(section){
-
-                  //Validate the section model
-                  var sectionErrors = section.validate();
-
-                  //If there is at least one error, then add an error to the PortalModel error list
-                  if( sectionErrors && Object.keys(sectionErrors).length ){
-                    errors.sections = "At least one section has an error";
-                  }
-
-                }, this);
-
-                //----Validate the logo----
-                if(requiredFields.logo && (!this.get("logo") ||
-                    !this.get("logo").get("identifier")))
-                {
-                  errors.logo = "A logo image is required";
-                } else if(this.get("logo")){
-                  logoErrors = this.get("logo").validate();
-                  if(logoErrors && Object.keys(logoErrors).length ){
-                    errors.logo = "A logo image is required";
-                  }
-                }
-
-                //---Validate the acknowledgmentsLogo---
-
-                var nonEmptyAckLogos = this.get("acknowledgmentsLogos").filter(function(portalImage){
-                  return(!portalImage.isEmpty())
-                });
-
-                if(
-                  requiredFields.acknowledgmentsLogos &&
-                  !nonEmptyAckLogos.length
-                ){
-                  errors.acknowledgmentsLogos = "At least one partner logo image is required.";
-                }
-                else if (
-                  nonEmptyAckLogos &&
-                  nonEmptyAckLogos.length
-                ){
-
-                  _.each( nonEmptyAckLogos, function(ackLogo){
-
-                    // Validate the portal image model
-                    var ackLogoErrors = ackLogo.validate();
-
-                    // If there is at least one error, then add an error to the PortalModel error list
-                    if( ackLogoErrors && Object.keys(ackLogoErrors).length ){
-                      errors.acknowledgmentsLogosImages = "At least one acknowledgment logo has an error";
-                    }
-
-                  }, this);
-
-                }
-
-                //TODO: Validate these other elements, listed below, as they are added to the portal editor
-
-                //---Validate the associatedParties---
-
-                //---Validate the acknowledgments---
-
-                //---Validate the award---
-
-                //---Validate the literatureCited---
-
-                //---Validate the filterGroups---
-
-                //Return the errors object
-                if( Object.keys(errors).length )
-                  return errors;
-                else{
-                  return;
-                }
-
-              }
-              catch(e){
-                console.error(e);
-              }
-
-            },
-
-            /**
-            * Checks for the existing block list for repository labels
-            * If at least one other Portal has the same label, then it is not available.
-            * @param {string} label - The label to query for
-            */
-            checkLabelAvailability: function(label){
-
-              //Validate the label set on the model if one isn't given
-              if(!label || typeof label != "string" ){
-                var label = this.get("label");
-                if(!label || typeof label != "string" ){
-                  //Trigger an error event
-                  this.trigger("errorValidatingLabel");
-                  console.error("error validating label, no label provided");
-                  return
-                }
-              }
-
-              var model = this;
-
-              if (!this.get("checkedNodeLabels")) {
-                // query CN to fetch the latest node data
-                model.updateNodeBlockList();
-
-                this.listenTo(this, "change:checkedNodeLabels", function(){
-                  this.checkPortalLabelAvailability(label);
-                });
-              }
-              else {
-                this.checkPortalLabelAvailability(label);
-              }
-
-            },
-
-            /**
-             * Queries the Solr discovery index for other Portal objects with this same label.
-             * Also, checks for the existing block list for repository labels
-             * If at least one other Portal has the same label, then it is not available.
-             * @param {string} label - The label to query for
-             */
-            checkPortalLabelAvailability: function(label) {
-              var model = this;
-
-              // Stop Listening to the node model. We only need to retrieve this node label once.
-              this.stopListening(this, "change:checkedNodeLabels", function(){
-                this.checkPortalLabelAvailability(label);
-              });
-
-              // Convert the block list to lower case for case insensitive match
-              var lowerCaseBlockList = this.get("labelBlockList").map(function(value) {
-                return value.toLowerCase();
-              });
-
-              // Check the existing blockList before making a Solr call
-              if (lowerCaseBlockList.indexOf(label.toLowerCase()) > -1) {
-                model.trigger("labelTaken");
-                return
-              }
-
-              // Query solr to see if other portals already use this label
-              var requestSettings = {
-                url: MetacatUI.appModel.get("queryServiceUrl") +
-                     "q=label:\"" + label + "\"" +
-                     " AND formatId:\"" + this.get("formatId") + "\"" +
-                     "&rows=0" +
-                     "&wt=json",
-                error: function(response) {
-                  model.trigger("errorValidatingLabel");
-                },
-                success: function(response){
-                  if( response.response.numFound > 0 ){
-                    //Add this label to the blockList so we don't have to query for it later
-                    var blockList = model.get("labelBlockList");
-                    if( Array.isArray(blockList) ){
-                      blockList.push(label);
-                    }
-
-                    model.trigger("labelTaken");
-                  } else {
-                    if( MetacatUI.appModel.get("alternateRepositories").length ){
-
-                      MetacatUI.appModel.setActiveAltRepo();
-                      var activeAltRepo = MetacatUI.appModel.getActiveAltRepo();
-                      if( activeAltRepo ){
-                        var requestSettings = {
-                          url: activeAltRepo.queryServiceUrl +
-                               "q=label:\"" + label + "\"" +
-                               " AND formatId:\"" + model.get("formatId") + "\"" +
-                               "&rows=0" +
-                               "&wt=json",
-                          error: function(response) {
-                            model.trigger("errorValidatingLabel");
-                          },
-                          success: function(response){
-                            if( response.response.numFound > 0 ){
-                              //Add this label to the blockList so we don't have to query for it later
-                              var blockList = model.get("labelBlockList");
-                              if( Array.isArray(blockList) ){
-                                blockList.push(label);
-                              }
-
-                              model.trigger("labelTaken");
-                            } else {
-                              model.trigger("labelAvailable");
-                            }
-                          }
-                        }
-                        //Attach the User auth info and send the request
-                        requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());
-                        $.ajax(requestSettings);
-                      }
-
-                    }
-                    else{
-                      model.trigger("labelAvailable");
-                    }
-                  }
-                }
-              }
-              //Attach the User auth info and send the request
-              requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());
-              $.ajax(requestSettings);
-            },
-
-
-
-            /**
-             * Queries the CN Solr to retrieve the updated BlockList
-             */
-            updateNodeBlockList: function(){
-              var model  = this;
-
-              $.ajax({
-                url: MetacatUI.appModel.get('nodeServiceUrl'),
-                dataType: "text",
-                error:  function(data, textStatus, xhr) {
-                  // if there is an error in retrieving the node list;
-                  // proceed with the existing node list to perform the checks
-                  model.checkPortalLabelAvailability()
-                },
-                success: function(data, textStatus, xhr) {
-
-                  var xmlResponse = $.parseXML(data) || null;
-                  if(!xmlResponse) return;
-
-                  // update the node block list on success
-                  model.saveNodeBlockList(xmlResponse);
-                }
-              });
-            },
-
-            /**
-             * Parses the retrieved XML document and saves the node information to the BlockList
-             *
-             * @param {XMLDocument} The XMLDocument returned from the fetch() AJAX call
-             */
-            saveNodeBlockList: function(xml){
-              var model = this,
-                children   = xml.children || xml.childNodes;
-
-              //Traverse the XML response to get the MN info
-              _.each(children, function(d1NodeList){
-
-                var d1NodeListChildren = d1NodeList.children || d1NodeList.childNodes;
-
-                //The first (and only) child should be the d1NodeList
-                _.each(d1NodeListChildren, function(thisNode){
-
-                  //Ignore parts of the XML that is not MN info
-                  if(!thisNode.attributes) return;
-
-                  //'node' will be a single node
-                  var node = {},
-                    nodeProperties = thisNode.children || thisNode.childNodes;
-
-                  //Grab information about this node from XML nodes
-                  _.each(nodeProperties, function(nodeProperty){
-
-                    if(nodeProperty.nodeName == "property")
-                      node[$(nodeProperty).attr("key")] = nodeProperty.textContent;
-                    else
-                      node[nodeProperty.nodeName] = nodeProperty.textContent;
-
-                    //Check if this member node has v2 read capabilities - important for the Package service
-                    if((nodeProperty.nodeName == "services") && nodeProperty.childNodes.length){
-                      var v2 = $(nodeProperty).find("service[name='MNRead'][version='v2'][available='true']").length;
-                      node["readv2"] = v2;
-                    }
-                  });
-
-                  //Grab information about this node from XLM attributes
-                  _.each(thisNode.attributes, function(attribute){
-                    node[attribute.nodeName] = attribute.nodeValue;
-                  });
-
-                  // Append Node name, node identifier and node short identifier to the array.
-                  // node identifier
-                  if (Array.isArray(model.get("labelBlockList")) && ((model.get("labelBlockList")).indexOf(node.identifier) < 0)) {
-                    model.get("labelBlockList").push(node.identifier);
-                  }
-
-                  // node name
-                  if(node.CN_node_name) {
-                    node.name = node.CN_node_name;
-                    if (Array.isArray(model.get("labelBlockList")) && ((model.get("labelBlockList")).indexOf(node.name) < 0)) {
-                      model.get("labelBlockList").push(node.name);
-                    }
-                  }
-
-                  // node short identifier
-                  node.shortIdentifier = node.identifier.substring(node.identifier.lastIndexOf(":") + 1);
-                  if (Array.isArray(model.get("labelBlockList")) && ((model.get("labelBlockList")).indexOf(node.shortIdentifier) < 0)) {
-                    model.get("labelBlockList").push(node.shortIdentifier);
-                  }
-
-                });
-              });
-
-              this.set("checkedNodeLabels", "true");
-            },
-
-            /**
-             * Removes nodes from the XML that do not have an accompanying model
-             * (i.e. nodes which were probably removed by the user during editing)
-             *
-             * @param {jQuery} nodes - The nodes to potentially remove
-             * @param {Model[]} models - The model to compare to
-            */
-            removeExtraNodes: function(nodes, models){
-              // Remove the extra nodes
-               var extraNodes =  nodes.length - models.length;
-               if(extraNodes > 0){
-                 for(var i = models.length; i < nodes.length; i++){
-                   $(nodes[i]).remove();
-                 }
-               }
-            },
-
-            /**
-             * Saves the portal XML document to the server using the DataONE API
-            */
-            save: function(){
-
-              var model = this;
-
-              // Remove empty filters from the custom portal search filters.
-              this.get("filterGroups").forEach(function(filterGroupModel){
-                filterGroupModel.get("filters").removeEmptyFilters();
-              }, this);
-
-              // Ensure empty filters (rule groups) are removed, including from
-              // within any nested filter groups
-              this.get("definitionFilters").removeEmptyFilters(true);
-
-              // Validate before we try anything else
-              if(!this.isValid()){
-                //Trigger the invalid and cancelSave events
-                this.trigger("invalid");
-                this.trigger("cancelSave");
-                //Don't save the model since it's invalid
-                return false;
-              }
-              else{
-                //Double-check that the label is available, if it was changed
-                if( (this.isNew() || this.get("originalLabel") != this.get("label")) && !this.get("labelDoubleChecked") ){
-                  //If the label is taken
-                  this.once("labelTaken", function(){
-
-                    //Stop listening to the label availability
-                    this.stopListening("labelAvailable");
-
-                    //Set that the label has been double-checked
-                    this.set("labelDoubleChecked", true);
-
-                    //If this portal is in a free trial of DataONE Plus, generate a new random label
-                    // and start the save process again
-                    if( MetacatUI.appModel.get("enableBookkeeperServices") ){
-
-                      var subscription = MetacatUI.appUserModel.get("dataoneSubscription");
-                      if(subscription && subscription.isTrialing()) {
-                        this.setRandomLabel();
-
-                        this.set("labelDoubleChecked", true);
-
-                        // Start the save process again
-                        this.save();
-
-                        return;
-                      }
-
-                    }
-                    else{
-                      //If the label is taken, trigger an invalid event
-                      this.trigger("invalid");
-                      //Trigger a cancellation of the save event
-                      this.trigger("cancelSave");
-                    }
-
-                  });
-
-                  this.once("labelAvailable", function(){
-                    this.stopListening("labelTaken");
-                    this.set("labelDoubleChecked", true);
-                    this.save();
-                  });
-
-                  // Check label availability
-                  this.checkLabelAvailability(this.get("label"));
-
-                  // console.log("Double checking label");
-
-                  //Don't proceed with the rest of the save
-                  return;
-                }
-                else{
-                  this.trigger("valid");
-                }
-
-              }
-
-              //Check if the checksum has been calculated yet.
-              if( !this.get("checksum") ){
-                // Serialize the XML
-                var xml = this.serialize();
-
-                //If there is no xml returned from the serialize() function, then there
-                // was an error, so don't save.
-                if( typeof xml === "undefined" || !xml ){
-                  //If no error message is set on the model, trigger an error now.
-                  // If there is an error message already, it means the error has already
-                  // been triggered inside the serialize() function.
-                  if( !this.get("errorMessage") ){
-                    this.trigger("errorSaving", MetacatUI.appModel.get("portalEditSaveErrorMsg"));
-                  }
-
-                  return;
-                }
-
-                var xmlBlob = new Blob([xml], {type : 'application/xml'});
-
-                //Set the Blob as the upload file
-                this.set("uploadFile", xmlBlob);
-
-                //When it is calculated, restart this function
-                this.off("checksumCalculated", this.save);
-                this.on("checksumCalculated", this.save);
-                //Calculate the checksum for this file
-                this.calculateChecksum();
-
-                //Exit this function until the checksum is done
-                return;
-              }
-
-              this.constructor.__super__.save.call(this);
-            },
-
-            /**
-            * Removes or hides the given section from this Portal
-            * @param {PortalSectionModel|string} section - Either the PortalSectionModel
-            * to remove, or the name of the section to remove. Some sections in the portals
-            * are not tied to PortalSectionModels, because they are created from other parts of the Portal
-            * document. For example, the Data, Metrics, and Members sections.
-            */
-            removeSection: function(section){
-
-              try{
-
-                //If this section is a string, remove it by adding custom options
-                if(typeof section == "string"){
-                  switch( section.toLowerCase() ){
-                    case "data":
-                      this.set("hideData", true);
-                      break;
-                    case "metrics":
-                      this.set("hideMetrics", true);
-                      break;
-                    case "members":
-                      this.set("hideMembers", true);
-                      break;
-                  }
-                }
-                //If this section is a section model, delete it from this Portal
-                else if( PortalSectionModel.prototype.isPrototypeOf(section) ){
-
-                  // Remove the section from the model's sections array object.
-                  // Use clone() to create new array reference and ensure change
-                  // event is tirggered.
-                  var sectionModels = _.clone(this.get("sections"));
-                  sectionModels.splice( $.inArray(section, sectionModels), 1);
-                  this.set({sections: sectionModels});
-                }
-                else{
-                  return;
-                }
-              }
-              catch(e){
-                console.error(e);
-              }
-
-            },
-
-            /**
-            * Adds the given section to this Portal
-            * @param {PortalSectionModel|string} section - Either the PortalSectionModel
-            * to add, or the name of the section to add. Some sections in the portals
-            * are not tied to PortalSectionModels, because they are created from other parts of the Portal
-            * document. For example, the Data, Metrics, and Members sections.
-            */
-            addSection: function(section){
-              try{
-                //If this section is a string, add it by adding custom options
-                if(typeof section == "string"){
-                  switch( section.toLowerCase() ){
-                    case "data":
-                      this.set("hideData", null);
-                      break;
-                    case "metrics":
-                      this.set("hideMetrics", null);
-                      break;
-                    case "members":
-                      this.set("hideMembers", null);
-                      break;
-                    case "freeform":
-
-                      // Add a new, blank markdown section with a default image
-                      var sectionModels = _.clone(this.get("sections")),
-                          newSection = new PortalSectionModel({
-                            portalModel: this,
-                            // Include a default image if some are configured.
-                            image: this.getRandomSectionImage()
-                          });
-
-                      sectionModels.push( newSection );
-                      this.set("sections", sectionModels);
-                      // Trigger event manually so we can just pass newSection
-                      this.trigger("addSection", newSection);
-                      break;
-                  }
-                }
-                // If this section is a section model, add it to this Portal
-                else if( PortalSectionModel.prototype.isPrototypeOf(section) ){
-                  var sectionModels = _.clone(this.get("sections"));
-                  sectionModels.push( section );
-                  this.set({sections: sectionModels});
-                  // trigger event manually so we can just pass newSection
-                  this.trigger("addSection", section);
-                }
-                else{
-                  return;
-                }
-              }
-              catch(e){
-                console.error(e);
-              }
-            },
-
-            /**
-             * removePortalImage - remove a PortalImage model from either the
-             * logo, sections, or acknowledgmentsLogos node of the portal model.
-             *
-             * @param  {Image} portalImage the portalImage model to remove
-             */
-            removePortalImage: function(portalImage){
-              try{
-                // find the portalImage to remove
-                switch (portalImage.get("nodeName")) {
-                  case "logo":
-                    if(portalImage === this.get("logo")){
-                      this.set("logo", this.defaults().logo);
-                    }
-                    break;
-                  case "image":
-                    _.each(this.get("sections"), function(section, i) {
-                      if(portalImage === section.get("image")){
-                        section.set("image", section.defaults().image)
-                      }
-                    });
-                    break;
-                  case "acknowledgmentsLogo":
-                    var ackLogos = _.clone(this.get("acknowledgmentsLogos"));
-                    ackLogos.splice( $.inArray(portalImage, ackLogos), 1);
-                    this.set({acknowledgmentsLogos: ackLogos});
-                    break;
-                }
-
-              } catch(e){
-                console.log("Failed to remove a portalImage model, error message: " + e);
-              }
-
-            },
-
-            /**
-            * Saves a reference to this Portal on the MetacatUI global object
-            */
-            cachePortal: function(){
-
-              if( this.get("id") ){
-                MetacatUI.portals = MetacatUI.portals || {};
-                MetacatUI.portals[this.get("id")] = this;
-              }
-
-              this.on("change:id", this.cachePortal);
-            },
-
-            /**
-            * Creates a URL for viewing more information about this object
-            * @return {string}
-            */
-            createViewURL: function(){
-              return MetacatUI.root + "/" + MetacatUI.appModel.get("portalTermPlural") + "/" + encodeURIComponent((this.get("label") || this.get("seriesId") || this.get("id")));
-            },
-
-            /**
-            * Sets attributes on this Portal using the given Member Node data
-            * @param {object} nodeInfoObject - A literal object taken from the NodeModel 'members' array
-            */
-            createNodeAttributes: function(nodeInfoObject) {
-              var nodePortalModel = {};
-
-              if (nodeInfoObject === undefined) {
-                nodeInfoObject = {}
-              }
-
-              //TODO - check for undefined for each of the nodeInfo properties
-
-              // Setting basic properties from the node info object
-              this.set("name", nodeInfoObject.name);
-              this.set("logo", nodeInfoObject.logo);
-              this.set("description", nodeInfoObject.description);
-
-              // Creating repo specific Filters
-              var nodeFilterModel = new FilterModel({
-                fields: ["datasource"],
-                values: [nodeInfoObject.identifier],
-                label: "Datasets for a repository",
-                matchSubstring: false,
-                operator: "OR"
-              });
-
-              // adding the filter in the node model
-              this.get("definitionFilters").add(nodeFilterModel);
-
-              // Set up the search model
-              this.get("searchModel").get("filters").add(nodeFilterModel);
-
-            },
-
-            /**
-            * Cleans up the given text so that it is XML-valid by escaping reserved characters, trimming white space, etc.
-            *
-            * @param {string} textString - The string to clean up
-            * @return {string} - The cleaned up string
-            */
-            cleanXMLText: function(textString){
-
-              if( typeof textString != "string" )
-                return;
-
-              textString = textString.trim();
-
-              //Check for XML/HTML elements
-              _.each(textString.match(/<\s*[^>]*>/g), function(xmlNode){
-
-                //Encode <, >, and </ substrings
-                var tagName = xmlNode.replace(/>/g, "&gt;");
-                tagName = tagName.replace(/</g, "&lt;");
-
-                //Replace the xmlNode in the full text string
-                textString = textString.replace(xmlNode, tagName);
-
-              });
-
-              //Remove Unicode characters that are not valid XML characters
-              //Create a regular expression that matches any character that is not a valid XML character
-              // (see https://www.w3.org/TR/xml/#charsets)
-              var invalidCharsRegEx = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/g;
-              textString = textString.replace(invalidCharsRegEx, "");
-
-              return textString;
-
-            },
-
-            /**
-            * Generates a random portal label for free trial portals
-            * @fires PortalModel#change:label
-            * @since 2.14.0
-            */
-            setRandomLabel: function() {
-
-              if( this.isNew() ){
-                var labelLength = MetacatUI.appModel.get("randomLabelNumericLength");
-                var randomGeneratedLabel = Math.floor(Math.pow(10,labelLength - 1) + Math.random() * ( 9 * Math.pow(10,labelLength - 1)));
-                randomGeneratedLabel = randomGeneratedLabel.toString();
-                this.set("label", randomGeneratedLabel);
-              }
-
-            }
+        //Check for XML/HTML elements
+        _.each(textString.match(/<\s*[^>]*>/g), function (xmlNode) {
+          //Encode <, >, and </ substrings
+          var tagName = xmlNode.replace(/>/g, "&gt;");
+          tagName = tagName.replace(/</g, "&lt;");
 
+          //Replace the xmlNode in the full text string
+          textString = textString.replace(xmlNode, tagName);
         });
 
-        return PortalModel;
-    });
+        //Remove Unicode characters that are not valid XML characters
+        //Create a regular expression that matches any character that is not a valid XML character
+        // (see https://www.w3.org/TR/xml/#charsets)
+        var invalidCharsRegEx =
+          /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/g;
+        textString = textString.replace(invalidCharsRegEx, "");
+
+        return textString;
+      },
+
+      /**
+       * Generates a random portal label for free trial portals
+       * @fires PortalModel#change:label
+       * @since 2.14.0
+       */
+      setRandomLabel: function () {
+        if (this.isNew()) {
+          var labelLength = MetacatUI.appModel.get("randomLabelNumericLength");
+          var randomGeneratedLabel = Math.floor(
+            Math.pow(10, labelLength - 1) +
+              Math.random() * (9 * Math.pow(10, labelLength - 1)),
+          );
+          randomGeneratedLabel = randomGeneratedLabel.toString();
+          this.set("label", randomGeneratedLabel);
+        }
+      },
+    },
+  );
+
+  return PortalModel;
+});
 

diff --git a/docs/docs/src_js_models_portals_PortalSectionModel.js.html b/docs/docs/src_js_models_portals_PortalSectionModel.js.html index ec1aeac25..fe8ad8a32 100644 --- a/docs/docs/src_js_models_portals_PortalSectionModel.js.html +++ b/docs/docs/src_js_models_portals_PortalSectionModel.js.html @@ -44,321 +44,336 @@

Source: src/js/models/portals/PortalSectionModel.js

-
/* global define */
-define(["jquery",
-        "underscore",
-        "backbone",
-        "models/portals/PortalImage",
-        "models/metadata/eml220/EMLText"
-    ],
-    function($, _, Backbone, PortalImage, EMLText) {
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/portals/PortalImage",
+  "models/metadata/eml220/EMLText",
+], function ($, _, Backbone, PortalImage, EMLText) {
+  /**
+   * @class PortalSectionModel
+   * @classdesc A Portal Section model represents the ContentSectionType from the portal schema
+   * @classcategory Models/Portals
+   * @extends Backbone.Model
+   */
+  var PortalSectionModel = Backbone.Model.extend(
+    /** @lends PortalSectionModel.prototype */ {
+      defaults: function () {
+        return {
+          label: "Untitled",
+          image: "",
+          title: "",
+          introduction: "",
+          content: new EMLText({
+            type: "content",
+            parentModel: this,
+          }),
+          literatureCited: null,
+          objectDOM: null,
+          sectionType: "",
+          portalModel: null,
+        };
+      },
 
       /**
-       * @class PortalSectionModel
-       * @classdesc A Portal Section model represents the ContentSectionType from the portal schema
-       * @classcategory Models/Portals
-       * @extends Backbone.Model
+       * Parses a <section> element from a portal document
+       *
+       *  @param {XMLElement} objectDOM - A ContentSectionType XML element from a portal document
+       *  @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
        */
-      var PortalSectionModel = Backbone.Model.extend(
-        /** @lends PortalSectionModel.prototype */{
-        defaults: function(){
-          return {
-            label: "Untitled",
-            image: "",
-            title: "",
-            introduction: "",
-            content: new EMLText({
-                          type: "content",
-                          parentModel: this
-                      }),
-            literatureCited: null,
-            objectDOM: null,
-            sectionType: "",
-            portalModel: null
-          }
-        },
-
-        /**
-         * Parses a <section> element from a portal document
-         *
-         *  @param {XMLElement} objectDOM - A ContentSectionType XML element from a portal document
-         *  @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
-        */
-        parse: function(objectDOM){
-
-          if(!objectDOM){
-            return {};
-          }
-
-          var $objectDOM = $(objectDOM),
-              modelJSON = {};
-
-          //Parse all the simple string elements
-          modelJSON.label = $objectDOM.children("label").text();
-          modelJSON.title = $objectDOM.children("title").text();
-          modelJSON.introduction = $objectDOM.children("introduction").text();
-
-          //Parse the image URL or identifier
-          var image = $objectDOM.children("image");
-          if( image.length ){
-            var portImageModel = new PortalImage({
-              objectDOM: image[0],
-              portalModel: this.get("portalModel")
-            });
-            portImageModel.set(portImageModel.parse());
-            modelJSON.image = portImageModel;
-          }
-
-          //Create an EMLText model for the section content
-          modelJSON.content = new EMLText({
-            objectDOM: $objectDOM.children("content")[0]
-          });
-          modelJSON.content.set(modelJSON.content.parse($objectDOM.children("content")));
+      parse: function (objectDOM) {
+        if (!objectDOM) {
+          return {};
+        }
 
-          return modelJSON;
+        var $objectDOM = $(objectDOM),
+          modelJSON = {};
 
-        },
+        //Parse all the simple string elements
+        modelJSON.label = $objectDOM.children("label").text();
+        modelJSON.title = $objectDOM.children("title").text();
+        modelJSON.introduction = $objectDOM.children("introduction").text();
 
-        /**
-         *  Makes a copy of the original XML DOM and updates it with the new values from the model.
-         *
-         *  @return {XMLElement} An updated ContentSectionType XML element from a portal document
-        */
-        updateDOM: function(){
+        //Parse the image URL or identifier
+        var image = $objectDOM.children("image");
+        if (image.length) {
+          var portImageModel = new PortalImage({
+            objectDOM: image[0],
+            portalModel: this.get("portalModel"),
+          });
+          portImageModel.set(portImageModel.parse());
+          modelJSON.image = portImageModel;
+        }
 
-          var objectDOM = this.get("objectDOM");
+        //Create an EMLText model for the section content
+        modelJSON.content = new EMLText({
+          objectDOM: $objectDOM.children("content")[0],
+        });
+        modelJSON.content.set(
+          modelJSON.content.parse($objectDOM.children("content")),
+        );
 
-          if (objectDOM) {
-            objectDOM = objectDOM.cloneNode(true);
-            $(objectDOM).empty();
-          } else {
-            // create an XML section element from scratch
-            var xmlText = "<section></section>",
-                objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
-                objectDOM = $(objectDOM).children()[0];
-          };
+        return modelJSON;
+      },
 
-          // Get and update the simple text strings (everything but content)
-          var sectionTextData = {
-            label: this.get("label"),
-            title: this.get("title"),
-            introduction: this.get("introduction")
-          };
+      /**
+       *  Makes a copy of the original XML DOM and updates it with the new values from the model.
+       *
+       *  @return {XMLElement} An updated ContentSectionType XML element from a portal document
+       */
+      updateDOM: function () {
+        var objectDOM = this.get("objectDOM");
+
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
+          $(objectDOM).empty();
+        } else {
+          // create an XML section element from scratch
+          var xmlText = "<section></section>",
+            objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
+            objectDOM = $(objectDOM).children()[0];
+        }
 
-          _.map(sectionTextData, function(value, nodeName){
+        // Get and update the simple text strings (everything but content)
+        var sectionTextData = {
+          label: this.get("label"),
+          title: this.get("title"),
+          introduction: this.get("introduction"),
+        };
 
+        _.map(
+          sectionTextData,
+          function (value, nodeName) {
             // Don't serialize default values, except for default label strings, since labels are required
-            if(value && (value != this.defaults()[nodeName] || (nodeName == "label" && typeof value == "string")) ){
+            if (
+              value &&
+              (value != this.defaults()[nodeName] ||
+                (nodeName == "label" && typeof value == "string"))
+            ) {
               // Make new sub-node
-              var sectionSubnodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
+              var sectionSubnodeSerialized =
+                objectDOM.ownerDocument.createElement(nodeName);
               $(sectionSubnodeSerialized).text(value);
 
               this.addUpdatedXMLNode(objectDOM, sectionSubnodeSerialized);
             }
             //If the value was removed from the model, then remove the element from the XML
-            else{
+            else {
               $(objectDOM).children(nodeName).remove();
             }
+          },
+          this,
+        );
+
+        //Update the image element
+        if (
+          this.get("image") &&
+          typeof this.get("image").updateDOM == "function"
+        ) {
+          var imageSerialized = this.get("image").updateDOM();
+
+          this.addUpdatedXMLNode(objectDOM, imageSerialized);
+        } else {
+          $(objectDOM).children("image").remove();
+        }
 
-          }, this);
-
-          //Update the image element
-          if( this.get("image") && typeof this.get("image").updateDOM == "function" ){
+        // Get markdown which is a child of content
+        var content = this.get("content");
 
-            var imageSerialized = this.get("image").updateDOM();
+        if (content) {
+          var contentSerialized = content.updateDOM("content");
 
-            this.addUpdatedXMLNode(objectDOM, imageSerialized);
-          }
-          else{
-            $(objectDOM).children("image").remove();
-          }
+          this.addUpdatedXMLNode(objectDOM, contentSerialized);
+        } else {
+          $(objectDOM).children("content").remove();
+        }
 
-          // Get markdown which is a child of content
-          var content = this.get("content");
+        //If nothing was serialized, return an empty string
+        if (!$(objectDOM).children().length) {
+          return "";
+        }
 
-          if(content){
-            var contentSerialized = content.updateDOM("content");
+        return objectDOM;
+      },
 
-            this.addUpdatedXMLNode(objectDOM, contentSerialized);
+      /**
+       * Takes the updated XML node and inserts it into the given object DOM in
+       * the correct position.
+       * @param {Element} objectDOM - The full object DOM for this model
+       * @param {Element} newElement - The updated element to insert into the object DOM
+       */
+      addUpdatedXMLNode: function (objectDOM, newElement) {
+        //If a parameter is missing, don't do anything
+        if (!objectDOM || !newElement) {
+          return;
+        }
 
-          }
-          else{
-            $(objectDOM).children("content").remove();
-          }
+        try {
+          //Get the node name of the new element
+          var nodeName = $(newElement)[0].nodeName;
+
+          if (nodeName) {
+            //Only insert the new element if there is content in it
+            if (
+              $(newElement).children().length ||
+              $(newElement).text().length
+            ) {
+              //Add the new element to the owner Document
+              objectDOM.ownerDocument.adoptNode(newElement);
+
+              //Get the existing node
+              var existingNodes = $(objectDOM).children(nodeName);
+
+              //Get the position that the image should be
+              var insertAfter = this.getXMLPosition(objectDOM, nodeName);
+
+              if (insertAfter) {
+                //Insert it into that position
+                $(insertAfter).after(newElement);
+              } else {
+                objectDOM.appendChild(newElement);
+              }
 
-          //If nothing was serialized, return an empty string
-          if( !$(objectDOM).children().length ){
-            return "";
+              existingNodes.remove();
+            }
           }
+        } catch (e) {
+          console.log(e);
+          return;
+        }
+      },
 
-          return objectDOM;
-
-        },
-
-        /**
-        * Takes the updated XML node and inserts it into the given object DOM in
-        * the correct position.
-        * @param {Element} objectDOM - The full object DOM for this model
-        * @param {Element} newElement - The updated element to insert into the object DOM
-        */
-        addUpdatedXMLNode: function(objectDOM, newElement){
+      /**
+       * Finds the node in the given portal XML document afterwhich the
+       * given node type should be inserted
+       *
+       * @param {Element} parentNode - The parent XML element
+       * @param {string} nodeName - The name of the node to be inserted
+       *                             into xml
+       * @return {(jQuery|boolean)} A jQuery object indicating a position,
+       *                            or false when nodeName is not in the
+       *                            portal schema
+       */
+      getXMLPosition: function (parentNode, nodeName) {
+        var nodeOrder = [
+          "label",
+          "title",
+          "introduction",
+          "image",
+          "content",
+          "option",
+        ];
+
+        var position = _.indexOf(nodeOrder, nodeName);
+
+        // First check that nodeName is in the list of nodes
+        if (position == -1) {
+          return false;
+        }
 
-          //If a parameter is missing, don't do anything
-          if( !objectDOM || !newElement ){
-            return;
+        // If there's already an occurence of nodeName...
+        if ($(parentNode).children(nodeName).length > 0) {
+          // ...insert it after the last occurence
+          return $(parentNode).children(nodeName).last();
+        } else {
+          // Go through each node in the node list and find the position
+          // after which this node will be inserted
+          for (var i = position - 1; i >= 0; i--) {
+            if ($(parentNode).children(nodeOrder[i]).length) {
+              return $(parentNode).children(nodeOrder[i]).last();
+            }
           }
+        }
 
-          try{
-            //Get the node name of the new element
-            var nodeName = $(newElement)[0].nodeName;
-
-            if( nodeName ){
-
-              //Only insert the new element if there is content in it
-              if( $(newElement).children().length || $(newElement).text().length ){
-
-                //Add the new element to the owner Document
-                objectDOM.ownerDocument.adoptNode(newElement);
+        return false;
+      },
 
-                //Get the existing node
-                var existingNodes = $(objectDOM).children(nodeName);
+      /**
+       * Overrides the default Backbone.Model.validate.function() to
+       * check if this PortalSection model has all the required values necessary
+       * to save to the server.
+       *
+       * @return {Object} If there are errors, an object comprising error
+       *                   messages. If no errors, returns nothing.
+       */
+      validate: function () {
+        try {
+          var errors = {},
+            requiredFields = MetacatUI.appModel.get(
+              "portalEditorRequiredFields",
+            );
+
+          //--Validate the label--
+          //Labels are always required
+          if (!this.get("label")) {
+            errors.label = "Please provide a page name.";
+          }
 
-                //Get the position that the image should be
-                var insertAfter = this.getXMLPosition(objectDOM, nodeName);
+          //---Validate the title---
+          //If section titles are required and there isn't one, set an error message
+          if (
+            requiredFields.sectionTitle &&
+            typeof this.get("title") == "string" &&
+            !this.get("title").length
+          ) {
+            errors.title = "Please provide a title for this page.";
+          }
 
-                if( insertAfter ){
-                  //Insert it into that position
-                  $(insertAfter).after(newElement);
-                } else {
-                  objectDOM.appendChild(newElement);
-                }
+          //---Validate the introduction---
+          //If section introductions are required and there isn't one, set an error message
+          if (
+            requiredFields.sectionIntroduction &&
+            typeof this.get("introduction") == "string" &&
+            !this.get("introduction").length
+          ) {
+            errors.introduction =
+              "Please provide some a sub-title or some introductory text for this page.";
+          }
 
-                existingNodes.remove();
-              }
-            }
+          //---Validate the section content---
+          //Content is always required
+          if (!this.get("content")) {
+            errors.markdown = "Please provide content for this page.";
           }
-          catch(e){
-            console.log(e);
-            return;
+          //Check if there is either markdown or an array of strings in the text attribute
+          else if (
+            !this.get("content").get("markdown") &&
+            !this.get("content").get("text").length
+          ) {
+            errors.markdown = "Please provide content for this page.";
           }
-
-        },
-
-        /**
-         * Finds the node in the given portal XML document afterwhich the
-         * given node type should be inserted
-         *
-         * @param {Element} parentNode - The parent XML element
-         * @param {string} nodeName - The name of the node to be inserted
-         *                             into xml
-         * @return {(jQuery|boolean)} A jQuery object indicating a position,
-         *                            or false when nodeName is not in the
-         *                            portal schema
-        */
-        getXMLPosition: function(parentNode, nodeName){
-
-          var nodeOrder = [ "label", "title", "introduction", "image", "content", "option"];
-
-          var position = _.indexOf(nodeOrder, nodeName);
-
-          // First check that nodeName is in the list of nodes
-          if ( position == -1 ) {
-              return false;
-          };
-
-          // If there's already an occurence of nodeName...
-          if($(parentNode).children(nodeName).length > 0){
-            // ...insert it after the last occurence
-            return $(parentNode).children(nodeName).last();
-          } else {
-            // Go through each node in the node list and find the position
-            // after which this node will be inserted
-            for (var i = position - 1; i >= 0; i--) {
-              if ( $(parentNode).children(nodeOrder[i]).length ) {
-                return $(parentNode).children(nodeOrder[i]).last();
-              }
-            }
+          //Check if the markdown hasn't been changed from the example markdown
+          else if (
+            this.get("content").get("markdown") ==
+            this.get("content").get("markdownExample")
+          ) {
+            errors.markdown = "Please provide content for this page.";
           }
 
-          return false;
-        },
-
-        /**
-         * Overrides the default Backbone.Model.validate.function() to
-         * check if this PortalSection model has all the required values necessary
-         * to save to the server.
-         *
-         * @return {Object} If there are errors, an object comprising error
-         *                   messages. If no errors, returns nothing.
-        */
-        validate: function(){
-
-          try{
-
-            var errors = {},
-                requiredFields = MetacatUI.appModel.get("portalEditorRequiredFields");
-
-            //--Validate the label--
-            //Labels are always required
-            if( !this.get("label") ){
-              errors.label = "Please provide a page name.";
-            }
-
-            //---Validate the title---
-            //If section titles are required and there isn't one, set an error message
-            if( requiredFields.sectionTitle &&
-                typeof this.get("title") == "string" &&
-                !this.get("title").length ){
-              errors.title = "Please provide a title for this page.";
-            }
-
-            //---Validate the introduction---
-            //If section introductions are required and there isn't one, set an error message
-            if( requiredFields.sectionIntroduction &&
-                typeof this.get("introduction") == "string" &&
-                !this.get("introduction").length ){
-              errors.introduction = "Please provide some a sub-title or some introductory text for this page.";
-            }
-
-            //---Validate the section content---
-            //Content is always required
-            if( !this.get("content") ){
-              errors.markdown = "Please provide content for this page.";
-            }
-            //Check if there is either markdown or an array of strings in the text attribute
-            else if( !this.get("content").get("markdown") && !this.get("content").get("text").length ){
-              errors.markdown = "Please provide content for this page.";
-            }
-            //Check if the markdown hasn't been changed from the example markdown
-            else if( this.get("content").get("markdown") == this.get("content").get("markdownExample") ){
-              errors.markdown = "Please provide content for this page.";
-            }
-
-            //---Validate the section image---
-
-            if(requiredFields.sectionImage && (!this.get("image") || this.get("image").isEmpty())){
-              errors.sectionImage = "A section image is required."
-            }
-
-            //Return the errors object
-            if( Object.keys(errors).length )
-              return errors;
-            else{
-              return;
-            }
+          //---Validate the section image---
 
+          if (
+            requiredFields.sectionImage &&
+            (!this.get("image") || this.get("image").isEmpty())
+          ) {
+            errors.sectionImage = "A section image is required.";
           }
-          catch(e){
-            console.error(e);
+
+          //Return the errors object
+          if (Object.keys(errors).length) return errors;
+          else {
             return;
           }
-
+        } catch (e) {
+          console.error(e);
+          return;
         }
+      },
+    },
+  );
 
-
-      });
-
-      return PortalSectionModel;
+  return PortalSectionModel;
 });
 
diff --git a/docs/docs/src_js_models_portals_PortalVizSectionModel.js.html b/docs/docs/src_js_models_portals_PortalVizSectionModel.js.html index dbdf5ecb1..8a5c1733e 100644 --- a/docs/docs/src_js_models_portals_PortalVizSectionModel.js.html +++ b/docs/docs/src_js_models_portals_PortalVizSectionModel.js.html @@ -44,200 +44,201 @@

Source: src/js/models/portals/PortalVizSectionModel.js
-
/* global define */
-define(["jquery",
-        "underscore",
-        "backbone",
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
   "models/portals/PortalSectionModel",
-  "models/maps/Map"
-    ],
-  function ($, _, Backbone, PortalSectionModel, Map) {
+  "models/maps/Map",
+], function ($, _, Backbone, PortalSectionModel, Map) {
+  /**
+   * @class PortalVizSectionModel
+   * @classdesc A Portal Section for Data Visualizations. This is still an experimental feature and not recommended for general use.
+   * @classcategory Models/Portals
+   * @extends PortalSectionModel
+   * @private
+   */
+  var PortalVizSectionModel = PortalSectionModel.extend(
+    /** @lends PortalVizSectionModel.prototype */ {
+      type: "PortalVizSection",
+
+      defaults: function () {
+        return _.extend(PortalSectionModel.prototype.defaults(), {
+          sectionType: "visualization",
+          visualizationType: "",
+          supportedVisualizationTypes: ["fever", "cesium"],
+        });
+      },
 
       /**
-       * @class PortalVizSectionModel
-       * @classdesc A Portal Section for Data Visualizations. This is still an experimental feature and not recommended for general use.
-       * @classcategory Models/Portals
-       * @extends PortalSectionModel
-       * @private
+       * Parses a <section> element from a portal document
+       *
+       *  @param {XMLElement} objectDOM - A ContentSectionType XML element from a portal document
+       *  @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
        */
-      var PortalVizSectionModel = PortalSectionModel.extend(
-        /** @lends PortalVizSectionModel.prototype */{
-
-        type: "PortalVizSection",
-
-        defaults: function(){
-          return _.extend(PortalSectionModel.prototype.defaults(), {
-            sectionType: "visualization",
-            visualizationType: "",
-            supportedVisualizationTypes: ["fever", "cesium"]
-          });
-        },
-
-        /**
-         * Parses a <section> element from a portal document
-         *
-         *  @param {XMLElement} objectDOM - A ContentSectionType XML element from a portal document
-         *  @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
-        */
-        parse: function(objectDOM){
-
-          if(!objectDOM){
-            return {};
-          }
+      parse: function (objectDOM) {
+        if (!objectDOM) {
+          return {};
+        }
 
-          //Create a jQuery object of the XML DOM
-          var $objectDOM = $(objectDOM),
+        //Create a jQuery object of the XML DOM
+        var $objectDOM = $(objectDOM),
           //Parse the XML using the parent class, PortalSectionModel.parse()
-              modelJSON  = this.constructor.__super__.parse(objectDOM);
+          modelJSON = this.constructor.__super__.parse(objectDOM);
 
-          //Parse the visualization type
-          var allOptions = $objectDOM.children("option"),
-              vizType = "";
+        //Parse the visualization type
+        var allOptions = $objectDOM.children("option"),
+          vizType = "";
 
-          var vizTypeNode = allOptions.find("optionName:contains(visualizationType)");
-          if( vizTypeNode.length ){
-            vizType = vizTypeNode.first().siblings("optionValue").text();
+        var vizTypeNode = allOptions.find(
+          "optionName:contains(visualizationType)",
+        );
+        if (vizTypeNode.length) {
+          vizType = vizTypeNode.first().siblings("optionValue").text();
 
-            //Right now, only support "fever" as a visualization type, until this feature is expanded.
-            if(vizType == "fever"){
+          //Right now, only support "fever" as a visualization type, until this feature is expanded.
+          if (vizType == "fever") {
             //  modelJSON.visualizationType = "fever";
-            }
+          }
 
-            var vizTypes = this.get("supportedVisualizationTypes");
-            if( Array.isArray(vizTypes) && vizTypes.includes(vizType) ){
-              modelJSON.visualizationType = vizType;
-            }
+          var vizTypes = this.get("supportedVisualizationTypes");
+          if (Array.isArray(vizTypes) && vizTypes.includes(vizType)) {
+            modelJSON.visualizationType = vizType;
+          }
 
-            // Find the map configuration JSON in the section option, if there is one.
-            if (vizType == "cesium") {
-              var mapConfigNode = allOptions.find("optionName:contains(mapConfig)");
-              var mapConfig = {};
-              if (mapConfigNode.length) {
-                mapConfig = mapConfigNode.first().siblings("optionValue").text();
-                if (mapConfig && mapConfig.length) {
-                  mapConfig = JSON.parse(mapConfig)
-                }
+          // Find the map configuration JSON in the section option, if there is one.
+          if (vizType == "cesium") {
+            var mapConfigNode = allOptions.find(
+              "optionName:contains(mapConfig)",
+            );
+            var mapConfig = {};
+            if (mapConfigNode.length) {
+              mapConfig = mapConfigNode.first().siblings("optionValue").text();
+              if (mapConfig && mapConfig.length) {
+                mapConfig = JSON.parse(mapConfig);
               }
-              modelJSON.mapModel = new Map(mapConfig)
             }
-
+            modelJSON.mapModel = new Map(mapConfig);
           }
+        }
 
-          return modelJSON;
-
-        },
-
-        /**
-         *  Makes a copy of the original XML DOM and updates it with the new values from the model.
-         *  For now, this function only updates the label. All other parts of Viz sections are not editable
-         * in MetacatUI, since this is still an experimental feature.
-         *
-         *  @return {XMLElement} An updated ContentSectionType XML element from a portal document
-         */
-        updateDOM: function(){
+        return modelJSON;
+      },
 
-          var objectDOM = this.get("objectDOM");
+      /**
+       *  Makes a copy of the original XML DOM and updates it with the new values from the model.
+       *  For now, this function only updates the label. All other parts of Viz sections are not editable
+       * in MetacatUI, since this is still an experimental feature.
+       *
+       *  @return {XMLElement} An updated ContentSectionType XML element from a portal document
+       */
+      updateDOM: function () {
+        var objectDOM = this.get("objectDOM");
 
-          //Clone the DOM if it exists already
-          if (objectDOM) {
-            objectDOM = objectDOM.cloneNode(true);
+        //Clone the DOM if it exists already
+        if (objectDOM) {
+          objectDOM = objectDOM.cloneNode(true);
           //Or create a new DOM
-          } else {
-            // create an XML section element from scratch
-            var xmlText = "<section>  <content>FEVer visualization</content><option><optionName>sectionType</optionName><optionValue>visualization</optionValue>" +
-                          "</option><option><optionName>visualizationType</optionName><optionValue>fever</optionValue></option></section>",
-                objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
-                objectDOM = $(objectDOM).children()[0];
-          };
-
-          // Get and update the simple text strings (everything but content)
-          var sectionTextData = {
-            label: this.get("label")
-          };
+        } else {
+          // create an XML section element from scratch
+          var xmlText =
+              "<section>  <content>FEVer visualization</content><option><optionName>sectionType</optionName><optionValue>visualization</optionValue>" +
+              "</option><option><optionName>visualizationType</optionName><optionValue>fever</optionValue></option></section>",
+            objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
+            objectDOM = $(objectDOM).children()[0];
+        }
 
-          _.map(sectionTextData, function(value, nodeName){
+        // Get and update the simple text strings (everything but content)
+        var sectionTextData = {
+          label: this.get("label"),
+        };
 
+        _.map(
+          sectionTextData,
+          function (value, nodeName) {
             // Don't serialize default values, except for default label strings, since labels are required
-            if(value && (value != this.defaults()[nodeName] || (nodeName == "label" && typeof value == "string")) ){
+            if (
+              value &&
+              (value != this.defaults()[nodeName] ||
+                (nodeName == "label" && typeof value == "string"))
+            ) {
               // Make new sub-node
-              var sectionSubnodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
+              var sectionSubnodeSerialized =
+                objectDOM.ownerDocument.createElement(nodeName);
               $(sectionSubnodeSerialized).text(value);
 
               this.addUpdatedXMLNode(objectDOM, sectionSubnodeSerialized);
             }
             //If the value was removed from the model, then remove the element from the XML
-            else{
+            else {
               $(objectDOM).children(nodeName).remove();
             }
-
-          }, this);
-
-          //Make sure the content element is valid
-          var contentEl = $(objectDOM).children("content");
-          if( contentEl.length ){
-              //If there is content in the content element
-              if( contentEl[0].childNodes.length ){
-                //If there is only text in the <content> element, we need to wrap it in a <markdown> element so it's schema valid
-                if( contentEl[0].childNodes[0].nodeType == 3 ){
-                  $(contentEl[0]).html("<markdown>" + contentEl[0].childNodes[0].textContent + "</markdown>");
-                }
-              }
-          }
-
-          //If nothing was serialized, return an empty string
-          if( !$(objectDOM).children().length ){
-            return "";
+          },
+          this,
+        );
+
+        //Make sure the content element is valid
+        var contentEl = $(objectDOM).children("content");
+        if (contentEl.length) {
+          //If there is content in the content element
+          if (contentEl[0].childNodes.length) {
+            //If there is only text in the <content> element, we need to wrap it in a <markdown> element so it's schema valid
+            if (contentEl[0].childNodes[0].nodeType == 3) {
+              $(contentEl[0]).html(
+                "<markdown>" +
+                  contentEl[0].childNodes[0].textContent +
+                  "</markdown>",
+              );
+            }
           }
+        }
 
-          return objectDOM;
-
-        },
-
-        /**
-         * Overrides the default Backbone.Model.validate.function() to
-         * check if this PortalSection model has all the required values necessary
-         * to save to the server.
-         *
-         * @return {Object} If there are errors, an object comprising error
-         *                   messages. If no errors, returns nothing.
-        */
-        validate: function(){
-
-          try{
-
-            var errors = {};
-
-            //--Validate the label--
-            //Labels are always required
-            if( !this.get("label") ){
-              errors.label = "Please provide a page name.";
-            }
+        //If nothing was serialized, return an empty string
+        if (!$(objectDOM).children().length) {
+          return "";
+        }
 
-            //---Validate the section content---
-            //Content is always required, but for visualizations, we can just input dummy content
-            if( !this.get("content") ){
-              this.set("content", "visualization");
-            }
+        return objectDOM;
+      },
 
-            //Return the errors object
-            if( Object.keys(errors).length )
-              return errors;
-            else{
-              return;
-            }
+      /**
+       * Overrides the default Backbone.Model.validate.function() to
+       * check if this PortalSection model has all the required values necessary
+       * to save to the server.
+       *
+       * @return {Object} If there are errors, an object comprising error
+       *                   messages. If no errors, returns nothing.
+       */
+      validate: function () {
+        try {
+          var errors = {};
+
+          //--Validate the label--
+          //Labels are always required
+          if (!this.get("label")) {
+            errors.label = "Please provide a page name.";
+          }
 
+          //---Validate the section content---
+          //Content is always required, but for visualizations, we can just input dummy content
+          if (!this.get("content")) {
+            this.set("content", "visualization");
           }
-          catch(e){
-            console.error(e);
+
+          //Return the errors object
+          if (Object.keys(errors).length) return errors;
+          else {
             return;
           }
-
+        } catch (e) {
+          console.error(e);
+          return;
         }
+      },
+    },
+  );
 
-
-      });
-
-      return PortalVizSectionModel;
+  return PortalVizSectionModel;
 });
 
diff --git a/docs/docs/src_js_models_projects_Project.js.html b/docs/docs/src_js_models_projects_Project.js.html index 0f4253c03..8cfe38328 100644 --- a/docs/docs/src_js_models_projects_Project.js.html +++ b/docs/docs/src_js_models_projects_Project.js.html @@ -44,78 +44,83 @@

Source: src/js/models/projects/Project.js

-
define(['jquery', 'backbone'],
-    function($, Backbone) {
-        'use strict';
-        /**
-         * @class Project
-         * @classdesc A Project model represents a single instance of a project. This can be
-         *          used for a projects list view populating EML projects. It also supports loading
-         *          projects from a third-party API in case projects information is located outside of
-         *          metacat.
-         * @classcategory Models/Projects
-         * @since 2.22.0
-         * @extends Backbone.Model
-         */
-
-        var Project = Backbone.Model.extend(/** @lends Project.prototype */{
-
-            idAttribute: "id",
-            defaults: {           // Set the project model attributes defaults here.
-                id: undefined,
-                title: undefined,
-            },
-            authToken: undefined,
-            urlBase: undefined,
-
-            /** Builds  from the urlBase **/
-            urlRoot: function(){
-                if(this.urlBase)
-                {
-                    return new URL(new URL(this.urlBase).pathname + this.urlEndpoint, this.urlBase).href
-                }
-                else {
-                    return undefined
-                }
-            },
-            /**
-             * Override backbone's parse to set the data after the request returns from the server
-             */
-            parse: function(response, options) {
-                // Add any custom data structure code here.
-                return response
-            },
-            /**
-             * Override backbone's sync to set the auth token
-             */
-            sync: function(method, model, options) {
-
-                if (this.authToken) {
-                    if (options.headers === undefined){
-                        options.headers = {}
-                    }
-                    options.headers["Authorization"] = "Bearer " + this.authToken
-                }
-
-                if(this.urlBase)
-                    return Backbone.Model.prototype.sync.apply(this, [method, model, options])
-            },
-            /**
-             * Initializing the Model objects project variables. The projects are retrieved from the
-             * model url service
-             */
-            initialize: function (options) {
-                if (MetacatUI && MetacatUI.appModel)  this.urlBase = MetacatUI.appModel.get("projectsApiUrl")
-                if (options.authToken)  this.authToken = options.authToken
-                if (options.urlBase) this.urlBase = options.urlBase;
-
-                Backbone.Model.prototype.initialize.apply(this, options)
-            }
-
-        });
-        return Project;
-    }
-);
+
define(["jquery", "backbone"], function ($, Backbone) {
+  "use strict";
+  /**
+   * @class Project
+   * @classdesc A Project model represents a single instance of a project. This can be
+   *          used for a projects list view populating EML projects. It also supports loading
+   *          projects from a third-party API in case projects information is located outside of
+   *          metacat.
+   * @classcategory Models/Projects
+   * @since 2.22.0
+   * @extends Backbone.Model
+   */
+
+  var Project = Backbone.Model.extend(
+    /** @lends Project.prototype */ {
+      idAttribute: "id",
+      defaults: {
+        // Set the project model attributes defaults here.
+        id: undefined,
+        title: undefined,
+      },
+      authToken: undefined,
+      urlBase: undefined,
+
+      /** Builds  from the urlBase **/
+      urlRoot: function () {
+        if (this.urlBase) {
+          return new URL(
+            new URL(this.urlBase).pathname + this.urlEndpoint,
+            this.urlBase,
+          ).href;
+        } else {
+          return undefined;
+        }
+      },
+      /**
+       * Override backbone's parse to set the data after the request returns from the server
+       */
+      parse: function (response, options) {
+        // Add any custom data structure code here.
+        return response;
+      },
+      /**
+       * Override backbone's sync to set the auth token
+       */
+      sync: function (method, model, options) {
+        if (this.authToken) {
+          if (options.headers === undefined) {
+            options.headers = {};
+          }
+          options.headers["Authorization"] = "Bearer " + this.authToken;
+        }
+
+        if (this.urlBase)
+          return Backbone.Model.prototype.sync.apply(this, [
+            method,
+            model,
+            options,
+          ]);
+      },
+      /**
+       * Initializing the Model objects project variables. The projects are retrieved from the
+       * model url service
+       */
+      initialize: function (options) {
+        if (MetacatUI && MetacatUI.appModel)
+          this.urlBase = MetacatUI.appModel.get("projectsApiUrl");
+        if (options.authToken) this.authToken = options.authToken;
+        if (options.urlBase) this.urlBase = options.urlBase;
+
+        Backbone.Model.prototype.initialize.apply(this, options);
+      },
+    },
+  );
+  return Project;
+});
+
diff --git a/docs/docs/src_js_models_queryFields_QueryField.js.html b/docs/docs/src_js_models_queryFields_QueryField.js.html index b536b8a4d..788698e38 100644 --- a/docs/docs/src_js_models_queryFields_QueryField.js.html +++ b/docs/docs/src_js_models_queryFields_QueryField.js.html @@ -44,34 +44,30 @@

Source: src/js/models/queryFields/QueryField.js

-
/* global define */
-define(
-  ['jquery', 'underscore', 'backbone'],
-  function($, _, Backbone) {
-    "use strict";
-
-    /**
-     * @class QueryField
-     * @classdesc A QueryField reprsents one of the fields supported by the DataONE
-     * Solr index, as provided by the DataONE API
-     * CNRead.getQueryEngineDescription() function. For more information, see:
-     * https://indexer-documentation.readthedocs.io/en/latest/generated/solr_schema.html
-     * https://dataone-architecture-documentation.readthedocs.io/en/latest/design/SearchMetadata.html
-     * @classcategory Models/QueryFields
-     * @name QueryField
-     * @since 2.14.0
-     * @extends Backbone.Model
-     */
-    var QueryField = Backbone.Model.extend(
-      /** @lends QueryField.prototype */ {
-
+            
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  "use strict";
+
+  /**
+   * @class QueryField
+   * @classdesc A QueryField reprsents one of the fields supported by the DataONE
+   * Solr index, as provided by the DataONE API
+   * CNRead.getQueryEngineDescription() function. For more information, see:
+   * https://indexer-documentation.readthedocs.io/en/latest/generated/solr_schema.html
+   * https://dataone-architecture-documentation.readthedocs.io/en/latest/design/SearchMetadata.html
+   * @classcategory Models/QueryFields
+   * @name QueryField
+   * @since 2.14.0
+   * @extends Backbone.Model
+   */
+  var QueryField = Backbone.Model.extend(
+    /** @lends QueryField.prototype */ {
       /**
        * Overrides the default Backbone.Model.defaults() function to
        * specify default attributes for the query fields model
        *
        * @return {object}
        */
-      defaults: function() {
+      defaults: function () {
         return {
           name: null,
           type: null,
@@ -86,55 +82,54 @@ 

Source: src/js/models/queryFields/QueryField.js

categoryOrder: null, icon: null, label: null, - caseSensitive: false + caseSensitive: false, }; }, - /** * initialize - When a new query field is created, set the label, category, * and filterType attributes */ - initialize: function(attrs, options){ - + initialize: function (attrs, options) { // Set a human-readable label var label = this.getReadableName(); - if(label){ + if (label) { this.set("label", label); } // Set an easier to read description var friendlyDescription = this.getFriendlyDescription(); - if(friendlyDescription){ + if (friendlyDescription) { this.set("friendlyDescription", friendlyDescription); } // Set a category and icon var category = this.getCategory(); - if (category){ - if(category.icon){ + if (category) { + if (category.icon) { this.set("icon", category.icon); } - if(category.label){ + if (category.label) { this.set("category", category.label); } - if(category.index || category.index === 0){ + if (category.index || category.index === 0) { this.set("categoryOrder", category.index); } } // Set a filter type var filterType = this.getFilterType(); - if (filterType){ - this.set("filterType", filterType) + if (filterType) { + this.set("filterType", filterType); } // Indicate when the field is case-sensitive - var isCaseSensitive = this.caseSensitiveTypes().includes(this.get("type")); - if(isCaseSensitive){ - this.set("caseSensitive", true) + var isCaseSensitive = this.caseSensitiveTypes().includes( + this.get("type"), + ); + if (isCaseSensitive) { + this.set("caseSensitive", true); } - }, /** @@ -143,7 +138,7 @@

Source: src/js/models/queryFields/QueryField.js

* * @return {object} A map of field names as keys and aliases (string) as values. */ - aliases: function(){ + aliases: function () { return { abstract: "Abstract", author: "First Creator Full Name", @@ -215,7 +210,7 @@

Source: src/js/models/queryFields/QueryField.js

prov_wasExecutedByUser: "Was executed by user", prov_wasGeneratedBy: "Was generated by", prov_wasInformedBy: "Was informed by", - } + }; }, /** @@ -224,74 +219,121 @@

Source: src/js/models/queryFields/QueryField.js

* * @return {object} A map of field names as keys and descriptions (string) as values. */ - descriptions: function(){ + descriptions: function () { return { - serviceDescription: "A full description of the service that can be used to download the dataset", - placeKey: "A location name, as assigned by the dataset creator. (FGDC metadata only)", - serviceTitle: "A short title of the service that can be used to download the dataset", - awardNumber: "A unique identifier used by a project funder to uniquely identify an award associated with the dataset.", - funderIdentifier: "An identifier that uniquely identifies the organization that funded the dataset.", - eastBoundCoord: "Eastern most longitude of the spatial extent, in decimal degrees, WGS84", - serviceCoupling: "Either 'tight', 'mixed', or 'loose'. Tight coupled service work only on the data described by this metadata document. Loose coupling means service works on any data. Mixed coupling means service works on data described by this metadata document but may work on other data.", - fundingText: "General information about the funding for a project", - isService: "If true, the dataset is available by a download service hosted by the member repository.", - isPartOf: "Include datasets that have been added to this data collection by the portal or dataset owners", - isPublic: "Include or exclude datasets that are available for anyone to view", - northBoundCoord: "Northern most latitude of the spatial extent, in decimal degrees, WGS84", - replicationAllowed: "Only include datasets that are allowed to be replicated to another member repo", - writePermission: "People or groups who can view and edit the dataset", - readPermission: "People or groups who can view the dataset if it's still unpublished", - changePermission: "People or groups who have been granted complete ownership of the dataset", - rightsHolder: "People or groups who have complete ownership of the dataset", + serviceDescription: + "A full description of the service that can be used to download the dataset", + placeKey: + "A location name, as assigned by the dataset creator. (FGDC metadata only)", + serviceTitle: + "A short title of the service that can be used to download the dataset", + awardNumber: + "A unique identifier used by a project funder to uniquely identify an award associated with the dataset.", + funderIdentifier: + "An identifier that uniquely identifies the organization that funded the dataset.", + eastBoundCoord: + "Eastern most longitude of the spatial extent, in decimal degrees, WGS84", + serviceCoupling: + "Either 'tight', 'mixed', or 'loose'. Tight coupled service work only on the data described by this metadata document. Loose coupling means service works on any data. Mixed coupling means service works on data described by this metadata document but may work on other data.", + fundingText: "General information about the funding for a project", + isService: + "If true, the dataset is available by a download service hosted by the member repository.", + isPartOf: + "Include datasets that have been added to this data collection by the portal or dataset owners", + isPublic: + "Include or exclude datasets that are available for anyone to view", + northBoundCoord: + "Northern most latitude of the spatial extent, in decimal degrees, WGS84", + replicationAllowed: + "Only include datasets that are allowed to be replicated to another member repo", + writePermission: "People or groups who can view and edit the dataset", + readPermission: + "People or groups who can view the dataset if it's still unpublished", + changePermission: + "People or groups who have been granted complete ownership of the dataset", + rightsHolder: + "People or groups who have complete ownership of the dataset", numberReplicas: "Requested number of replicas for the dataset", text: "Search all of the fields listed here", - southBoundCoord: "Southern most latitude of the spatial extent, in decimal degrees, WGS84", - projectText: "The authorized name of a research effort for which data is collected. This name is often reduced to a convenient abbreviation or acronym. All investigators involved in a project should use a common, agreed-upon name.", + southBoundCoord: + "Southern most latitude of the spatial extent, in decimal degrees, WGS84", + projectText: + "The authorized name of a research effort for which data is collected. This name is often reduced to a convenient abbreviation or acronym. All investigators involved in a project should use a common, agreed-upon name.", attributeLabel: "The data attribute description label", - attributeName: "The data attribute description name", + attributeName: "The data attribute description name", attributeDescription: "The data attribute description text", - attributeUnit: "The data attribute description unit", - serviceOutput: "The data formats available for download from the data service", - dateUploaded: "The date that the content of the dataset was last updated", - datePublished: "The date that the dataset was published, as specified in the metadata.", - replicaVerifiedDate: "The date that the dataset was replicated to another member repository", - dateModified: "The date that the system metadata was last updated (e.g. files renamed, file format changed)", + attributeUnit: "The data attribute description unit", + serviceOutput: + "The data formats available for download from the data service", + dateUploaded: + "The date that the content of the dataset was last updated", + datePublished: + "The date that the dataset was published, as specified in the metadata.", + replicaVerifiedDate: + "The date that the dataset was replicated to another member repository", + dateModified: + "The date that the system metadata was last updated (e.g. files renamed, file format changed)", fileName: "The file name of the metadata file", - attribute: "The full attribute metadata, which may include a description, label, name, and unit", - originText: "The full name of all people and organizations who are responsible for creating the dataset", - author: "The full name of the dataset creator who is listed first in the metadata", - authorGivenNameSort: "The first name of the dataset creator who is listed first in the metadata", - authorSurNameSort: "The last name of the dataset creator who is listed first in the metadata", + attribute: + "The full attribute metadata, which may include a description, label, name, and unit", + originText: + "The full name of all people and organizations who are responsible for creating the dataset", + author: + "The full name of the dataset creator who is listed first in the metadata", + authorGivenNameSort: + "The first name of the dataset creator who is listed first in the metadata", + authorSurNameSort: + "The last name of the dataset creator who is listed first in the metadata", abstract: "The full text of the abstract", - resourceMap: "The identifier of the resource map of the dataset", + resourceMap: "The identifier of the resource map of the dataset", authorLastName: "The last name of every creator of the dataset", - investigatorText: "The last names of all people responsible for creating the dataset", - blockedReplicationMN: "The member repositories that are blocked from holding replicas of the dataset", - replicaMN: "The member repositories that are holding copies of the dataset", - preferredReplicationMN: "The member repositories that are preferred replication targets of the dataset", - authoritativeMN: "The member repository that currently holds this dataset, which may differ from the repository that the dataset is originally from.", - datasource: "The member repository that originally contributed the dataset.", + investigatorText: + "The last names of all people responsible for creating the dataset", + blockedReplicationMN: + "The member repositories that are blocked from holding replicas of the dataset", + replicaMN: + "The member repositories that are holding copies of the dataset", + preferredReplicationMN: + "The member repositories that are preferred replication targets of the dataset", + authoritativeMN: + "The member repository that currently holds this dataset, which may differ from the repository that the dataset is originally from.", + datasource: + "The member repository that originally contributed the dataset.", formatId: "The metadata standard or format type", - contactOrganizationText: "The name of all organizations responsible for creating the dataset", - geoform: "The name of the general form in which the item's geospatial data is presented", - funderName: "The name of the organization that funded the project or dataset.", - purpose: "The purpose describes why the dataset was created. (Limited to FGDC metadata only)", + contactOrganizationText: + "The name of all organizations responsible for creating the dataset", + geoform: + "The name of the general form in which the item's geospatial data is presented", + funderName: + "The name of the organization that funded the project or dataset.", + purpose: + "The purpose describes why the dataset was created. (Limited to FGDC metadata only)", size: "The size of the metadata file, in bytes.", - replicationStatus: "The status of the DataONE replication process for this dataset. (completed, failed, queued, requested)", - awardTitle: "The title of the award or grant that funded the dataset.", - serviceType: "The type of service that is available to download the dataset from the member repository", - documents: "The unique identifier of a data object in the dataset.", + replicationStatus: + "The status of the DataONE replication process for this dataset. (completed, failed, queued, requested)", + awardTitle: + "The title of the award or grant that funded the dataset.", + serviceType: + "The type of service that is available to download the dataset from the member repository", + documents: "The unique identifier of a data object in the dataset.", seriesId: "The unique identifier of the dataset version chain", identifier: "The unique identifier or DOI of the metadata", sem_annotation: "The URI or identifier of the semantic annotation ", - serviceEndpoint: "The URL of the service that can be used to download the dataset", - submitter: "The username (e.g. ORCID) of the person who submitted or updated the dataset. This may differ from the person responsible for creating the data.", - westBoundCoord: "Western most longitude of the spatial extent, in decimal degrees, WGS84", - siteText: "The name or description of the physical location where the data were collected", - prov_hasSources: "One or more identifiers of metadata documents that have been used as sources to create a derived dataset. The derived datasets will be included in the search results.", - prov_hasDerivations: "One or more identifiers of metadata documents that have been derived from another dataset. The source datasets will be included in the search results.", - indexeddate: "The date that the metadata document was last indexed (or re-indexed) in the search system." - } + serviceEndpoint: + "The URL of the service that can be used to download the dataset", + submitter: + "The username (e.g. ORCID) of the person who submitted or updated the dataset. This may differ from the person responsible for creating the data.", + westBoundCoord: + "Western most longitude of the spatial extent, in decimal degrees, WGS84", + siteText: + "The name or description of the physical location where the data were collected", + prov_hasSources: + "One or more identifiers of metadata documents that have been used as sources to create a derived dataset. The derived datasets will be included in the search results.", + prov_hasDerivations: + "One or more identifiers of metadata documents that have been derived from another dataset. The source datasets will be included in the search results.", + indexeddate: + "The date that the metadata document was last indexed (or re-indexed) in the search system.", + }; }, /** @@ -304,13 +346,20 @@

Source: src/js/models/queryFields/QueryField.js

* query types in the array must exactly match the query types in the type * attribute of a query field model. */ - filterTypesMap: function(){ + filterTypesMap: function () { return { - filter : ["string", "alphaOnlySort", "text_en_splitting", "text_en_splitting_tight", "text_general", "text_case_insensitive"], - booleanFilter : ["boolean"], - numericFilter : ["int", "tfloat", "tlong", "long"], - dateFilter : ["tdate", "date"] - } + filter: [ + "string", + "alphaOnlySort", + "text_en_splitting", + "text_en_splitting_tight", + "text_general", + "text_case_insensitive", + ], + booleanFilter: ["boolean"], + numericFilter: ["int", "tfloat", "tlong", "long"], + dateFilter: ["tdate", "date"], + }; }, /** @@ -319,8 +368,8 @@

Source: src/js/models/queryFields/QueryField.js

* types set in the QueryField model "type" attribute. * @since 2.15.0 */ - caseSensitiveTypes: function(){ - return ["string"] + caseSensitiveTypes: function () { + return ["string"]; }, /** @@ -340,14 +389,18 @@

Source: src/js/models/queryFields/QueryField.js

* * @return {CategoryMap[]} Returns an array of objects that define how to categorize fields. */ - categoriesMap: function(){ + categoriesMap: function () { return [ { label: "General", icon: "list-ul", queryFields: [ - "abstract", "text", "isPartOf", "keywordsText", - "title", "purpose" + "abstract", + "text", + "isPartOf", + "keywordsText", + "title", + "purpose", ], default: true, }, @@ -355,112 +408,164 @@

Source: src/js/models/queryFields/QueryField.js

label: "People & organizations", icon: "group", queryFields: [ - "author", "authorGivenNameSort", "authorSurNameSort", + "author", + "authorGivenNameSort", + "authorSurNameSort", "originText", "authorLastName", - "contactOrganization", - "contactOrganizationText", "investigator", "investigatorText", - "originator", "originatorText", "submitter", + "contactOrganization", + "contactOrganizationText", + "investigator", + "investigatorText", + "originator", + "originatorText", + "submitter", ], }, { label: "Geography", icon: "globe", queryFields: [ - "namedLocation", "siteText", "placeKey", - "northBoundCoord", "eastBoundCoord", "southBoundCoord", "westBoundCoord", + "namedLocation", + "siteText", + "placeKey", + "northBoundCoord", + "eastBoundCoord", + "southBoundCoord", + "westBoundCoord", ], }, { label: "Dates", icon: "calendar", queryFields: [ - "dateUploaded", "beginDate", "endDate", "datePublished", "dateModified", "indexeddate" + "dateUploaded", + "beginDate", + "endDate", + "datePublished", + "dateModified", + "indexeddate", ], }, { label: "Taxon", icon: "sitemap", queryFields: [ - "kingdom", "phylum", "class", "order", "family", "genus", - "species", "scientificName" + "kingdom", + "phylum", + "class", + "order", + "family", + "genus", + "species", + "scientificName", ], }, { label: "Awards & funding", icon: "certificate", queryFields: [ - "projectText", "awardNumber", "awardTitle", "funderIdentifier", - "funderName", "fundingText", + "projectText", + "awardNumber", + "awardTitle", + "funderIdentifier", + "funderName", + "fundingText", ], }, { label: "Repository information", icon: "archive", - queryFields: [ - "authoritativeMN", "datasource" - ], + queryFields: ["authoritativeMN", "datasource"], }, { label: "Permissions", icon: "lock", queryFields: [ - "writePermission", "readPermission", "changePermission", - "rightsHolder", "isPublic", + "writePermission", + "readPermission", + "changePermission", + "rightsHolder", + "isPublic", ], }, { label: "Identifier", icon: "tag", - queryFields: [ - "identifier", "resourceMap", "documents", "seriesId", - ], + queryFields: ["identifier", "resourceMap", "documents", "seriesId"], }, { label: "Data attributes", icon: "table", queryFields: [ - "attributeName", "attributeLabel", "attributeDescription", - "attributeUnit", "sem_annotation", "attribute", + "attributeName", + "attributeLabel", + "attributeDescription", + "attributeUnit", + "sem_annotation", + "attribute", ], }, { label: "File details", icon: "file", queryFields: [ - "fileName", "formatId", "size", "checksum", "geoform" + "fileName", + "formatId", + "size", + "checksum", + "geoform", ], }, { label: "Provenance", icon: "exchange", queryFields: [ - "prov_wasGeneratedBy", "prov_generated", "prov_generatedByExecution", - "prov_generatedByProgram", "prov_generatedByUser", "prov_hasDerivations", - "prov_hasSources", "prov_instanceOfClass", "prov_used", - "prov_usedByExecution", "prov_usedByProgram", "prov_usedByUser", - "prov_wasDerivedFrom", "prov_wasExecutedByExecution", - "prov_wasExecutedByUser", "prov_wasInformedBy" + "prov_wasGeneratedBy", + "prov_generated", + "prov_generatedByExecution", + "prov_generatedByProgram", + "prov_generatedByUser", + "prov_hasDerivations", + "prov_hasSources", + "prov_instanceOfClass", + "prov_used", + "prov_usedByExecution", + "prov_usedByProgram", + "prov_usedByUser", + "prov_wasDerivedFrom", + "prov_wasExecutedByExecution", + "prov_wasExecutedByUser", + "prov_wasInformedBy", ], }, { label: "DataONE replication", icon: "copy", queryFields: [ - "replicationStatus", "blockedReplicationMN", - "preferredReplicationMN", "replicaMN", "replicaVerifiedDate", - "replicationAllowed", "numberReplicas", - ] + "replicationStatus", + "blockedReplicationMN", + "preferredReplicationMN", + "replicaMN", + "replicaVerifiedDate", + "replicationAllowed", + "numberReplicas", + ], }, { label: "Advanced", icon: "code", queryFields: [ - "serviceCoupling", "serviceDescription", "serviceEndpoint", - "serviceOutput","serviceTitle","serviceType","isService", - "archived" - ] - } + "serviceCoupling", + "serviceDescription", + "serviceEndpoint", + "serviceOutput", + "serviceTitle", + "serviceType", + "isService", + "archived", + ], + }, // { // label: "True or False Fields", // icon: "asterisk", @@ -479,7 +584,7 @@

Source: src/js/models/queryFields/QueryField.js

// "text_en_splitting_tight", "text_general", "text_case_insensitive" // ] // }, - ] + ]; }, /** @@ -487,32 +592,37 @@

Source: src/js/models/queryFields/QueryField.js

* * @return {string} A humanized alias for the field */ - getReadableName: function(){ - + getReadableName: function () { try { - var name = this.get("name"), - alias = this.aliases()[name]; + var name = this.get("name"), + alias = this.aliases()[name]; // First see if there's an alias - if(alias){ + if (alias) { return alias; } // Otherwise, humanize the camel-cased field - return name - // Replace "MN" at the end of a name with "Repository" - .replace(/MN$/, "Repository") - // Replace underscores with spaces - .replace(/_/g, ' ') - // Insert a space before all caps - .replace(/([A-Z])/g, ' $1') - // Remove white space from both ends (e.g. when converting _root_) - .trim() - // Uppercase the first character - .replace(/^./, function(str){ return str.toUpperCase(); }) - + return ( + name + // Replace "MN" at the end of a name with "Repository" + .replace(/MN$/, "Repository") + // Replace underscores with spaces + .replace(/_/g, " ") + // Insert a space before all caps + .replace(/([A-Z])/g, " $1") + // Remove white space from both ends (e.g. when converting _root_) + .trim() + // Uppercase the first character + .replace(/^./, function (str) { + return str.toUpperCase(); + }) + ); } catch (e) { - console.log("Failed to create a readable name for a Query Field, error message: " + e); + console.log( + "Failed to create a readable name for a Query Field, error message: " + + e, + ); } }, @@ -522,12 +632,15 @@

Source: src/js/models/queryFields/QueryField.js

* @returns Returns one of the descriptions configured in this Model for this Field * or "undefined" if one is not set. */ - getFriendlyDescription: function(){ + getFriendlyDescription: function () { try { - var name = this.get("name"); + var name = this.get("name"); return this.descriptions()[name]; } catch (e) { - console.log("Failed to create a readable name for a Query Field, error message: " + e); + console.log( + "Failed to create a readable name for a Query Field, error message: " + + e, + ); } }, @@ -538,72 +651,73 @@

Source: src/js/models/queryFields/QueryField.js

* * @return {object} returns an object with an icon and category property (both strings) */ - getCategory: function(){ + getCategory: function () { try { - var categoriesMap = this.categoriesMap(), - fieldType = this.get("type"), - fieldName = this.get("name"), - match = null, - category = {}; - - // First check for a matching field name. - match = _.find(categoriesMap, function(category){ - if(category.queryFields){ - return category.queryFields.includes(fieldName); - } - }); - - // If a matching field name wasn't found, then match by field type. - if(!match){ - match = _.find(categoriesMap, function(category){ - if(category.queryTypes){ - return category.queryTypes.includes(fieldType); - } - }); + fieldType = this.get("type"), + fieldName = this.get("name"), + match = null, + category = {}; + + // First check for a matching field name. + match = _.find(categoriesMap, function (category) { + if (category.queryFields) { + return category.queryFields.includes(fieldName); } + }); - // If there's still no match, look for the default category - if(!match){ - var match = _.find(categoriesMap, function(category){ - return category.default - }); - } + // If a matching field name wasn't found, then match by field type. + if (!match) { + match = _.find(categoriesMap, function (category) { + if (category.queryTypes) { + return category.queryTypes.includes(fieldType); + } + }); + } - if(match){ - match.index = _.indexOf(categoriesMap, match) + 1; - } + // If there's still no match, look for the default category + if (!match) { + var match = _.find(categoriesMap, function (category) { + return category.default; + }); + } - return match + if (match) { + match.index = _.indexOf(categoriesMap, match) + 1; + } + return match; } catch (e) { - console.log("Failed to categorize a Query Field, error message: " + e); + console.log( + "Failed to categorize a Query Field, error message: " + e, + ); } }, - /** * getFilterType - Searches the filterTypesMap and returns the filter type * that is required for this query field * * @return {string} The nodeName of the filter that should be used for this query field */ - getFilterType: function(){ + getFilterType: function () { try { var filterMap = this.filterTypesMap(), - fieldType = this.get("type"), - filterType = null; + fieldType = this.get("type"), + filterType = null; for (const [key, value] of Object.entries(filterMap)) { - if (value.includes(fieldType)){ - filterType = key + if (value.includes(fieldType)) { + filterType = key; } } return filterType; - } catch (e) { - console.log("Failed to find a matching filter type for a Query Field, error message: " + e); + console.log( + "Failed to find a matching filter type for a Query Field, error message: " + + e, + ); } }, @@ -613,11 +727,13 @@

Source: src/js/models/queryFields/QueryField.js

* @param {string} type the solr field type * @return {boolean} returns true of the field exactly matches */ - isType: function(type){ + isType: function (type) { try { - return this.get('type') === type + return this.get("type") === type; } catch (e) { - console.log("Failed to check if query field is a type, error message: " + e); + console.log( + "Failed to check if query field is a type, error message: " + e, + ); } }, @@ -626,14 +742,14 @@

Source: src/js/models/queryFields/QueryField.js

* * @return {boolean} always returns false */ - save: function() { + save: function () { return false; - } - - }); + }, + }, + ); - return QueryField; - }); + return QueryField; +});
diff --git a/docs/docs/src_js_routers_router.js.html b/docs/docs/src_js_routers_router.js.html index cd06e2644..e313e97aa 100644 --- a/docs/docs/src_js_routers_router.js.html +++ b/docs/docs/src_js_routers_router.js.html @@ -44,743 +44,792 @@

Source: src/js/routers/router.js

-
/*global Backbone */
-'use strict';
-
-define(['jquery',	'underscore', 'backbone'],
-function ($, _, Backbone) {
-
-	/**
-  * @class UIRouter
-  * @classdesc MetacatUI Router
-  * @classcategory Router
-  * @extends Backbone.Router
-  * @constructor
-  */
-	var UIRouter = Backbone.Router.extend(
-    /** @lends UIRouter.prototype */{
-
-		routes: {
-			''                                  : 'renderIndex',    // the default route
-			'about(/:anchorId)(/)'              : 'renderAbout',    // about page
-			'help(/:page)(/:anchorId)(/)'       : 'renderHelp',
-			'tools(/:anchorId)(/)'              : 'renderTools',    // tools page
-			'data/my-data(/page/:page)(/)'      : 'renderMyData',    // data search page
-			'data(/mode=:mode)(/query=:query)(/page/:page)(/)' : 'renderData',    // data search page
-			'data/my-data(/)'                   : 'renderMyData',
-			'profile(/*username)(/s=:section)(/s=:subsection)(/)' : 'renderProfile',
-			'my-profile(/s=:section)(/s=:subsection)(/)' : 'renderMyProfile',
-			'logout(/)'                         : 'logout', // logout the user
-			'signout(/)'                        : 'logout', // logout the user
-			'signin(/)'                         : 'renderSignIn', // signin the user
-			"signinsuccess(/)"                  : "renderSignInSuccess",
-			"signinldaperror(/)"                : "renderLdapSignInError",
-			"signinLdap(/)"                     : "renderLdapSignIn",
-			"signinSuccessLdap(/)"              : "renderLdapSignInSuccess",
-      		"signin-help"                       : "renderSignInHelp", //The Sign In troubleshotting page
-			'share(/*pid)(/)'                   : 'renderEditor', // registry page
-			'submit(/*pid)(/)'                  : 'renderEditor', // registry page
-			'quality(/s=:suiteId)(/:pid)(/)'    : 'renderMdqRun', // MDQ page
-			'api(/:anchorId)(/)'                : 'renderAPI',       // API page
-			"edit/:portalTermPlural(/:portalIdentifier)(/:portalSection)(/)" : "renderPortalEditor",
-			'drafts' : 'renderDrafts'
-		},
-
-		helpPages: {
-			"search" : "searchTips",
-			defaultPage : "searchTips"
-		},
-
-		initialize: function(){
-
-			// Add routes to portal dynamically using the appModel portal term
-			var portalTermPlural = MetacatUI.appModel.get("portalTermPlural");
-  		this.route( portalTermPlural + "(/:portalId)(/:portalSection)(/)",
-									["portalId", "portalSection"], this.renderPortal
-								);
-
-			this.listenTo(Backbone.history, "routeNotFound", this.navigateToDefault);
-
-			// This route handler replaces the route handler we had in the
-			// routes table before which was "view/*pid". The * only finds URL
-			// parts until the ? but DataONE PIDs can have ? in them so we need
-			// to make this route more inclusive.
-			this.route(/^view\/(.*)$/, "renderMetadata");
-
-			this.on("route", this.trackPathName);
-
-			// Clear stale JSONLD and meta tags
-			this.on("route", this.clearJSONLD);
-			this.on("route", this.clearHighwirePressMetaTags);
-		},
-
-		//Keep track of navigation movements
-		routeHistory: new Array(),
-		pathHistory: new Array(),
-
-		// Will return the last route, which is actually the second to last item in the route history,
-		// since the last item is the route being currently viewed
-		lastRoute: function(){
-			if((typeof this.routeHistory === "undefined") || (this.routeHistory.length <= 1))
-				return false;
-			else
-				return this.routeHistory[this.routeHistory.length-2];
-		},
-
-		trackPathName: function(e){
-			if(_.last(this.pathHistory) != window.location.pathname)
-				this.pathHistory.push(window.location.pathname);
-		},
-
-		//If the user or app cancelled the last route, call this function to revert
-		// the window location pathname back to the correct value
-		undoLastRoute: function(){
-			this.routeHistory.pop();
-
-			// Remove the last route and pathname from the history
-			if(_.last(this.pathHistory) == window.location.pathname)
-				this.pathHistory.pop();
-
-			//Change the pathname in the window location back
-			this.navigate(_.last(this.pathHistory), {replace: true});
-		},
-
-		renderIndex: function (param) {
-			this.routeHistory.push("index");
-
-			if(!MetacatUI.appView.indexView){
-				require(["views/IndexView"], function(IndexView){
-					MetacatUI.appView.indexView = new IndexView();
-					MetacatUI.appView.showView(MetacatUI.appView.indexView);
-				});
-			}
-			else
-				MetacatUI.appView.showView(MetacatUI.appView.indexView);
-		},
-
-		renderText: function(options){
-			if(!MetacatUI.appView.textView){
-				require(['views/TextView'], function(TextView){
-					MetacatUI.appView.textView = new TextView();
-					MetacatUI.appView.showView(MetacatUI.appView.textView, options);
-				});
-			}
-			else
-				MetacatUI.appView.showView(MetacatUI.appView.textView, options);
-		},
-
-		renderHelp: function(page, anchorId){
-			this.routeHistory.push("help");
-			MetacatUI.appModel.set('anchorId', anchorId);
-
-			if(page)
-				var pageName = this.helpPages[page];
-			else
-				var pageName = this.helpPages["defaultPage"]; //default
-
-			var options = {
-				pageName: pageName,
-				anchorId: anchorId
-			}
-
-			this.renderText(options);
-		},
-
-    renderSignInHelp: function(){
-      this.routeHistory.push("signin-help");
-      this.renderText({ pageName: "signInHelp" });
-    },
+            
"use strict";
+
+define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
+  /**
+   * @class UIRouter
+   * @classdesc MetacatUI Router
+   * @classcategory Router
+   * @extends Backbone.Router
+   * @constructor
+   */
+  var UIRouter = Backbone.Router.extend(
+    /** @lends UIRouter.prototype */ {
+      routes: {
+        "": "renderIndex", // the default route
+        "about(/:anchorId)(/)": "renderAbout", // about page
+        "help(/:page)(/:anchorId)(/)": "renderHelp",
+        "tools(/:anchorId)(/)": "renderTools", // tools page
+        "data/my-data(/page/:page)(/)": "renderMyData", // data search page
+        "data(/mode=:mode)(/query=:query)(/page/:page)(/)": "renderData", // data search page
+        "data/my-data(/)": "renderMyData",
+        "profile(/*username)(/s=:section)(/s=:subsection)(/)": "renderProfile",
+        "my-profile(/s=:section)(/s=:subsection)(/)": "renderMyProfile",
+        "logout(/)": "logout", // logout the user
+        "signout(/)": "logout", // logout the user
+        "signin(/)": "renderSignIn", // signin the user
+        "signinsuccess(/)": "renderSignInSuccess",
+        "signinldaperror(/)": "renderLdapSignInError",
+        "signinLdap(/)": "renderLdapSignIn",
+        "signinSuccessLdap(/)": "renderLdapSignInSuccess",
+        "signin-help": "renderSignInHelp", //The Sign In troubleshotting page
+        "share(/*pid)(/)": "renderEditor", // registry page
+        "submit(/*pid)(/)": "renderEditor", // registry page
+        "quality(/s=:suiteId)(/:pid)(/)": "renderMdqRun", // MDQ page
+        "api(/:anchorId)(/)": "renderAPI", // API page
+        "edit/:portalTermPlural(/:portalIdentifier)(/:portalSection)(/)":
+          "renderPortalEditor",
+        drafts: "renderDrafts",
+      },
+
+      helpPages: {
+        search: "searchTips",
+        defaultPage: "searchTips",
+      },
+
+      initialize: function () {
+        // Add routes to portal dynamically using the appModel portal term
+        var portalTermPlural = MetacatUI.appModel.get("portalTermPlural");
+        this.route(
+          portalTermPlural + "(/:portalId)(/:portalSection)(/)",
+          ["portalId", "portalSection"],
+          this.renderPortal,
+        );
+
+        this.listenTo(
+          Backbone.history,
+          "routeNotFound",
+          this.navigateToDefault,
+        );
+
+        // This route handler replaces the route handler we had in the
+        // routes table before which was "view/*pid". The * only finds URL
+        // parts until the ? but DataONE PIDs can have ? in them so we need
+        // to make this route more inclusive.
+        this.route(/^view\/(.*)$/, "renderMetadata");
+
+        this.on("route", this.trackPathName);
+
+        // Clear stale JSONLD and meta tags
+        this.on("route", this.clearJSONLD);
+        this.on("route", this.clearHighwirePressMetaTags);
+      },
+
+      //Keep track of navigation movements
+      routeHistory: new Array(),
+      pathHistory: new Array(),
+
+      // Will return the last route, which is actually the second to last item in the route history,
+      // since the last item is the route being currently viewed
+      lastRoute: function () {
+        if (
+          typeof this.routeHistory === "undefined" ||
+          this.routeHistory.length <= 1
+        )
+          return false;
+        else return this.routeHistory[this.routeHistory.length - 2];
+      },
+
+      trackPathName: function (e) {
+        if (_.last(this.pathHistory) != window.location.pathname)
+          this.pathHistory.push(window.location.pathname);
+      },
+
+      //If the user or app cancelled the last route, call this function to revert
+      // the window location pathname back to the correct value
+      undoLastRoute: function () {
+        this.routeHistory.pop();
+
+        // Remove the last route and pathname from the history
+        if (_.last(this.pathHistory) == window.location.pathname)
+          this.pathHistory.pop();
+
+        //Change the pathname in the window location back
+        this.navigate(_.last(this.pathHistory), { replace: true });
+      },
+
+      renderIndex: function (param) {
+        this.routeHistory.push("index");
+
+        if (!MetacatUI.appView.indexView) {
+          require(["views/IndexView"], function (IndexView) {
+            MetacatUI.appView.indexView = new IndexView();
+            MetacatUI.appView.showView(MetacatUI.appView.indexView);
+          });
+        } else MetacatUI.appView.showView(MetacatUI.appView.indexView);
+      },
+
+      renderText: function (options) {
+        if (!MetacatUI.appView.textView) {
+          require(["views/TextView"], function (TextView) {
+            MetacatUI.appView.textView = new TextView();
+            MetacatUI.appView.showView(MetacatUI.appView.textView, options);
+          });
+        } else MetacatUI.appView.showView(MetacatUI.appView.textView, options);
+      },
+
+      renderHelp: function (page, anchorId) {
+        this.routeHistory.push("help");
+        MetacatUI.appModel.set("anchorId", anchorId);
+
+        if (page) var pageName = this.helpPages[page];
+        else var pageName = this.helpPages["defaultPage"]; //default
+
+        var options = {
+          pageName: pageName,
+          anchorId: anchorId,
+        };
+
+        this.renderText(options);
+      },
+
+      renderSignInHelp: function () {
+        this.routeHistory.push("signin-help");
+        this.renderText({ pageName: "signInHelp" });
+      },
+
+      renderAbout: function (anchorId) {
+        this.routeHistory.push("about");
+        MetacatUI.appModel.set("anchorId", anchorId);
+        var options = {
+          pageName: "about",
+          anchorId: anchorId,
+        };
 
-		renderAbout: function (anchorId) {
-			this.routeHistory.push("about");
-			MetacatUI.appModel.set('anchorId', anchorId);
-			var options = {
-					pageName: "about",
-					anchorId: anchorId
-				}
-
-			this.renderText(options);
-		},
-
-		renderAPI: function (anchorId) {
-			this.routeHistory.push("api");
-			MetacatUI.appModel.set('anchorId', anchorId);
-			var options = {
-					pageName: "api",
-					anchorId: anchorId
-				}
-
-			this.renderText(options);
-		},
-
-		renderProjects: function() {
-			require(["views/projects/ProjectView"], function(ProjectView) {
-				MetacatUI.appView.projectView = new ProjectView();
-				MetacatUI.appView.showView(MetacatUI.appView.projectView)
-			});
-		},
-
-		/*
-    * Renders the editor view given a root package identifier,
-    * or a metadata identifier.  If the latter, the corresponding
-    * package identifier will be queried and then rendered.
-    */
-		renderEditor: function (pid) {
-
-			//If there is no EML211EditorView yet, create one
-			if( ! MetacatUI.appView.eml211EditorView ){
-
-				var router = this;
-
-				//Load the EML211EditorView file
-				require(['views/metadata/EML211EditorView'], function(EML211EditorView) {
-					//Add the submit route to the router history
-					router.routeHistory.push("submit");
-
-					//Create a new EML211EditorView
-					MetacatUI.appView.eml211EditorView = new EML211EditorView({pid: pid});
-
-					//Set the pid from the pid given in the URL
-					MetacatUI.appView.eml211EditorView.pid = pid;
-
-					//Render the EML211EditorView
-					MetacatUI.appView.showView(MetacatUI.appView.eml211EditorView);
-				});
-
-			}
-			else {
-
-					//Set the pid from the pid given in the URL
-					MetacatUI.appView.eml211EditorView.pid = pid;
-
-					//Add the submit route to the router history
-					this.routeHistory.push("submit");
-
-					//Render the Editor View
-					MetacatUI.appView.showView(MetacatUI.appView.eml211EditorView);
-
-			}
-		},
-
-		/**
-		 * Renders the Drafts view which is a simple view backed by LocalForage that
-		 * lists drafts created in the Editor so users can recover any failed
-		 * submissions.
-		 */
-		renderDrafts: function() {
-			require(['views/DraftsView'], function(DraftsView){
-				MetacatUI.appView.draftsView = new DraftsView();
-				MetacatUI.appView.showView(MetacatUI.appView.draftsView);
-			});
-		 },
-
-		renderMdqRun: function (suiteId, pid) {
-			this.routeHistory.push("quality");
-
-			if (!MetacatUI.appView.mdqRunView) {
-				require(["views/MdqRunView"], function(MdqRunView) {
-					MetacatUI.appView.mdqRunView = new MdqRunView();
-					MetacatUI.appView.mdqRunView.suiteId = suiteId;
-					MetacatUI.appView.mdqRunView.pid = pid;
-					MetacatUI.appView.showView(MetacatUI.appView.mdqRunView);
-				});
-			} else {
-				MetacatUI.appView.mdqRunView.suiteId = suiteId;
-				MetacatUI.appView.mdqRunView.pid = pid;
-				MetacatUI.appView.showView(MetacatUI.appView.mdqRunView);
-			}
-		},
-
-		renderTools: function (anchorId) {
-			this.routeHistory.push("tools");
-			MetacatUI.appModel.set('anchorId', anchorId);
-
-			var options = {
-					pageName: "tools",
-					anchorId: anchorId
-				}
-
-			this.renderText(options);
-		},
-
-		renderMyData: function(page){
-			//Only display this is the user is logged in
-			if(!MetacatUI.appUserModel.get("loggedIn") && MetacatUI.appUserModel.get("checked")) this.navigate("data", { trigger: true });
-			else if(!MetacatUI.appUserModel.get("checked")){
-				var router = this;
-
-				this.listenToOnce(MetacatUI.appUserModel, "change:checked", function(){
-
-					if(MetacatUI.appUserModel.get("loggedIn"))
-						router.renderMyData(page);
-					else
-						this.navigate("data", { trigger: true });
-				});
-
-				return;
-			}
-
-			this.routeHistory.push("data");
-
-			///Check for a page URL parameter
-			if(typeof page === "undefined")
-				MetacatUI.appModel.set("page", 0);
-			else
-				MetacatUI.appModel.set('page', page);
-
-			if(!MetacatUI.appView.dataCatalogView){
-				require(['views/DataCatalogView'], function(DataCatalogView){
-					MetacatUI.appView.dataCatalogView = new DataCatalogView();
-					MetacatUI.appView.dataCatalogView.searchModel = MetacatUI.appUserModel.get("searchModel").clone();
-					MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
-				});
-			}
-			else{
-				MetacatUI.appView.dataCatalogView.searchModel = MetacatUI.appUserModel.get("searchModel").clone();
-				MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
-			}
-		},
-
-		renderData: function (mode, query, page) {
-			this.routeHistory.push("data");
-			// Check for a page URL parameter
-			page = parseInt(page);
-			page = (isNaN(page) || page < 1) ? 1 : page;
-			MetacatUI.appModel.set("page", (page-1));
-
-			// Check if we are using the new CatalogSearchView
-			if(!MetacatUI.appModel.get("useDeprecatedDataCatalogView")){
-				require(["views/search/CatalogSearchView"], function(CatalogSearchView){
-					MetacatUI.appView.catalogSearchView = new CatalogSearchView({
-						initialQuery: query,
-					});
-					MetacatUI.appView.showView(MetacatUI.appView.catalogSearchView);
-				});
-				return;
-			}
-
-			// Check for a query URL parameter
-			if ((typeof query !== "undefined") && query) {
-				MetacatUI.appSearchModel.set('additionalCriteria', [query]);
-			}
-
-			require(['views/DataCatalogView'], function(DataCatalogView){
-				if (!MetacatUI.appView.dataCatalogView) {
-					MetacatUI.appView.dataCatalogView = new DataCatalogView();
-				}
-				if (mode) MetacatUI.appView.dataCatalogView.mode = mode;
-				MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
-			});
-		},
-
-     /**
-      * Renders the Portals Search view.
-      */
-     renderPortalsSearch: function() {
-         require(['views/portals/PortalsSearchView'], function(PortalsSearchView){
-             MetacatUI.appView.showView(new PortalsSearchView({ el: "#Content" }));
-         });
-      },
-
-    /**
-     * renderPortal - Render the portal view based on the given name or id, as
-     * well as optional section
-     *
-     * @param  {string} label         The portal ID or name
-     * @param  {string} portalSection A specific section within the portal
-     */
-		renderPortal: function(label, portalSection) {
-
-      //If no portal was specified, go to the portal search view
-      if( !label ){
+        this.renderText(options);
+      },
+
+      renderAPI: function (anchorId) {
+        this.routeHistory.push("api");
+        MetacatUI.appModel.set("anchorId", anchorId);
+        var options = {
+          pageName: "api",
+          anchorId: anchorId,
+        };
+
+        this.renderText(options);
+      },
+
+      renderProjects: function () {
+        require(["views/projects/ProjectView"], function (ProjectView) {
+          MetacatUI.appView.projectView = new ProjectView();
+          MetacatUI.appView.showView(MetacatUI.appView.projectView);
+        });
+      },
+
+      /*
+       * Renders the editor view given a root package identifier,
+       * or a metadata identifier.  If the latter, the corresponding
+       * package identifier will be queried and then rendered.
+       */
+      renderEditor: function (pid) {
+        //If there is no EML211EditorView yet, create one
+        if (!MetacatUI.appView.eml211EditorView) {
+          var router = this;
+
+          //Load the EML211EditorView file
+          require(["views/metadata/EML211EditorView"], function (
+            EML211EditorView,
+          ) {
+            //Add the submit route to the router history
+            router.routeHistory.push("submit");
+
+            //Create a new EML211EditorView
+            MetacatUI.appView.eml211EditorView = new EML211EditorView({
+              pid: pid,
+            });
+
+            //Set the pid from the pid given in the URL
+            MetacatUI.appView.eml211EditorView.pid = pid;
+
+            //Render the EML211EditorView
+            MetacatUI.appView.showView(MetacatUI.appView.eml211EditorView);
+          });
+        } else {
+          //Set the pid from the pid given in the URL
+          MetacatUI.appView.eml211EditorView.pid = pid;
+
+          //Add the submit route to the router history
+          this.routeHistory.push("submit");
+
+          //Render the Editor View
+          MetacatUI.appView.showView(MetacatUI.appView.eml211EditorView);
+        }
+      },
+
+      /**
+       * Renders the Drafts view which is a simple view backed by LocalForage that
+       * lists drafts created in the Editor so users can recover any failed
+       * submissions.
+       */
+      renderDrafts: function () {
+        require(["views/DraftsView"], function (DraftsView) {
+          MetacatUI.appView.draftsView = new DraftsView();
+          MetacatUI.appView.showView(MetacatUI.appView.draftsView);
+        });
+      },
+
+      renderMdqRun: function (suiteId, pid) {
+        this.routeHistory.push("quality");
+
+        if (!MetacatUI.appView.mdqRunView) {
+          require(["views/MdqRunView"], function (MdqRunView) {
+            MetacatUI.appView.mdqRunView = new MdqRunView();
+            MetacatUI.appView.mdqRunView.suiteId = suiteId;
+            MetacatUI.appView.mdqRunView.pid = pid;
+            MetacatUI.appView.showView(MetacatUI.appView.mdqRunView);
+          });
+        } else {
+          MetacatUI.appView.mdqRunView.suiteId = suiteId;
+          MetacatUI.appView.mdqRunView.pid = pid;
+          MetacatUI.appView.showView(MetacatUI.appView.mdqRunView);
+        }
+      },
+
+      renderTools: function (anchorId) {
+        this.routeHistory.push("tools");
+        MetacatUI.appModel.set("anchorId", anchorId);
+
+        var options = {
+          pageName: "tools",
+          anchorId: anchorId,
+        };
+
+        this.renderText(options);
+      },
+
+      renderMyData: function (page) {
+        //Only display this is the user is logged in
+        if (
+          !MetacatUI.appUserModel.get("loggedIn") &&
+          MetacatUI.appUserModel.get("checked")
+        )
+          this.navigate("data", { trigger: true });
+        else if (!MetacatUI.appUserModel.get("checked")) {
+          var router = this;
+
+          this.listenToOnce(
+            MetacatUI.appUserModel,
+            "change:checked",
+            function () {
+              if (MetacatUI.appUserModel.get("loggedIn"))
+                router.renderMyData(page);
+              else this.navigate("data", { trigger: true });
+            },
+          );
+
+          return;
+        }
+
+        this.routeHistory.push("data");
+
+        ///Check for a page URL parameter
+        if (typeof page === "undefined") MetacatUI.appModel.set("page", 0);
+        else MetacatUI.appModel.set("page", page);
+
+        if (!MetacatUI.appView.dataCatalogView) {
+          require(["views/DataCatalogView"], function (DataCatalogView) {
+            MetacatUI.appView.dataCatalogView = new DataCatalogView();
+            MetacatUI.appView.dataCatalogView.searchModel =
+              MetacatUI.appUserModel.get("searchModel").clone();
+            MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
+          });
+        } else {
+          MetacatUI.appView.dataCatalogView.searchModel = MetacatUI.appUserModel
+            .get("searchModel")
+            .clone();
+          MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
+        }
+      },
+
+      renderData: function (mode, query, page) {
+        this.routeHistory.push("data");
+        // Check for a page URL parameter
+        page = parseInt(page);
+        page = isNaN(page) || page < 1 ? 1 : page;
+        MetacatUI.appModel.set("page", page - 1);
+
+        // Check if we are using the new CatalogSearchView
+        if (!MetacatUI.appModel.get("useDeprecatedDataCatalogView")) {
+          require(["views/search/CatalogSearchView"], function (
+            CatalogSearchView,
+          ) {
+            MetacatUI.appView.catalogSearchView = new CatalogSearchView({
+              initialQuery: query,
+            });
+            MetacatUI.appView.showView(MetacatUI.appView.catalogSearchView);
+          });
+          return;
+        }
+
+        // Check for a query URL parameter
+        if (typeof query !== "undefined" && query) {
+          MetacatUI.appSearchModel.set("additionalCriteria", [query]);
+        }
+
+        require(["views/DataCatalogView"], function (DataCatalogView) {
+          if (!MetacatUI.appView.dataCatalogView) {
+            MetacatUI.appView.dataCatalogView = new DataCatalogView();
+          }
+          if (mode) MetacatUI.appView.dataCatalogView.mode = mode;
+          MetacatUI.appView.showView(MetacatUI.appView.dataCatalogView);
+        });
+      },
+
+      /**
+       * Renders the Portals Search view.
+       */
+      renderPortalsSearch: function () {
+        require(["views/portals/PortalsSearchView"], function (
+          PortalsSearchView,
+        ) {
+          MetacatUI.appView.showView(new PortalsSearchView({ el: "#Content" }));
+        });
+      },
+
+      /**
+       * renderPortal - Render the portal view based on the given name or id, as
+       * well as optional section
+       *
+       * @param  {string} label         The portal ID or name
+       * @param  {string} portalSection A specific section within the portal
+       */
+      renderPortal: function (label, portalSection) {
+        //If no portal was specified, go to the portal search view
+        if (!label) {
           this.renderPortalsSearch();
           return;
-      }
-
-			// Add the overall class immediately so the navbar is styled correctly right away
-			$("body").addClass("PortalView");
-      // Look up the portal document seriesId by its registered name if given
-      if ( portalSection ) {
-        this.routeHistory.push( MetacatUI.appModel.get("portalTermPlural") + "/" + label + "/" + portalSection);
-      }
-      else{
-        this.routeHistory.push( MetacatUI.appModel.get("portalTermPlural")+ "/" + label);
-      }
-
-      require(['views/portals/PortalView'], function(PortalView){
-        MetacatUI.appView.portalView = new PortalView({
+        }
+
+        // Add the overall class immediately so the navbar is styled correctly right away
+        $("body").addClass("PortalView");
+        // Look up the portal document seriesId by its registered name if given
+        if (portalSection) {
+          this.routeHistory.push(
+            MetacatUI.appModel.get("portalTermPlural") +
+              "/" +
+              label +
+              "/" +
+              portalSection,
+          );
+        } else {
+          this.routeHistory.push(
+            MetacatUI.appModel.get("portalTermPlural") + "/" + label,
+          );
+        }
+
+        require(["views/portals/PortalView"], function (PortalView) {
+          MetacatUI.appView.portalView = new PortalView({
             label: label,
-            activeSectionLabel: portalSection
+            activeSectionLabel: portalSection,
+          });
+          MetacatUI.appView.showView(MetacatUI.appView.portalView);
         });
-        MetacatUI.appView.showView(MetacatUI.appView.portalView);
-      });
-
-		},
-
-    /**
-    * Renders the PortalEditorView
-    * @param {string} [portalTermPlural] - This should match the `portalTermPlural` configured in the AppModel.
-    * @param {string} [portalIdentifier] - The id or labebl of the portal
-		* @param {string} [portalSection] - The name of the section within the portal to navigate to (e.g. "data")
-    */
-    renderPortalEditor: function(portalTermPlural, portalIdentifier, portalSection){
-
-      //If the user navigated to a route with a portal term other than the one supported, then this is not a portal editor route.
-      if( portalTermPlural != MetacatUI.appModel.get("portalTermPlural") ){
-        this.navigateToDefault();
-        return;
-      }
-
-			// Add the overall class immediately so the navbar is styled correctly right away
-      $("body").addClass("Editor")
-               .addClass("Portal");
-
-      // Look up the portal document seriesId by its registered name if given
-      if ( portalSection ) {
-        this.routeHistory.push("edit/"+ MetacatUI.appModel.get("portalTermPlural") +"/" + portalIdentifier + "/" + portalSection);
-      }
-      else{
-        if( !portalIdentifier ){
-          this.routeHistory.push("edit/" + MetacatUI.appModel.get("portalTermPlural"));
+      },
+
+      /**
+       * Renders the PortalEditorView
+       * @param {string} [portalTermPlural] - This should match the `portalTermPlural` configured in the AppModel.
+       * @param {string} [portalIdentifier] - The id or labebl of the portal
+       * @param {string} [portalSection] - The name of the section within the portal to navigate to (e.g. "data")
+       */
+      renderPortalEditor: function (
+        portalTermPlural,
+        portalIdentifier,
+        portalSection,
+      ) {
+        //If the user navigated to a route with a portal term other than the one supported, then this is not a portal editor route.
+        if (portalTermPlural != MetacatUI.appModel.get("portalTermPlural")) {
+          this.navigateToDefault();
+          return;
         }
-        else{
-          this.routeHistory.push("edit/" + MetacatUI.appModel.get("portalTermPlural") +"/" + portalIdentifier);
+
+        // Add the overall class immediately so the navbar is styled correctly right away
+        $("body").addClass("Editor").addClass("Portal");
+
+        // Look up the portal document seriesId by its registered name if given
+        if (portalSection) {
+          this.routeHistory.push(
+            "edit/" +
+              MetacatUI.appModel.get("portalTermPlural") +
+              "/" +
+              portalIdentifier +
+              "/" +
+              portalSection,
+          );
+        } else {
+          if (!portalIdentifier) {
+            this.routeHistory.push(
+              "edit/" + MetacatUI.appModel.get("portalTermPlural"),
+            );
+          } else {
+            this.routeHistory.push(
+              "edit/" +
+                MetacatUI.appModel.get("portalTermPlural") +
+                "/" +
+                portalIdentifier,
+            );
+          }
         }
-      }
 
-      require(['views/portals/editor/PortalEditorView'], function(PortalEditorView){
-        MetacatUI.appView.portalEditorView = new PortalEditorView({
-          activeSectionLabel: portalSection,
-          portalIdentifier: portalIdentifier
+        require(["views/portals/editor/PortalEditorView"], function (
+          PortalEditorView,
+        ) {
+          MetacatUI.appView.portalEditorView = new PortalEditorView({
+            activeSectionLabel: portalSection,
+            portalIdentifier: portalIdentifier,
+          });
+          MetacatUI.appView.showView(MetacatUI.appView.portalEditorView);
         });
-        MetacatUI.appView.showView(MetacatUI.appView.portalEditorView);
-      });
-    },
+      },
+
+      renderMetadata: function (pid) {
+        pid = decodeURIComponent(pid);
+
+        this.routeHistory.push("metadata");
+        MetacatUI.appModel.set("lastPid", MetacatUI.appModel.get("pid"));
+
+        var seriesId;
+
+        //Check for a seriesId
+        if (pid.indexOf("version:") > -1) {
+          seriesId = pid.substr(0, pid.indexOf(", version:"));
+
+          pid = pid.substr(pid.indexOf(", version: ") + ", version: ".length);
+        }
+
+        //Save the id in the app model
+        MetacatUI.appModel.set("pid", pid);
+
+        if (!MetacatUI.appView.metadataView) {
+          require(["views/MetadataView"], function (MetadataView) {
+            MetacatUI.appView.metadataView = new MetadataView();
+
+            //Send the id(s) to the view
+            MetacatUI.appView.metadataView.seriesId = seriesId;
+            MetacatUI.appView.metadataView.pid = pid;
+
+            MetacatUI.appView.showView(MetacatUI.appView.metadataView);
+          });
+        } else {
+          //Send the id(s) to the view
+          MetacatUI.appView.metadataView.seriesId = seriesId;
+          MetacatUI.appView.metadataView.pid = pid;
+
+          // MetacatUI resets the dataPackage and dataPackageSynced
+          // attributes before rendering the view. These attributes are
+          // initialized on a per-dataset basis to prevent displaying the
+          // same dataset repeatedly.
+
+          MetacatUI.appView.metadataView.dataPackage = null;
+          MetacatUI.appView.metadataView.dataPackageSynced = false;
+
+          MetacatUI.appView.showView(MetacatUI.appView.metadataView);
+        }
+      },
+
+      renderProfile: function (username, section, subsection) {
+        this.closeLastView();
+
+        var viewChoice;
+
+        //If there is a username specified and user profiles are disabled,
+        // forward to the entire repo profile view.
+        if (username && !MetacatUI.appModel.get("enableUserProfiles")) {
+          this.navigate("profile", { trigger: true, replace: true });
+          return;
+        }
+
+        if (!username) {
+          this.routeHistory.push("summary");
+
+          // flag indicating /profile view
+          var viewOptions = { nodeSummaryView: true };
+
+          if (!MetacatUI.appView.statsView) {
+            require(["views/StatsView"], function (StatsView) {
+              MetacatUI.appView.statsView = new StatsView({
+                userType: "repository",
+              });
+
+              MetacatUI.appView.showView(
+                MetacatUI.appView.statsView,
+                viewOptions,
+              );
+            });
+          } else
+            MetacatUI.appView.showView(
+              MetacatUI.appView.statsView,
+              viewOptions,
+            );
+        } else {
+          this.routeHistory.push("profile");
+          MetacatUI.appModel.set("profileUsername", username);
+
+          if (section || subsection) {
+            var viewOptions = { section: section, subsection: subsection };
+          }
+
+          if (!MetacatUI.appView.userView) {
+            require(["views/UserView"], function (UserView) {
+              MetacatUI.appView.userView = new UserView();
+
+              MetacatUI.appView.showView(
+                MetacatUI.appView.userView,
+                viewOptions,
+              );
+            });
+          } else
+            MetacatUI.appView.showView(MetacatUI.appView.userView, viewOptions);
+        }
+      },
+
+      renderMyProfile: function (section, subsection) {
+        if (
+          MetacatUI.appUserModel.get("checked") &&
+          !MetacatUI.appUserModel.get("loggedIn")
+        )
+          this.renderSignIn();
+        else if (!MetacatUI.appUserModel.get("checked")) {
+          this.listenToOnce(
+            MetacatUI.appUserModel,
+            "change:checked",
+            function () {
+              if (MetacatUI.appUserModel.get("loggedIn"))
+                this.renderProfile(
+                  MetacatUI.appUserModel.get("username"),
+                  section,
+                  subsection,
+                );
+              else this.renderSignIn();
+            },
+          );
+        } else if (
+          MetacatUI.appUserModel.get("checked") &&
+          MetacatUI.appUserModel.get("loggedIn")
+        ) {
+          this.renderProfile(
+            MetacatUI.appUserModel.get("username"),
+            section,
+            subsection,
+          );
+        }
+      },
+
+      logout: function (param) {
+        //Clear our browsing history when we log out
+        this.routeHistory.length = 0;
+
+        if (
+          (typeof MetacatUI.appModel.get("tokenUrl") == "undefined" ||
+            !MetacatUI.appModel.get("tokenUrl")) &&
+          !MetacatUI.appView.registryView
+        ) {
+          require(["views/RegistryView"], function (RegistryView) {
+            MetacatUI.appView.registryView = new RegistryView();
+            if (MetacatUI.appView.currentView.onClose)
+              MetacatUI.appView.currentView.onClose();
+            MetacatUI.appUserModel.logout();
+          });
+        } else {
+          if (
+            MetacatUI.appView.currentView &&
+            MetacatUI.appView.currentView.onClose
+          )
+            MetacatUI.appView.currentView.onClose();
+          MetacatUI.appUserModel.logout();
+        }
+      },
+
+      renderSignIn: function () {
+        var router = this;
 
-		renderMetadata: function (pid) {
-			pid = decodeURIComponent(pid);
-
-			this.routeHistory.push("metadata");
-			MetacatUI.appModel.set('lastPid', MetacatUI.appModel.get("pid"));
-
-			var seriesId;
-
-			//Check for a seriesId
-			if( pid.indexOf("version:") > -1 ){
-				seriesId = pid.substr(0, pid.indexOf(", version:"));
-
-				pid = pid.substr(pid.indexOf(", version: ") + ", version: ".length);
-			}
-
-			//Save the id in the app model
-			MetacatUI.appModel.set('pid', pid);
-
-			if(!MetacatUI.appView.metadataView){
-				require(['views/MetadataView'], function(MetadataView){
-					MetacatUI.appView.metadataView = new MetadataView();
-
-					//Send the id(s) to the view
-					MetacatUI.appView.metadataView.seriesId = seriesId;
-					MetacatUI.appView.metadataView.pid = pid;
-
-					MetacatUI.appView.showView(MetacatUI.appView.metadataView);
-				});
-			}
-			else{
-				//Send the id(s) to the view
-				MetacatUI.appView.metadataView.seriesId = seriesId;
-				MetacatUI.appView.metadataView.pid = pid;
-
-				// MetacatUI resets the dataPackage and dataPackageSynced
-				// attributes before rendering the view. These attributes are 
-				// initialized on a per-dataset basis to prevent displaying the 
-				// same dataset repeatedly.
-
-				MetacatUI.appView.metadataView.dataPackage = null;
-				MetacatUI.appView.metadataView.dataPackageSynced = false;
-
-				MetacatUI.appView.showView(MetacatUI.appView.metadataView);
-			}
-		},
-
-		renderProfile: function(username, section, subsection){
-			this.closeLastView();
-
-			var viewChoice;
-
-      //If there is a username specified and user profiles are disabled,
-      // forward to the entire repo profile view.
-      if( username && !MetacatUI.appModel.get("enableUserProfiles") ){
-        this.navigate("profile", { trigger: true, replace: true });
-        return;
-      }
-
-			if(!username){
-				this.routeHistory.push("summary");
-
-				// flag indicating /profile view
-				var viewOptions = { nodeSummaryView: true };
-
-				if(!MetacatUI.appView.statsView){
-
-					require(['views/StatsView'], function(StatsView){
-						MetacatUI.appView.statsView = new StatsView({
-							userType: "repository"
-						});
-
-						MetacatUI.appView.showView(MetacatUI.appView.statsView, viewOptions);
-					});
-				}
-				else
-					MetacatUI.appView.showView(MetacatUI.appView.statsView, viewOptions);
-			}
-			else{
-				this.routeHistory.push("profile");
-				MetacatUI.appModel.set("profileUsername", username);
-
-				if(section || subsection){
-					var viewOptions = { section: section, subsection: subsection }
-				}
-
-				if(!MetacatUI.appView.userView){
-
-					require(['views/UserView'], function(UserView){
-						MetacatUI.appView.userView = new UserView();
-
-						MetacatUI.appView.showView(MetacatUI.appView.userView, viewOptions);
-					});
-				}
-				else
-					MetacatUI.appView.showView(MetacatUI.appView.userView, viewOptions);
-			}
-		},
-
-		renderMyProfile: function(section, subsection){
-			if(MetacatUI.appUserModel.get("checked") && !MetacatUI.appUserModel.get("loggedIn"))
-				this.renderSignIn();
-			else if(!MetacatUI.appUserModel.get("checked")){
-				this.listenToOnce(MetacatUI.appUserModel, "change:checked", function(){
-					if(MetacatUI.appUserModel.get("loggedIn"))
-						this.renderProfile(MetacatUI.appUserModel.get("username"), section, subsection);
-					else
-						this.renderSignIn();
-				});
-			}
-			else if(MetacatUI.appUserModel.get("checked") && MetacatUI.appUserModel.get("loggedIn")){
-				this.renderProfile(MetacatUI.appUserModel.get("username"), section, subsection);
-			}
-		},
-
-		logout: function (param) {
-			//Clear our browsing history when we log out
-			this.routeHistory.length = 0;
-
-			if(((typeof MetacatUI.appModel.get("tokenUrl") == "undefined") || !MetacatUI.appModel.get("tokenUrl")) && !MetacatUI.appView.registryView){
-				require(['views/RegistryView'], function(RegistryView){
-					MetacatUI.appView.registryView = new RegistryView();
-					if(MetacatUI.appView.currentView.onClose)
-						MetacatUI.appView.currentView.onClose();
-					MetacatUI.appUserModel.logout();
-				});
-			}
-			else{
-				if(MetacatUI.appView.currentView && MetacatUI.appView.currentView.onClose)
-					MetacatUI.appView.currentView.onClose();
-				MetacatUI.appUserModel.logout();
-			}
-		},
-
-		renderSignIn: function(){
-
-			var router = this;
-
-			//If there is no SignInView yet, create one
-			if(!MetacatUI.appView.signInView){
-				require(['views/SignInView'], function(SignInView){
-					MetacatUI.appView.signInView = new SignInView({ el: "#Content", fullPage: true });
-					router.renderSignIn();
-				});
-
-				return;
-			}
-
-			//If the user status has been checked and they are already logged in, we will forward them to their profile
-			if( MetacatUI.appUserModel.get("checked") && MetacatUI.appUserModel.get("loggedIn") ){
-				this.navigate("my-profile", { trigger: true });
-				return;
-			}
-			//If the user status has been checked and they are NOT logged in, show the SignInView
-			else if( MetacatUI.appUserModel.get("checked") && !MetacatUI.appUserModel.get("loggedIn") ){
-				this.routeHistory.push("signin");
-				MetacatUI.appView.showView(MetacatUI.appView.signInView);
-			}
-			//If the user status has not been checked yet, wait for it
-			else if( !MetacatUI.appUserModel.get("checked") ){
-				this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.renderSignIn);
-        MetacatUI.appView.showView(MetacatUI.appView.signInView);
-			}
-		},
-
-		renderSignInSuccess: function(){
-			$("body").html("Sign-in successful.");
-			setTimeout(window.close, 1000);
-		},
-
-		renderLdapSignInSuccess: function(){
-
-			//If there is an LDAP sign in error message
-			if(window.location.pathname.indexOf("error=Unable%20to%20authenticate%20LDAP%20user") > -1){
-				this.renderLdapOnlySignInError();
-			}
-			else{
-				this.renderSignInSuccess();
-			}
-
-		},
-
-		renderLdapSignInError: function(){
-			this.routeHistory.push("signinldaperror");
-
-			if(!MetacatUI.appView.signInView){
-				require(['views/SignInView'], function(SignInView){
-					MetacatUI.appView.signInView = new SignInView({ el: "#Content"});
-					MetacatUI.appView.signInView.ldapError = true;
-					MetacatUI.appView.signInView.ldapOnly = true;
-					MetacatUI.appView.signInView.fullPage = true;
-					MetacatUI.appView.showView(MetacatUI.appView.signInView);
-				});
-			}
-			else{
-				MetacatUI.appView.signInView.ldapError = true;
-				MetacatUI.appView.signInView.ldapOnly = true;
-				MetacatUI.appView.signInView.fullPage = true;
-				MetacatUI.appView.showView(MetacatUI.appView.signInView);
-			}
-		},
-
-		renderLdapOnlySignInError: function(){
-			this.routeHistory.push("signinldaponlyerror");
-
-			if(!MetacatUI.appView.signInView){
-
-				require(['views/SignInView'], function(SignInView){
-					var signInView = new SignInView({ el: "#Content"});
-					signInView.ldapError = true;
-					signInView.ldapOnly = true;
-					signInView.fullPage = true;
-					MetacatUI.appView.showView(signInView);
-				});
-
-			}
-			else{
-
-				var signInView = new SignInView({ el: "#Content"});
-				signInView.ldapError = true;
-				signInView.ldapOnly = true;
-				signInView.fullPage = true;
-				MetacatUI.appView.showView(signInView);
-
-			}
-		},
-
-		renderLdapSignIn: function(){
-
-			this.routeHistory.push("signinLdap");
-
-			if(!MetacatUI.appView.signInView){
-				require(['views/SignInView'], function(SignInView){
-					MetacatUI.appView.signInView = new SignInView({ el: "#Content"});
-					MetacatUI.appView.signInView.ldapOnly = true;
-					MetacatUI.appView.signInView.fullPage = true;
-					MetacatUI.appView.showView(MetacatUI.appView.signInView);
-				});
-			}
-			else{
-				var signInLdapView = new SignInView({ el: "#Content"});
-				MetacatUI.appView.signInView.ldapOnly = true;
-				MetacatUI.appView.signInView.fullPage = true;
-				MetacatUI.appView.showView(signInLdapView);
-			}
-
-		},
-
-		navigateToDefault: function(){
-			//Navigate to the default view
-			this.navigate(MetacatUI.appModel.defaultView, {trigger: true});
-		},
-
-		/*
-		* Gets an array of route names that are set on this router.
-		* @return {Array} - An array of route names, not including any special characters
-		*/
-		getRouteNames: function(){
-
-			var router = this;
-
-		  var routeNames = _.map(Object.keys(this.routes), function(routeName){
-
-				return router.getRouteName(routeName);
-
-			});
-
-			//The "view" and portals routes are not included in the route hash (they are set up during initialize),
-			// so we have to manually add it here.
-			routeNames.push("view");
-      if( !routeNames.includes(MetacatUI.appModel.get("portalTermPlural")) ){
-        routeNames.push(MetacatUI.appModel.get("portalTermPlural"));
-      }
-
-			return routeNames;
-
-		},
-
-		/*
-		* Gets the route name based on the route pattern given
-		* @param {string} routePattern - A string that represents the route pattern e.g. "view(/pid)"
-		* @return {string} - The name of the route without any pattern special characters e.g. "view"
-		*/
-		getRouteName: function(routePattern){
-
-			var specialChars = ["/", "(", "*", ":"];
-
-			_.each(specialChars, function(specialChar){
-
-				var substring = routePattern.substring(0, routePattern.indexOf(specialChar));
+        //If there is no SignInView yet, create one
+        if (!MetacatUI.appView.signInView) {
+          require(["views/SignInView"], function (SignInView) {
+            MetacatUI.appView.signInView = new SignInView({
+              el: "#Content",
+              fullPage: true,
+            });
+            router.renderSignIn();
+          });
 
-				if( substring && substring.length < routePattern.length ){
-					routePattern = substring;
-				}
+          return;
+        }
 
-			});
+        //If the user status has been checked and they are already logged in, we will forward them to their profile
+        if (
+          MetacatUI.appUserModel.get("checked") &&
+          MetacatUI.appUserModel.get("loggedIn")
+        ) {
+          this.navigate("my-profile", { trigger: true });
+          return;
+        }
+        //If the user status has been checked and they are NOT logged in, show the SignInView
+        else if (
+          MetacatUI.appUserModel.get("checked") &&
+          !MetacatUI.appUserModel.get("loggedIn")
+        ) {
+          this.routeHistory.push("signin");
+          MetacatUI.appView.showView(MetacatUI.appView.signInView);
+        }
+        //If the user status has not been checked yet, wait for it
+        else if (!MetacatUI.appUserModel.get("checked")) {
+          this.listenToOnce(
+            MetacatUI.appUserModel,
+            "change:checked",
+            this.renderSignIn,
+          );
+          MetacatUI.appView.showView(MetacatUI.appView.signInView);
+        }
+      },
+
+      renderSignInSuccess: function () {
+        $("body").html("Sign-in successful.");
+        setTimeout(window.close, 1000);
+      },
 
-			return routePattern;
+      renderLdapSignInSuccess: function () {
+        //If there is an LDAP sign in error message
+        if (
+          window.location.pathname.indexOf(
+            "error=Unable%20to%20authenticate%20LDAP%20user",
+          ) > -1
+        ) {
+          this.renderLdapOnlySignInError();
+        } else {
+          this.renderSignInSuccess();
+        }
+      },
 
-		},
+      renderLdapSignInError: function () {
+        this.routeHistory.push("signinldaperror");
+
+        if (!MetacatUI.appView.signInView) {
+          require(["views/SignInView"], function (SignInView) {
+            MetacatUI.appView.signInView = new SignInView({ el: "#Content" });
+            MetacatUI.appView.signInView.ldapError = true;
+            MetacatUI.appView.signInView.ldapOnly = true;
+            MetacatUI.appView.signInView.fullPage = true;
+            MetacatUI.appView.showView(MetacatUI.appView.signInView);
+          });
+        } else {
+          MetacatUI.appView.signInView.ldapError = true;
+          MetacatUI.appView.signInView.ldapOnly = true;
+          MetacatUI.appView.signInView.fullPage = true;
+          MetacatUI.appView.showView(MetacatUI.appView.signInView);
+        }
+      },
 
-		closeLastView: function(){
-			//Get the last route and close the view
-			var lastRoute = _.last(this.routeHistory);
+      renderLdapOnlySignInError: function () {
+        this.routeHistory.push("signinldaponlyerror");
+
+        if (!MetacatUI.appView.signInView) {
+          require(["views/SignInView"], function (SignInView) {
+            var signInView = new SignInView({ el: "#Content" });
+            signInView.ldapError = true;
+            signInView.ldapOnly = true;
+            signInView.fullPage = true;
+            MetacatUI.appView.showView(signInView);
+          });
+        } else {
+          var signInView = new SignInView({ el: "#Content" });
+          signInView.ldapError = true;
+          signInView.ldapOnly = true;
+          signInView.fullPage = true;
+          MetacatUI.appView.showView(signInView);
+        }
+      },
 
-			if(lastRoute == "summary")
-				MetacatUI.appView.statsView.onClose();
-			else if(lastRoute == "profile")
-				MetacatUI.appView.userView.onClose();
-		},
+      renderLdapSignIn: function () {
+        this.routeHistory.push("signinLdap");
+
+        if (!MetacatUI.appView.signInView) {
+          require(["views/SignInView"], function (SignInView) {
+            MetacatUI.appView.signInView = new SignInView({ el: "#Content" });
+            MetacatUI.appView.signInView.ldapOnly = true;
+            MetacatUI.appView.signInView.fullPage = true;
+            MetacatUI.appView.showView(MetacatUI.appView.signInView);
+          });
+        } else {
+          var signInLdapView = new SignInView({ el: "#Content" });
+          MetacatUI.appView.signInView.ldapOnly = true;
+          MetacatUI.appView.signInView.fullPage = true;
+          MetacatUI.appView.showView(signInLdapView);
+        }
+      },
 
-		clearJSONLD: function() {
-			$("#jsonld").remove();
-		},
+      navigateToDefault: function () {
+        //Navigate to the default view
+        this.navigate(MetacatUI.appModel.defaultView, { trigger: true });
+      },
 
-		clearHighwirePressMetaTags: function() {
-			$("head > meta[name='citation_title']").remove()
-			$("head > meta[name='citation_authors']").remove()
-			$("head > meta[name='citation_publisher']").remove()
-			$("head > meta[name='citation_date']").remove()
-		}
+      /*
+       * Gets an array of route names that are set on this router.
+       * @return {Array} - An array of route names, not including any special characters
+       */
+      getRouteNames: function () {
+        var router = this;
 
-	});
+        var routeNames = _.map(Object.keys(this.routes), function (routeName) {
+          return router.getRouteName(routeName);
+        });
+
+        //The "view" and portals routes are not included in the route hash (they are set up during initialize),
+        // so we have to manually add it here.
+        routeNames.push("view");
+        if (!routeNames.includes(MetacatUI.appModel.get("portalTermPlural"))) {
+          routeNames.push(MetacatUI.appModel.get("portalTermPlural"));
+        }
+
+        return routeNames;
+      },
+
+      /*
+       * Gets the route name based on the route pattern given
+       * @param {string} routePattern - A string that represents the route pattern e.g. "view(/pid)"
+       * @return {string} - The name of the route without any pattern special characters e.g. "view"
+       */
+      getRouteName: function (routePattern) {
+        var specialChars = ["/", "(", "*", ":"];
+
+        _.each(specialChars, function (specialChar) {
+          var substring = routePattern.substring(
+            0,
+            routePattern.indexOf(specialChar),
+          );
+
+          if (substring && substring.length < routePattern.length) {
+            routePattern = substring;
+          }
+        });
+
+        return routePattern;
+      },
+
+      closeLastView: function () {
+        //Get the last route and close the view
+        var lastRoute = _.last(this.routeHistory);
+
+        if (lastRoute == "summary") MetacatUI.appView.statsView.onClose();
+        else if (lastRoute == "profile") MetacatUI.appView.userView.onClose();
+      },
+
+      clearJSONLD: function () {
+        $("#jsonld").remove();
+      },
+
+      clearHighwirePressMetaTags: function () {
+        $("head > meta[name='citation_title']").remove();
+        $("head > meta[name='citation_authors']").remove();
+        $("head > meta[name='citation_publisher']").remove();
+        $("head > meta[name='citation_date']").remove();
+      },
+    },
+  );
 
-	return UIRouter;
+  return UIRouter;
 });
 
diff --git a/docs/docs/src_js_views_AccessPolicyView.js.html b/docs/docs/src_js_views_AccessPolicyView.js.html index 6df9819df..c84583bd2 100644 --- a/docs/docs/src_js_views_AccessPolicyView.js.html +++ b/docs/docs/src_js_views_AccessPolicyView.js.html @@ -44,766 +44,854 @@

Source: src/js/views/AccessPolicyView.js

-
define(['underscore',
-        'jquery',
-        'backbone',
-        "models/AccessRule",
-        "collections/AccessPolicy",
-        "views/AccessRuleView",
-        "text!templates/accessPolicy.html",
-        "text!templates/filters/toggleFilter.html"],
-function(_, $, Backbone, AccessRule, AccessPolicy, AccessRuleView, Template, ToggleTemplate){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/AccessRule",
+  "collections/AccessPolicy",
+  "views/AccessRuleView",
+  "text!templates/accessPolicy.html",
+  "text!templates/filters/toggleFilter.html",
+], function (
+  _,
+  $,
+  Backbone,
+  AccessRule,
+  AccessPolicy,
+  AccessRuleView,
+  Template,
+  ToggleTemplate,
+) {
   /**
-  * @class AccessPolicyView
-  * @classdesc A view of an Access Policy of a DataONEObject
-  * @classcategory Views
-  * @extends Backbone.View
-  * @screenshot views/AccessPolicyView.png
-  * @constructor
-  */
+   * @class AccessPolicyView
+   * @classdesc A view of an Access Policy of a DataONEObject
+   * @classcategory Views
+   * @extends Backbone.View
+   * @screenshot views/AccessPolicyView.png
+   * @constructor
+   */
   var AccessPolicyView = Backbone.View.extend(
     /** @lends AccessPolicyView.prototype */
     {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "AccessPolicy",
+
+      /**
+       * The type of object/resource that this AccessPolicy is for. This is used for display purposes only.
+       * @example "dataset", "portal", "data file"
+       * @type {string}
+       */
+      resourceType: "resource",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "access-policy-view",
+
+      /**
+       * The AccessPolicy collection that is displayed in this View
+       * @type {AccessPolicy}
+       */
+      collection: undefined,
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       * @type {Underscore.Template}
+       */
+      template: _.template(Template),
+      toggleTemplate: _.template(ToggleTemplate),
+
+      /**
+       * Used to track the collection of models set on the view in order to handle
+       * undoing all changes made when we either hit Cancel or click otherwise
+       * hide the modal (such as clicking outside of it).
+       * @type {AccessRule[]}
+       * @since 2.15.0
+       */
+      cachedModels: null,
+
+      /**
+       * Whether or not changes to the accessPolicy managed by this view will be
+       * broadcasted to the accessPolicy of the editor's rootDataPackage's
+       * packageModle.
+       *
+       * This implementation is very likely to change in the future as we iron out
+       * how to handle bulk accessPolicy (and other) changes.
+       * @type {boolean}
+       * @since 2.15.0
+       */
+      broadcast: false,
+
+      /**
+       * A selector for the element in this view that contains the public/private toggle section
+       * @type {string}
+       * @since 2.15.0
+       */
+      publicToggleSection: "#public-toggle-section",
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "change .public-toggle-container input": "togglePrivacy",
+        "click .save": "save",
+        "click .cancel": "reset",
+        "click .access-rule .remove": "handleRemove",
+      },
+
+      /**
+       * Creates a new AccessPolicyView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        this.cachedModels = _.clone(this.collection.models);
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        try {
+          //If there is no AccessPolicy collection, then exit now
+          if (!this.collection) {
+            return;
+          }
 
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "AccessPolicy",
-
-    /**
-    * The type of object/resource that this AccessPolicy is for. This is used for display purposes only.
-    * @example "dataset", "portal", "data file"
-    * @type {string}
-    */
-    resourceType: "resource",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "access-policy-view",
-
-    /**
-    * The AccessPolicy collection that is displayed in this View
-    * @type {AccessPolicy}
-    */
-    collection: undefined,
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    * @type {Underscore.Template}
-    */
-    template: _.template(Template),
-    toggleTemplate: _.template(ToggleTemplate),
-
-    /**
-     * Used to track the collection of models set on the view in order to handle
-     * undoing all changes made when we either hit Cancel or click otherwise
-     * hide the modal (such as clicking outside of it).
-     * @type {AccessRule[]}
-     * @since 2.15.0
-     */
-    cachedModels: null,
-
-    /**
-     * Whether or not changes to the accessPolicy managed by this view will be
-     * broadcasted to the accessPolicy of the editor's rootDataPackage's
-     * packageModle.
-     *
-     * This implementation is very likely to change in the future as we iron out
-     * how to handle bulk accessPolicy (and other) changes.
-     * @type {boolean}
-     * @since 2.15.0
-     */
-    broadcast: false,
-
-    /**
-    * A selector for the element in this view that contains the public/private toggle section
-    * @type {string}
-    * @since 2.15.0
-    */
-    publicToggleSection: "#public-toggle-section",
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "change .public-toggle-container input" : "togglePrivacy",
-      "click .save" : "save",
-      "click .cancel": "reset",
-      "click .access-rule .remove" : "handleRemove"
-    },
+          var dataONEObject = this.collection.dataONEObject;
+
+          if (dataONEObject && dataONEObject.type) {
+            switch (dataONEObject.type) {
+              case "Portal":
+                this.resourceType =
+                  MetacatUI.appModel.get("portalTermSingular");
+                break;
+              case "DataPackage":
+                this.resourceType = "dataset";
+                break;
+              case "EML" || "ScienceMetadata":
+                this.resourceType = "metadata record";
+                break;
+              case "DataONEObject":
+                this.resourceType = "data file";
+                break;
+              case "Collection":
+                this.resourceType = "collection";
+                break;
+              default:
+                this.resourceType = "resource";
+                break;
+            }
+          } else {
+            this.resourceType = "resource";
+          }
 
-    /**
-    * Creates a new AccessPolicyView
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options) {
-      this.cachedModels = _.clone(this.collection.models);
-    },
+          //Insert the template into this view
+          this.$el.html(
+            this.template({
+              resourceType: this.resourceType,
+              fileName: dataONEObject.get("fileName"),
+            }),
+          );
+
+          //If the user is not authorized to change the permissions of this object,
+          // then skip rendering the rest of the AccessPolicy.
+          if (dataONEObject.get("isAuthorized_changePermission") === false) {
+            this.showUnauthorized();
+            return;
+          }
 
-    /**
-    * Renders this view
-    */
-    render: function(){
+          //Show the rightsHolder as an AccessRuleView
+          this.showRightsholder();
 
-      try{
+          var modelsToRemove = [];
 
-        //If there is no AccessPolicy collection, then exit now
-        if( !this.collection ){
-          return;
+          //Iterate over each AccessRule in the AccessPolicy and render a AccessRuleView
+          this.collection.each(function (accessRule) {
+            //Don't display access rules for the public since these are controlled via the public/private toggle
+            if (accessRule.get("subject") == "public") {
+              return;
+            }
+
+            //If this AccessRule is a duplicate of the rightsHolder, remove it from the policy and don't display it
+            if (
+              accessRule.get("subject") == dataONEObject.get("rightsHolder")
+            ) {
+              modelsToRemove.push(accessRule);
+              return;
+            }
+
+            //Create an AccessRuleView
+            var accessRuleView = new AccessRuleView();
+            accessRuleView.model = accessRule;
+            accessRuleView.accessPolicyView = this;
+
+            //Add the AccessRuleView to this view
+            this.$(".access-rules-container").append(accessRuleView.el);
+
+            //Render the view
+            accessRuleView.render();
+
+            //Listen to changes on the access rule, to check that there is at least one owner
+            this.listenTo(
+              accessRule,
+              "change:read change:write change:changePermission",
+              this.checkForOwners,
+            );
+          }, this);
+
+          //Remove each AccessRule from the AccessPolicy that should be removed.
+          // We don't remove these during the collection.each() function because it
+          // messes up the .each() iteration.
+          this.collection.remove(modelsToRemove);
+
+          //Get the subject info for each subject in the AccessPolicy, so we can display names
+          this.collection.getSubjectInfo();
+
+          //Show a blank row at the bottom of the table for adding a new Access Rule.
+          this.addEmptyRow();
+
+          //Render various help text for this view
+          this.renderHelpText();
+
+          //Render the public/private toggle, if it's enabled in the app config
+          this.renderPublicToggle();
+        } catch (e) {
+          MetacatUI.appView.showAlert(
+            "Something went wrong while trying to display the " +
+              MetacatUI.appModel.get("accessPolicyName") +
+              ". <p>Technical details: " +
+              e.message +
+              "</p>",
+            "alert-error",
+            this.$el,
+            null,
+          );
+          console.error(e);
         }
+      },
+
+      /**
+       * Renders a public/private toggle that toggles the public readability of the given resource.
+       */
+      renderPublicToggle: function () {
+        //Check if the public/private toggle is enabled. Default to enabling it.
+        var isEnabled = true,
+          enabledSubjects = [];
 
+        //Get the DataONEObject that this AccessPlicy is about
         var dataONEObject = this.collection.dataONEObject;
 
-        if(dataONEObject && dataONEObject.type){
-          switch( dataONEObject.type ){
-            case "Portal":
-              this.resourceType = MetacatUI.appModel.get("portalTermSingular");
-              break;
-            case "DataPackage":
-              this.resourceType = "dataset";
-              break;
-            case ("EML" || "ScienceMetadata"):
-              this.resourceType = "metadata record";
-              break;
-            case "DataONEObject":
-              this.resourceType = "data file";
-              break;
-            case "Collection":
-              this.resourceType = "collection";
-              break;
-            default:
-              this.resourceType = "resource";
-              break;
+        //If there is a DataONEObject model found, and it has a type
+        if (dataONEObject && dataONEObject.type) {
+          //Get the Portal configs from the AppConfig
+          if (dataONEObject.type == "Portal") {
+            isEnabled = MetacatUI.appModel.get("showPortalPublicToggle");
+            enabledSubjects = MetacatUI.appModel.get(
+              "showPortalPublicToggleForSubjects",
+            );
+          }
+          //Get the Dataset configs from the AppConfig
+          else {
+            isEnabled = MetacatUI.appModel.get("showDatasetPublicToggle");
+            enabledSubjects = MetacatUI.appModel.get(
+              "showDatasetPublicToggleForSubjects",
+            );
           }
         }
-        else{
-          this.resourceType = "resource";
-        }
-
-        //Insert the template into this view
-        this.$el.html(this.template({
-          resourceType: this.resourceType,
-          fileName: dataONEObject.get("fileName")
-        }));
 
-        //If the user is not authorized to change the permissions of this object,
-        // then skip rendering the rest of the AccessPolicy.
-        if( dataONEObject.get("isAuthorized_changePermission") === false ){
-          this.showUnauthorized();
+        //Get the public/private help text
+        let helpText = this.getPublicToggleHelpText();
+
+        // Or if the public toggle is limited to a set of users and/or groups, and the current user is
+        // not in that list, then display a message instead of the toggle
+        if (
+          !isEnabled ||
+          (Array.isArray(enabledSubjects) &&
+            enabledSubjects.length &&
+            !_.intersection(
+              enabledSubjects,
+              MetacatUI.appUserModel.get("allIdentitiesAndGroups"),
+            ).length)
+        ) {
+          let isPublicClass = this.collection.isPublic() ? "public" : "private";
+          this.$(".public-toggle-container").html(
+            $(document.createElement("p"))
+              .addClass("public-toggle-disabled-text " + isPublicClass)
+              .text(helpText),
+          );
+          this.$(this.publicToggleSection).find("p.help").remove();
           return;
         }
 
-        //Show the rightsHolder as an AccessRuleView
-        this.showRightsholder();
-
-        var modelsToRemove = [];
-
-        //Iterate over each AccessRule in the AccessPolicy and render a AccessRuleView
-        this.collection.each(function(accessRule){
-
-          //Don't display access rules for the public since these are controlled via the public/private toggle
-          if( accessRule.get("subject") == "public" ){
-            return;
-          }
-
-          //If this AccessRule is a duplicate of the rightsHolder, remove it from the policy and don't display it
-          if( accessRule.get("subject") == dataONEObject.get("rightsHolder") ){
-            modelsToRemove.push(accessRule);
-            return;
-          }
-
-          //Create an AccessRuleView
+        //Render the private/public toggle
+        this.$(".public-toggle-container")
+          .html(
+            this.toggleTemplate({
+              label: "",
+              id: this.collection.id,
+              trueLabel: "Public",
+              falseLabel: "Private",
+            }),
+          )
+          .tooltip({
+            placement: "top",
+            trigger: "hover",
+            title: helpText,
+            container: this.$(".public-toggle-container"),
+            delay: {
+              show: 800,
+            },
+          });
+
+        //If the dataset is public, check the checkbox
+        this.$(".public-toggle-container input").prop(
+          "checked",
+          this.collection.isPublic(),
+        );
+      },
+
+      /**
+       * Constructs and returns a message that explains if this resource is public or private. This message is displayed
+       * in the tooltip for the public/private toggle or in place of the toggle when the toggle is disabled. Override this
+       * function to create a custom message.
+       * @returns {string}
+       * @since 2.15.0
+       */
+      getPublicToggleHelpText: function () {
+        if (this.collection.isPublic()) {
+          return (
+            "Your " +
+            this.resourceType +
+            " is public. Anyone can see this " +
+            this.resourceType +
+            " in searches or by a direct link."
+          );
+        } else {
+          return (
+            "Your " +
+            this.resourceType +
+            " is private. Only people you approve can see this " +
+            this.resourceType +
+            "."
+          );
+        }
+      },
+
+      /**
+       * Render a row with input elements for adding a new AccessRule
+       */
+      addEmptyRow: function () {
+        try {
+          //Create a new AccessRule model and add to the collection
+          var accessRule = new AccessRule({
+            read: true,
+            dataONEObject: this.collection.dataONEObject,
+          });
+
+          //Create a new AccessRuleView
           var accessRuleView = new AccessRuleView();
           accessRuleView.model = accessRule;
-          accessRuleView.accessPolicyView = this;
+          accessRuleView.isNew = true;
+
+          this.listenTo(accessRule, "change", this.addAccessRule);
 
-          //Add the AccessRuleView to this view
+          //Add the new row to the table
           this.$(".access-rules-container").append(accessRuleView.el);
 
-          //Render the view
+          //Render the AccessRuleView
           accessRuleView.render();
+        } catch (e) {
+          console.error(
+            "Something went wrong while adding the empty access policy row ",
+            e,
+          );
+        }
+      },
+
+      /**
+       * Adds the given AccessRule model to the AccessPolicy collection associated with this view
+       * @param {AccessRule} accessRule - The AccessRule to add
+       */
+      addAccessRule: function (accessRule) {
+        //If this AccessPolicy already contains this AccessRule, then exit
+        if (this.collection.contains(accessRule)) {
+          return;
+        }
 
-          //Listen to changes on the access rule, to check that there is at least one owner
-          this.listenTo(accessRule, "change:read change:write change:changePermission", this.checkForOwners);
-
-        }, this);
+        //If there is no subject set on this AccessRule, exit
+        if (!accessRule.get("subject")) {
+          return;
+        }
 
-        //Remove each AccessRule from the AccessPolicy that should be removed.
-        // We don't remove these during the collection.each() function because it
-        // messes up the .each() iteration.
-        this.collection.remove(modelsToRemove);
+        //Add the AccessRule to the AccessPolicy
+        this.collection.push(accessRule);
 
-        //Get the subject info for each subject in the AccessPolicy, so we can display names
-        this.collection.getSubjectInfo();
+        //Get the name for this new person or group
+        accessRule.getSubjectInfo();
 
-        //Show a blank row at the bottom of the table for adding a new Access Rule.
+        //Render a new empty row
         this.addEmptyRow();
-
-        //Render various help text for this view
-        this.renderHelpText();
-
-        //Render the public/private toggle, if it's enabled in the app config
-        this.renderPublicToggle();
-
-      }
-      catch(e){
-        MetacatUI.appView.showAlert("Something went wrong while trying to display the " +
-                                      MetacatUI.appModel.get("accessPolicyName") +
-                                      ". <p>Technical details: " + e.message + "</p>",
-                                    "alert-error",
-                                    this.$el,
-                                    null);
-        console.error(e);
-      }
-
-    },
-
-    /**
-    * Renders a public/private toggle that toggles the public readability of the given resource.
-    */
-    renderPublicToggle: function(){
-
-      //Check if the public/private toggle is enabled. Default to enabling it.
-      var isEnabled = true,
-          enabledSubjects = [];
-
-      //Get the DataONEObject that this AccessPlicy is about
-      var dataONEObject = this.collection.dataONEObject;
-
-      //If there is a DataONEObject model found, and it has a type
-      if(dataONEObject && dataONEObject.type){
-        //Get the Portal configs from the AppConfig
-        if( dataONEObject.type == "Portal" ){
-          isEnabled = MetacatUI.appModel.get("showPortalPublicToggle");
-          enabledSubjects = MetacatUI.appModel.get("showPortalPublicToggleForSubjects");
-        }
-        //Get the Dataset configs from the AppConfig
-        else{
-          isEnabled = MetacatUI.appModel.get("showDatasetPublicToggle");
-          enabledSubjects = MetacatUI.appModel.get("showDatasetPublicToggleForSubjects");
-        }
-      }
-
-      //Get the public/private help text
-      let helpText = this.getPublicToggleHelpText();
-
-      // Or if the public toggle is limited to a set of users and/or groups, and the current user is
-      // not in that list, then display a message instead of the toggle
-      if( !isEnabled || (Array.isArray(enabledSubjects) && enabledSubjects.length &&
-          !_.intersection(enabledSubjects, MetacatUI.appUserModel.get("allIdentitiesAndGroups")).length)){
-            let isPublicClass = this.collection.isPublic()? "public" : "private";
-            this.$(".public-toggle-container").html( $(document.createElement("p")).addClass("public-toggle-disabled-text " + isPublicClass).text(helpText) );
-            this.$(this.publicToggleSection).find("p.help").remove();
-            return;
-      }
-
-      //Render the private/public toggle
-      this.$(".public-toggle-container").html(
-        this.toggleTemplate({
-          label: "",
-          id: this.collection.id,
-          trueLabel: "Public",
-          falseLabel: "Private"
-        })
-      ).tooltip({
-        placement: "top",
-        trigger: "hover",
-        title: helpText,
-        container: this.$(".public-toggle-container"),
-        delay: {
-          show: 800
+      },
+
+      /**
+       * Adds an AccessRuleView that represents the rightsHolder of the object.
+       *  The rightsHolder needs to be handled specially because it's not a regular access rule in the system metadata.
+       */
+      showRightsholder: function () {
+        //If the app is configured to hide the rightsHolder, then exit now
+        if (!MetacatUI.appModel.get("displayRightsHolderInAccessPolicy")) {
+          return;
         }
-      });
 
-      //If the dataset is public, check the checkbox
-      this.$(".public-toggle-container input").prop("checked", this.collection.isPublic());
-    },
-
-    /**
-    * Constructs and returns a message that explains if this resource is public or private. This message is displayed
-    * in the tooltip for the public/private toggle or in place of the toggle when the toggle is disabled. Override this
-    * function to create a custom message.
-    * @returns {string}
-    * @since 2.15.0
-    */
-    getPublicToggleHelpText: function(){
-      if( this.collection.isPublic() ){
-        return "Your " + this.resourceType + " is public. Anyone can see this " + this.resourceType + " in searches or by a direct link.";
-      }
-      else{
-        return "Your " + this.resourceType + " is private. Only people you approve can see this " + this.resourceType + ".";
-      }
-    },
-
-    /**
-    * Render a row with input elements for adding a new AccessRule
-    */
-    addEmptyRow: function(){
+        //Get the DataONEObject associated with this access policy
+        var dataONEObject = this.collection.dataONEObject;
 
-      try{
+        //If there is no DataONEObject associated with this access policy, then exit
+        if (!dataONEObject || !dataONEObject.get("rightsHolder")) {
+          return;
+        }
 
-        //Create a new AccessRule model and add to the collection
-        var accessRule = new AccessRule({
+        //Create an AccessRule model that represents the rightsHolder
+        var accessRuleModel = new AccessRule({
+          subject: dataONEObject.get("rightsHolder"),
           read: true,
-          dataONEObject: this.collection.dataONEObject
+          write: true,
+          changePermission: true,
+          dataONEObject: dataONEObject,
         });
 
-        //Create a new AccessRuleView
+        //Create an AccessRuleView
         var accessRuleView = new AccessRuleView();
-        accessRuleView.model = accessRule;
-        accessRuleView.isNew = true;
-
-        this.listenTo(accessRule, "change", this.addAccessRule);
-
-        //Add the new row to the table
-        this.$(".access-rules-container").append(accessRuleView.el);
+        accessRuleView.accessPolicyView = this;
+        accessRuleView.model = accessRuleModel;
+        accessRuleView.allowChanges = MetacatUI.appModel.get(
+          "allowChangeRightsHolder",
+        );
+
+        //Add the AccessRuleView to this view
+        if (this.$(".access-rules-container .new").length) {
+          this.$(".access-rules-container .new").before(accessRuleView.el);
+        } else {
+          this.$(".access-rules-container").append(accessRuleView.el);
+        }
 
-        //Render the AccessRuleView
+        //Render the view
         accessRuleView.render();
-      }
-      catch(e){
-        console.error("Something went wrong while adding the empty access policy row ", e);
-      }
 
-    },
-
-    /**
-    * Adds the given AccessRule model to the AccessPolicy collection associated with this view
-    * @param {AccessRule} accessRule - The AccessRule to add
-    */
-    addAccessRule: function(accessRule){
-
-      //If this AccessPolicy already contains this AccessRule, then exit
-      if( this.collection.contains(accessRule) ){
-        return;
-      }
-
-      //If there is no subject set on this AccessRule, exit
-      if( !accessRule.get("subject") ){
-        return;
-      }
-
-      //Add the AccessRule to the AccessPolicy
-      this.collection.push(accessRule);
-
-      //Get the name for this new person or group
-      accessRule.getSubjectInfo();
-
-      //Render a new empty row
-      this.addEmptyRow();
-
-    },
-
-    /**
-    * Adds an AccessRuleView that represents the rightsHolder of the object.
-    *  The rightsHolder needs to be handled specially because it's not a regular access rule in the system metadata.
-    */
-    showRightsholder: function(){
-
-      //If the app is configured to hide the rightsHolder, then exit now
-      if( !MetacatUI.appModel.get("displayRightsHolderInAccessPolicy") ){
-        return;
-      }
-
-      //Get the DataONEObject associated with this access policy
-      var dataONEObject = this.collection.dataONEObject;
-
-      //If there is no DataONEObject associated with this access policy, then exit
-      if( !dataONEObject || !dataONEObject.get("rightsHolder") ){
-        return;
-      }
-
-      //Create an AccessRule model that represents the rightsHolder
-      var accessRuleModel = new AccessRule({
-        subject: dataONEObject.get("rightsHolder"),
-        read: true,
-        write: true,
-        changePermission: true,
-        dataONEObject: dataONEObject
-      });
-
-      //Create an AccessRuleView
-      var accessRuleView = new AccessRuleView();
-      accessRuleView.accessPolicyView = this;
-      accessRuleView.model = accessRuleModel;
-      accessRuleView.allowChanges = MetacatUI.appModel.get("allowChangeRightsHolder");
-
-
-      //Add the AccessRuleView to this view
-      if( this.$(".access-rules-container .new").length ){
-        this.$(".access-rules-container .new").before(accessRuleView.el);
-      }
-      else{
-        this.$(".access-rules-container").append(accessRuleView.el);
-      }
-
-      //Render the view
-      accessRuleView.render();
-
-      //Get the name for this subject
-      accessRuleModel.getSubjectInfo();
-
-      //When the access type is changed, check that there is still at least one owner.
-      this.listenTo(accessRuleModel, "change:read change:write change:changePermission", this.checkForOwners);
+        //Get the name for this subject
+        accessRuleModel.getSubjectInfo();
+
+        //When the access type is changed, check that there is still at least one owner.
+        this.listenTo(
+          accessRuleModel,
+          "change:read change:write change:changePermission",
+          this.checkForOwners,
+        );
+      },
+
+      /**
+       * Checks that there is at least one owner of this resource, and displays a warning message if not.
+       * @param {AccessRule} accessRuleModel
+       */
+      checkForOwners: function (accessRuleModel) {
+        try {
+          if (!accessRuleModel) {
+            return;
+          }
 
-    },
+          //If changing the rightsHolder is disabled, we don't need to check for owners,
+          // since the rightsHolder will always be the owner.
+          if (
+            !MetacatUI.appModel.get("allowChangeRightsHolder") ||
+            !MetacatUI.appModel.get("displayRightsHolderInAccessPolicy")
+          ) {
+            return;
+          }
 
-    /**
-    * Checks that there is at least one owner of this resource, and displays a warning message if not.
-    * @param {AccessRule} accessRuleModel
-    */
-    checkForOwners: function(accessRuleModel){
+          //Get the rightsHolder for this resource
+          var rightsHolder;
+          if (
+            this.collection.dataONEObject &&
+            this.collection.dataONEObject.get("rightsHolder")
+          ) {
+            rightsHolder = this.collection.dataONEObject.get("rightsHolder");
+          }
 
-      try{
-        if( !accessRuleModel ){
-          return;
+          //Check if any priveleges have been removed
+          if (
+            !accessRuleModel.get("read") ||
+            !accessRuleModel.get("write") ||
+            !accessRuleModel.get("changePermission")
+          ) {
+            //If there is no owner of this resource
+            if (!this.collection.hasOwner()) {
+              //If there is no rightsHolder either, then make this person the rightsHolder
+              // or if this is the rightsHolder, keep them the rightsHolder
+              if (
+                !rightsHolder ||
+                rightsHolder == accessRuleModel.get("subject")
+              ) {
+                //Change this access rule back to an ownership level, since there needs to be at least one owner per object
+                accessRuleModel.set({
+                  read: true,
+                  write: true,
+                  changePermission: true,
+                });
+
+                this.showOwnerWarning();
+
+                if (!rightsHolder) {
+                  this.collection.dataONEObject.set(
+                    "rightsHolder",
+                    accessRuleModel.get("subject"),
+                  );
+                  this.collection.remove(accessRuleModel);
+                }
+              }
+              //If there is a rightsHolder, we don't need to do anything
+              else {
+                return;
+              }
+            }
+            //If the AccessRule model that was just changed was the rightsHolder,
+            // demote that subject as the rightsHolder, and replace with another subject
+            else if (rightsHolder == accessRuleModel.get("subject")) {
+              //Replace the rightsHolder with a different subject with ownership permissions
+              this.collection.replaceRightsHolder();
+
+              //Add the old rightsHolder AccessRule to the AccessPolicy
+              this.collection.add(accessRuleModel);
+            }
+          }
+        } catch (e) {
+          console.error(
+            "Could not check that there are owners in this access policy: ",
+            e,
+          );
         }
+      },
 
-        //If changing the rightsHolder is disabled, we don't need to check for owners,
-        // since the rightsHolder will always be the owner.
-        if( !MetacatUI.appModel.get("allowChangeRightsHolder") || !MetacatUI.appModel.get("displayRightsHolderInAccessPolicy") ){
-          return;
-        }
+      /**
+       * Checks that there is at least one owner of this resource, and displays a warning message if not.
+       * @param {Event} e
+       */
+      handleRemove: function (e) {
+        var accessRuleModel = $(e.target).parents(".access-rule").data("model");
 
         //Get the rightsHolder for this resource
         var rightsHolder;
-        if( this.collection.dataONEObject && this.collection.dataONEObject.get("rightsHolder") ){
+        if (
+          this.collection.dataONEObject &&
+          this.collection.dataONEObject.get("rightsHolder")
+        ) {
           rightsHolder = this.collection.dataONEObject.get("rightsHolder");
         }
 
-        //Check if any priveleges have been removed
-        if( !accessRuleModel.get("read") || !accessRuleModel.get("write") || !accessRuleModel.get("changePermission") ){
-
-          //If there is no owner of this resource
-          if( !this.collection.hasOwner() ){
-
-            //If there is no rightsHolder either, then make this person the rightsHolder
-            // or if this is the rightsHolder, keep them the rightsHolder
-            if( !rightsHolder || rightsHolder == accessRuleModel.get("subject")){
-
-              //Change this access rule back to an ownership level, since there needs to be at least one owner per object
-              accessRuleModel.set({
-                "read" : true,
-                "write" : true,
-                "changePermission" : true
-              });
-
-              this.showOwnerWarning();
-
-              if( !rightsHolder ){
-                this.collection.dataONEObject.set("rightsHolder", accessRuleModel.get("subject"));
-                this.collection.remove(accessRuleModel);
-              }
-            }
-            //If there is a rightsHolder, we don't need to do anything
-            else{
-              return;
-            }
+        //If the rightsHolder was just removed,
+        if (rightsHolder == accessRuleModel.get("subject")) {
+          //If changing the rightsHolder is disabled, we don't need to check for owners,
+          // since the rightsHolder will always be the owner.
+          if (
+            !MetacatUI.appModel.get("allowChangeRightsHolder") ||
+            !MetacatUI.appModel.get("displayRightsHolderInAccessPolicy")
+          ) {
+            return;
           }
-          //If the AccessRule model that was just changed was the rightsHolder,
-          // demote that subject as the rightsHolder, and replace with another subject
-          else if( rightsHolder == accessRuleModel.get("subject") ){
 
+          //If there is another owner of this resource
+          if (this.collection.hasOwner()) {
             //Replace the rightsHolder with a different subject with ownership permissions
             this.collection.replaceRightsHolder();
 
-            //Add the old rightsHolder AccessRule to the AccessPolicy
-            this.collection.add(accessRuleModel);
-
+            var accessRuleView = $(e.target)
+              .parents(".access-rule")
+              .data("view");
+            if (accessRuleView) {
+              accessRuleView.remove();
+            }
           }
+          //If there are no other owners of this dataset, keep this person as the rightsHolder
+          else {
+            this.showOwnerWarning();
+          }
+        } else {
+          //Remove the AccessRule from the AccessPolicy
+          this.collection.remove(accessRuleModel);
         }
+      },
+
+      /**
+       * Displays a warning message in this view that the object needs at least one owner.
+       */
+      showOwnerWarning: function () {
+        //Show warning message
+        var msgContainer = this.$(".modal-body").length
+          ? this.$(".modal-body")
+          : this.$el;
+        MetacatUI.appView.showAlert(
+          "At least one person or group needs to be an owner of this " +
+            this.resourceType +
+            ".",
+          "alert-warning",
+          msgContainer,
+          2000,
+          { remove: true },
+        );
+      },
+
+      /**
+       * Renders help text for the form in this view
+       */
+      renderHelpText: function () {
+        try {
+          //Create HTML that shows the access policy help text
+          var accessExplanationEl = $(document.createElement("div")),
+            listEl = $(document.createElement("ul")).addClass("unstyled");
+
+          accessExplanationEl.append(listEl);
+
+          //Get the AccessRule options names
+          var accessRuleOptionNames = MetacatUI.appModel.get(
+            "accessRuleOptionNames",
+          );
+          if (
+            typeof accessRuleOptionNames !== "object" ||
+            !Object.keys(accessRuleOptionNames).length
+          ) {
+            accessRuleOptionNames = {};
+          }
 
-      }
-      catch(e){
-        console.error("Could not check that there are owners in this access policy: ", e);
-      }
-
-    },
-
-    /**
-    * Checks that there is at least one owner of this resource, and displays a warning message if not.
-    * @param {Event} e
-    */
-    handleRemove: function(e){
-
-      var accessRuleModel = $(e.target).parents(".access-rule").data("model");
-
-      //Get the rightsHolder for this resource
-      var rightsHolder;
-      if( this.collection.dataONEObject && this.collection.dataONEObject.get("rightsHolder") ){
-        rightsHolder = this.collection.dataONEObject.get("rightsHolder");
-      }
+          //Create HTML that shows an explanation of each enabled access rule option
+          _.mapObject(
+            MetacatUI.appModel.get("accessRuleOptions"),
+            function (isEnabled, accessType) {
+              //If this access type is disabled, exit
+              if (!isEnabled) {
+                return;
+              }
 
-      //If the rightsHolder was just removed,
-      if( rightsHolder == accessRuleModel.get("subject") ){
+              var accessTypeExplanation = "",
+                accessTypeName = accessRuleOptionNames[accessType];
+
+              //Get explanation text for the given access type
+              switch (accessType) {
+                case "read":
+                  accessTypeExplanation =
+                    " - can view this content, even when it's private.";
+                  break;
+                case "write":
+                  accessTypeExplanation =
+                    " - can view and edit this content, even when it's private.";
+                  break;
+                case "changePermission":
+                  accessTypeExplanation =
+                    " - can view and edit this content, even when it's private. In addition, can add and remove other people from these " +
+                    MetacatUI.appModel.get("accessPolicyName") +
+                    ".";
+                  break;
+              }
 
-        //If changing the rightsHolder is disabled, we don't need to check for owners,
-        // since the rightsHolder will always be the owner.
-        if( !MetacatUI.appModel.get("allowChangeRightsHolder") || !MetacatUI.appModel.get("displayRightsHolderInAccessPolicy") ){
-          return;
+              //Add this to the list
+              listEl.append(
+                $(document.createElement("li")).append(
+                  $(document.createElement("h5")).text(accessTypeName),
+                  $(document.createElement("span")).text(accessTypeExplanation),
+                ),
+              );
+            },
+          );
+
+          //Add a popover to the Access column header to give more help text about the access types
+          this.$(".access-icon.popover-this").popover({
+            title: 'What does "Access" mean?',
+            delay: {
+              show: 800,
+            },
+            placement: "top",
+            trigger: "hover focus click",
+            container: this.$el,
+            html: true,
+            content: accessExplanationEl,
+          });
+        } catch (e) {
+          console.error("Could not render help text", e);
         }
-
-        //If there is another owner of this resource
-        if( this.collection.hasOwner() ){
-
-          //Replace the rightsHolder with a different subject with ownership permissions
-          this.collection.replaceRightsHolder();
-
-          var accessRuleView = $(e.target).parents(".access-rule").data("view");
-          if( accessRuleView ){
-            accessRuleView.remove();
-          }
-
+      },
+
+      /**
+       * Toggles the public-read AccessRule for this resource
+       */
+      togglePrivacy: function () {
+        //If this AccessPolicy is public already, make it private
+        if (this.collection.isPublic()) {
+          this.collection.makePrivate();
         }
-        //If there are no other owners of this dataset, keep this person as the rightsHolder
-        else{
-          this.showOwnerWarning();
+        //Otherwise, make it public
+        else {
+          this.collection.makePublic();
         }
+      },
 
-      }
-      else{
-        //Remove the AccessRule from the AccessPolicy
-        this.collection.remove(accessRuleModel);
-      }
-
-    },
-
-    /**
-    * Displays a warning message in this view that the object needs at least one owner.
-    */
-    showOwnerWarning: function(){
-      //Show warning message
-      var msgContainer = this.$(".modal-body").length? this.$(".modal-body") : this.$el;
-      MetacatUI.appView.showAlert("At least one person or group needs to be an owner of this " + this.resourceType + ".",
-                                  "alert-warning",
-                                  msgContainer,
-                                  2000,
-                                  { remove: true });
-    },
-
-    /**
-    * Renders help text for the form in this view
-    */
-    renderHelpText: function(){
-
-      try{
-        //Create HTML that shows the access policy help text
-        var accessExplanationEl = $(document.createElement("div")),
-            listEl              = $(document.createElement("ul")).addClass("unstyled");
+      /**
+       * Saves the AccessPolicy associated with this view
+       */
+      save: function () {
+        //Remove any alerts that are currently displayed
+        this.$(".alert-container").remove();
 
-        accessExplanationEl.append(listEl);
+        //Get the DataONE Object that this Access Policy is for
+        var dataONEObject = this.collection.dataONEObject;
 
-        //Get the AccessRule options names
-        var accessRuleOptionNames = MetacatUI.appModel.get("accessRuleOptionNames");
-        if( typeof accessRuleOptionNames !== "object" || !Object.keys(accessRuleOptionNames).length ){
-          accessRuleOptionNames = {};
+        if (!dataONEObject) {
+          return;
         }
 
-        //Create HTML that shows an explanation of each enabled access rule option
-        _.mapObject(MetacatUI.appModel.get("accessRuleOptions"), function(isEnabled, accessType){
-
-          //If this access type is disabled, exit
-          if( !isEnabled ){
-            return;
-          }
-
-          var accessTypeExplanation = "",
-              accessTypeName = accessRuleOptionNames[accessType];
-
-          //Get explanation text for the given access type
-          switch( accessType ){
-            case "read":
-              accessTypeExplanation = " - can view this content, even when it's private.";
-              break;
-            case "write":
-              accessTypeExplanation = " - can view and edit this content, even when it's private.";
-              break;
-            case "changePermission":
-              accessTypeExplanation = " - can view and edit this content, even when it's private. In addition, can add and remove other people from these " + MetacatUI.appModel.get("accessPolicyName") + ".";
-              break;
-          }
-
-          //Add this to the list
-          listEl.append($(document.createElement("li")).append(
-                          $(document.createElement("h5")).text(accessTypeName),
-                          $(document.createElement("span")).text(accessTypeExplanation)));
-
-        });
-
-        //Add a popover to the Access column header to give more help text about the access types
-        this.$(".access-icon.popover-this").popover({
-          title: "What does \"Access\" mean?",
-          delay: {
-            show: 800
-          },
-          placement: "top",
-          trigger: "hover focus click",
-          container: this.$el,
-          html: true,
-          content: accessExplanationEl
-        });
-      }
-      catch(e){
-        console.error("Could not render help text", e);
-      }
-    },
-
-    /**
-    * Toggles the public-read AccessRule for this resource
-    */
-    togglePrivacy: function(){
-
-      //If this AccessPolicy is public already, make it private
-      if( this.collection.isPublic() ){
-        this.collection.makePrivate();
-      }
-      //Otherwise, make it public
-      else{
-        this.collection.makePublic();
-      }
-
-    },
-
-    /**
-    * Saves the AccessPolicy associated with this view
-    */
-    save: function(){
+        // Broadcast the change across the package if appropriate
+        if (this.broadcast) {
+          MetacatUI.rootDataPackage.broadcastAccessPolicy(this.collection);
+        }
 
-      //Remove any alerts that are currently displayed
-      this.$(".alert-container").remove();
+        // Don't trigger a save if the item is new and just close the modal
+        if (dataONEObject.isNew()) {
+          $(this.$el).modal("hide");
 
-      //Get the DataONE Object that this Access Policy is for
-      var dataONEObject = this.collection.dataONEObject;
+          return;
+        }
 
-      if( !dataONEObject ){
-        return;
-      }
+        //Show the save progress as it is in progress, complete, in error, etc.
+        this.listenTo(
+          dataONEObject,
+          "change:uploadStatus",
+          this.showSaveProgress,
+        );
+
+        //Update the SystemMetadata for this object
+        dataONEObject.updateSysMeta();
+      },
+
+      /**
+       * Show visual cues in this view to show the user the status of the system metadata update.
+       * @param {DataONEObject} dataONEObject - The object being updated
+       */
+      showSaveProgress: function (dataONEObject) {
+        if (!dataONEObject) {
+          return;
+        }
 
-      // Broadcast the change across the package if appropriate
-      if (this.broadcast) {
-        MetacatUI.rootDataPackage.broadcastAccessPolicy(this.collection);
-      }
+        var status = dataONEObject.get("uploadStatus");
 
-      // Don't trigger a save if the item is new and just close the modal
-      if (dataONEObject.isNew()) {
-        $(this.$el).modal("hide");
+        //When the status is "in progress"
+        if (status == "p") {
+          //Disable the Save button and change the text to say, "Saving..."
+          this.$(".save.btn").text("Saving...").attr("disabled", "disabled");
+          this.$(".cancel.btn").attr("disabled", "disabled");
 
-        return;
-      }
+          return;
+        }
+        //When the status is "complete"
+        else if (status == "c") {
+          //Create a checkmark icon
+          var icon = $(document.createElement("i")).addClass(
+              "icon icon-ok icon-on-left",
+            ),
+            cancelBtn = this.$(".cancel.btn");
+          saveBtn = this.$(".save.btn");
 
-      //Show the save progress as it is in progress, complete, in error, etc.
-      this.listenTo(dataONEObject, "change:uploadStatus", this.showSaveProgress);
+          //Disable the Save button and change the text to say, "Saving..."
+          cancelBtn.text("Saved").removeAttr("disabled");
+          saveBtn.text("Saved").prepend(icon).removeAttr("disabled");
 
-      //Update the SystemMetadata for this object
-      dataONEObject.updateSysMeta();
+          setTimeout(function () {
+            saveBtn.empty().text("Save");
+          }, 2000);
 
-    },
+          this.cachedModels = _.clone(this.collection.models);
 
-    /**
-    * Show visual cues in this view to show the user the status of the system metadata update.
-    * @param {DataONEObject} dataONEObject - The object being updated
-    */
-    showSaveProgress: function(dataONEObject){
-      if( !dataONEObject ){
-        return;
-      }
-
-      var status = dataONEObject.get("uploadStatus");
-
-      //When the status is "in progress"
-      if( status == "p" ){
-        //Disable the Save button and change the text to say, "Saving..."
-        this.$(".save.btn").text("Saving...").attr("disabled", "disabled");
-        this.$(".cancel.btn").attr("disabled", "disabled");
-
-        return;
-      }
-      //When the status is "complete"
-      else if( status == "c" ){
-        //Create a checkmark icon
-        var icon = $(document.createElement("i")).addClass("icon icon-ok icon-on-left"),
-            cancelBtn = this.$(".cancel.btn");
-            saveBtn = this.$(".save.btn");
+          // Hide the modal only on a successful save
+          $(this.$el).modal("hide");
+        }
+        //When the status is "error"
+        else if (status == "e") {
+          var msgContainer = this.$(".modal-body").length
+            ? this.$(".modal-body")
+            : this.$el;
+
+          MetacatUI.appView.showAlert(
+            "Your changes could not be saved.",
+            "alert-error",
+            msgContainer,
+            0,
+            { remove: true },
+          );
+
+          //Reset the save button
+          this.$(".save.btn").text("Save").removeAttr("disabled");
+        }
 
-        //Disable the Save button and change the text to say, "Saving..."
-        cancelBtn.text("Saved").removeAttr("disabled");
-        saveBtn.text("Saved").prepend(icon).removeAttr("disabled");
+        //Remove the listener for this function
+        this.stopListening(
+          dataONEObject,
+          "change:uploadStatus",
+          this.showSaveProgress,
+        );
+      },
+
+      /**
+       * Resets the state of the models stored in the view's collection to the
+       * latest cached copy. Triggered either when the Cancel button is hit or
+       * the modal containing this view is hidden.
+       * @since 2.15.0
+       */
+      reset: function () {
+        if (!this.collection || !this.cachedModels) {
+          return;
+        }
 
-        setTimeout(function(){ saveBtn.empty().text("Save") }, 2000);
+        this.collection.set(this.cachedModels);
+      },
 
-        this.cachedModels = _.clone(this.collection.models);
+      /**
+       * Adds messaging to this view to tell the user they are unauthorized to change the AccessPolicy
+       * of this object(s)
+       */
+      showUnauthorized: function () {
+        //Get the container element for the message
+        var msgContainer = this.$(".modal-body").length
+          ? this.$(".modal-body")
+          : this.$el;
 
-        // Hide the modal only on a successful save
-        $(this.$el).modal("hide");
-      }
-      //When the status is "error"
-      else if( status == "e" ){
-        var msgContainer = this.$(".modal-body").length? this.$(".modal-body") : this.$el;
+        //Empty the container element
+        msgContainer.empty();
 
+        //Show the info message
         MetacatUI.appView.showAlert(
-          "Your changes could not be saved.",
-          "alert-error",
+          "The person who owns this " +
+            this.resourceType +
+            " has not given you permission to change the " +
+            MetacatUI.appModel.get("accessPolicyName") +
+            ". Contact the owner to be added " +
+            " as another owner of this " +
+            this.resourceType +
+            ".",
+          "alert-info subtle",
           msgContainer,
-          0,
-          { remove: true });
-
-        //Reset the save button
-        this.$(".save.btn").text("Save").removeAttr("disabled");
-      }
+          null,
+          { remove: false },
+        );
 
-      //Remove the listener for this function
-      this.stopListening(dataONEObject, "change:uploadStatus", this.showSaveProgress);
+        //Add an unauthorized class to this view for further styling options
+        this.$el.addClass("unauthorized");
+      },
     },
-
-    /**
-    * Resets the state of the models stored in the view's collection to the
-    * latest cached copy. Triggered either when the Cancel button is hit or
-    * the modal containing this view is hidden.
-    * @since 2.15.0
-    */
-    reset: function() {
-      if (!this.collection || !this.cachedModels) {
-        return;
-      }
-
-      this.collection.set(this.cachedModels);
-    },
-
-    /**
-    * Adds messaging to this view to tell the user they are unauthorized to change the AccessPolicy
-    * of this object(s)
-    */
-    showUnauthorized: function(){
-
-      //Get the container element for the message
-      var msgContainer = this.$(".modal-body").length? this.$(".modal-body") : this.$el;
-
-      //Empty the container element
-      msgContainer.empty();
-
-      //Show the info message
-      MetacatUI.appView.showAlert("The person who owns this " + this.resourceType + " has not given you permission to change the " +
-                                    MetacatUI.appModel.get("accessPolicyName") + ". Contact the owner to be added " +
-                                    " as another owner of this " + this.resourceType + ".",
-                                  "alert-info subtle",
-                                  msgContainer,
-                                  null,
-                                  { remove: false });
-
-      //Add an unauthorized class to this view for further styling options
-      this.$el.addClass("unauthorized");
-
-    }
-
-  });
+  );
 
   return AccessPolicyView;
-
 });
 
diff --git a/docs/docs/src_js_views_AccessRuleView.js.html b/docs/docs/src_js_views_AccessRuleView.js.html index a54cc21fe..d24d578bf 100644 --- a/docs/docs/src_js_views_AccessRuleView.js.html +++ b/docs/docs/src_js_views_AccessRuleView.js.html @@ -44,490 +44,550 @@

Source: src/js/views/AccessRuleView.js

-
define(['underscore',
-        'jquery',
-        'backbone',
-        "models/AccessRule"],
-function(_, $, Backbone, AccessRule){
-
+            
define(["underscore", "jquery", "backbone", "models/AccessRule"], function (
+  _,
+  $,
+  Backbone,
+  AccessRule,
+) {
   /**
-  * @class AccessRuleView
-  * @classdesc Renders a single access rule from an object's access policy
-  * @classcategory Views
-  * @screenshot views/AccessRuleView.png
-  * @extends Backbone.View
-  */
+   * @class AccessRuleView
+   * @classdesc Renders a single access rule from an object's access policy
+   * @classcategory Views
+   * @screenshot views/AccessRuleView.png
+   * @extends Backbone.View
+   */
   var AccessRuleView = Backbone.View.extend(
-    /** @lends AccessRuleView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "AccessRule",
-
-    /**
-    * The HTML tag name for this view's element
-    * @type {string}
-    */
-    tagName: "tr",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "access-rule",
-
-    /**
-    * The AccessRule model that is displayed in this view
-    * @type {AccessRule}
-    */
-    model: undefined,
-
-    /**
-    * If true, this view represents a new AccessRule that hasn't been added to the AccessPolicy yet
-    * @type {boolean}
-    */
-    isNew: false,
-
-    /**
-    * If true, the user can change the AccessRule via this view.
-    * If false, the AccessRule will just be displayed.
-    * @type {boolean}
-    */
-    allowChanges: true,
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "keypress .search input"        : "listenForEnter",
-      "click    .add.icon"            : "updateModel",
-      "change   .access select"       : "updateModel"
-    },
-
-    /**
-    * Is executed when a new AccessRuleView is created
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options){
-
-    },
-
-    /**
-    * Renders a single Access Rule
-    */
-    render: function(){
-
-      try{
-
-        this.$el.empty();
-
-        //If there's no model, exit now since there's nothing to render
-        if( !this.model ){
-          return;
-        }
-
-        //Get the subjects that should be hidden
-        var hiddenSubjects = MetacatUI.appModel.get("hiddenSubjectsInAccessPolicy");
-        //If this AccessRule is for a subject that should be hidden,
-        if( Array.isArray(hiddenSubjects) &&
-            _.contains(hiddenSubjects, this.model.get("subject") ) ){
-
-          var usersGroups = _.pluck(MetacatUI.appUserModel.get("isMemberOf"), "groupId");
-
-          //If the current user is not part of this hidden group or is not the hidden user
-          if( !_.contains(hiddenSubjects, MetacatUI.appUserModel.get("username")) &&
-              !_.intersection(hiddenSubjects, usersGroups).length){
-            //Remove this view
-            this.remove();
-            //Exit
+    /** @lends AccessRuleView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "AccessRule",
+
+      /**
+       * The HTML tag name for this view's element
+       * @type {string}
+       */
+      tagName: "tr",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "access-rule",
+
+      /**
+       * The AccessRule model that is displayed in this view
+       * @type {AccessRule}
+       */
+      model: undefined,
+
+      /**
+       * If true, this view represents a new AccessRule that hasn't been added to the AccessPolicy yet
+       * @type {boolean}
+       */
+      isNew: false,
+
+      /**
+       * If true, the user can change the AccessRule via this view.
+       * If false, the AccessRule will just be displayed.
+       * @type {boolean}
+       */
+      allowChanges: true,
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "keypress .search input": "listenForEnter",
+        "click    .add.icon": "updateModel",
+        "change   .access select": "updateModel",
+      },
+
+      /**
+       * Is executed when a new AccessRuleView is created
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {},
+
+      /**
+       * Renders a single Access Rule
+       */
+      render: function () {
+        try {
+          this.$el.empty();
+
+          //If there's no model, exit now since there's nothing to render
+          if (!this.model) {
             return;
           }
 
-        }
-
-        if( this.isNew ){
-
-          //If we aren't allowing changes to this AccessRule, then don't display
-          // anything for new AcccessRule rows
-          if( !this.allowChanges ){
-            return;
+          //Get the subjects that should be hidden
+          var hiddenSubjects = MetacatUI.appModel.get(
+            "hiddenSubjectsInAccessPolicy",
+          );
+          //If this AccessRule is for a subject that should be hidden,
+          if (
+            Array.isArray(hiddenSubjects) &&
+            _.contains(hiddenSubjects, this.model.get("subject"))
+          ) {
+            var usersGroups = _.pluck(
+              MetacatUI.appUserModel.get("isMemberOf"),
+              "groupId",
+            );
+
+            //If the current user is not part of this hidden group or is not the hidden user
+            if (
+              !_.contains(
+                hiddenSubjects,
+                MetacatUI.appUserModel.get("username"),
+              ) &&
+              !_.intersection(hiddenSubjects, usersGroups).length
+            ) {
+              //Remove this view
+              this.remove();
+              //Exit
+              return;
+            }
           }
 
-          this.$el.addClass("new");
+          if (this.isNew) {
+            //If we aren't allowing changes to this AccessRule, then don't display
+            // anything for new AcccessRule rows
+            if (!this.allowChanges) {
+              return;
+            }
+
+            this.$el.addClass("new");
 
-          //Create a text input for adding a subject or name
-          var label = $(document.createElement("label"))
-                        .attr("for", "search")
-                        .text("Search by name, ORCID, or group name")
-                        .addClass("subtle"),
+            //Create a text input for adding a subject or name
+            var label = $(document.createElement("label"))
+                .attr("for", "search")
+                .text("Search by name, ORCID, or group name")
+                .addClass("subtle"),
               input = $(document.createElement("input"))
-                        .attr("type", "text")
-                        .attr("name", "search")
-                        .attr("placeholder", "e.g. Lauren Walker"),
+                .attr("type", "text")
+                .attr("name", "search")
+                .attr("placeholder", "e.g. Lauren Walker"),
               hiddenInput = $(document.createElement("input"))
-                              .attr("type", "hidden")
-                              .attr("name", "subject")
-                              .addClass("hidden"),
+                .attr("type", "hidden")
+                .attr("name", "subject")
+                .addClass("hidden"),
               searchCell = $(document.createElement("td"))
-                             .addClass("search")
-                             .attr("colspan", "2")
-                             .append(label, input, hiddenInput),
-              view  = this;
-
-          //Setup the autocomplete widget for the input so users can search for people and groups
-          input.autocomplete({
-            source: function(request, response){
-              var beforeRequest = function(){
-                //loadingSpinner.show();
+                .addClass("search")
+                .attr("colspan", "2")
+                .append(label, input, hiddenInput),
+              view = this;
+
+            //Setup the autocomplete widget for the input so users can search for people and groups
+            input.autocomplete({
+              source: function (request, response) {
+                var beforeRequest = function () {
+                  //loadingSpinner.show();
+                };
+
+                var afterRequest = function () {
+                  //loadingSpinner.hide();
+                };
+
+                return MetacatUI.appLookupModel.getAccountsAutocomplete(
+                  request,
+                  response,
+                  beforeRequest,
+                  afterRequest,
+                );
+              },
+              select: function (e, ui) {
+                e.preventDefault();
+
+                var value = ui.item.value;
+                hiddenInput.val(value);
+                input.val(value);
+
+                view.updateSubject();
+              },
+              position: {
+                my: "left top",
+                at: "left bottom",
+                of: input,
+                collision: "flip",
+              },
+              appendTo: searchCell,
+              minLength: 2,
+            });
+
+            this.$el.append(searchCell);
+          } else {
+            try {
+              if (this.$el.is(".new")) {
+                this.$el.removeClass("new");
               }
 
-              var afterRequest = function(){
-                //loadingSpinner.hide();
+              //Create elements for the 'Name' column of this table row
+              var subject = this.model.get("subject"),
+                icon;
+
+              //If the subject is public, don't display an icon
+              if (subject == "public") {
+                icon = "";
+              }
+              //If this is a group subject, display the group icon
+              else if (this.model.isGroup()) {
+                icon = $(document.createElement("i")).addClass(
+                  "icon icon-on-left icon-group",
+                );
+              }
+              //If this is a username, display the user icon
+              else {
+                icon = $(document.createElement("i")).addClass(
+                  "icon icon-on-left icon-user",
+                );
               }
 
-              return MetacatUI.appLookupModel.getAccountsAutocomplete(request, response, beforeRequest, afterRequest)
-            },
-            select: function(e, ui) {
-              e.preventDefault();
-
-              var value = ui.item.value;
-              hiddenInput.val(value);
-              input.val(value);
-
-              view.updateSubject();
-
-            },
-            position: {
-              my: "left top",
-              at: "left bottom",
-              of: input,
-              collision: "flip"
-            },
-            appendTo: searchCell,
-            minLength: 2
-          });
-
-          this.$el.append( searchCell );
-        }
-        else{
-          try{
+              //Get the user or group's name - or use the subject, as a backup
+              var name = this.model.get("name") || subject;
 
-            if( this.$el.is(".new") ){
-              this.$el.removeClass("new");
-            }
-
-            //Create elements for the 'Name' column of this table row
-            var subject = this.model.get("subject"),
-                icon;
+              //Display "You" next to the user's own name, for extra helpfulness
+              if (subject == MetacatUI.appUserModel.get("username")) {
+                name += " (You)";
+              }
 
-            //If the subject is public, don't display an icon
-            if( subject == "public" ){
-              icon = "";
-            }
-            //If this is a group subject, display the group icon
-            else if( this.model.isGroup() ){
-              icon = $(document.createElement("i")).addClass("icon icon-on-left icon-group");
+              //Create an element for the name
+              var nameEl = $(document.createElement("span")).text(name);
+
+              this.$el.append(
+                $(document.createElement("td"))
+                  .addClass("name")
+                  .append(icon, nameEl),
+              );
+            } catch (e) {
+              console.error(
+                "Couldn't render the name column of the AccessRuleView: ",
+                e,
+              );
             }
-            //If this is a username, display the user icon
-            else{
-              icon = $(document.createElement("i")).addClass("icon icon-on-left icon-user");
-            }
-
-            //Get the user or group's name - or use the subject, as a backup
-            var name = this.model.get("name") || subject;
 
-            //Display "You" next to the user's own name, for extra helpfulness
-            if( subject == MetacatUI.appUserModel.get("username") ){
-              name += " (You)";
+            try {
+              //If this subject is an ORCID, display the ORCID and ORCID icon
+              if (subject.indexOf("orcid") >= 0) {
+                //Create the "subject/orcid" column
+                var orcidImg = $(document.createElement("img"))
+                    .attr("src", MetacatUI.root + "/img/orcid_64x64.png")
+                    .addClass("orcid icon icon-on-left"),
+                  orcid = $(document.createElement("span")).text(
+                    this.model.get("subject"),
+                  );
+
+                this.$el.append(
+                  $(document.createElement("td"))
+                    .addClass("subject")
+                    .append(orcidImg, orcid),
+                );
+              } else {
+                //For other subject types, don't show an ORCID icon
+                this.$el.append(
+                  $(document.createElement("td"))
+                    .addClass("subject")
+                    .text(this.model.get("subject")),
+                );
+              }
+            } catch (e) {
+              console.error(
+                "Couldn't render the subject column of the AccessRuleView: ",
+                e,
+              );
             }
-
-            //Create an element for the name
-            var nameEl = $(document.createElement("span")).text(name);
-
-            this.$el.append($(document.createElement("td")).addClass("name").append(icon, nameEl) );
-          }
-          catch(e){
-            console.error("Couldn't render the name column of the AccessRuleView: ", e);
           }
 
-          try{
-            //If this subject is an ORCID, display the ORCID and ORCID icon
-            if( subject.indexOf("orcid") >= 0 ){
-              //Create the "subject/orcid" column
-              var orcidImg = $(document.createElement("img")).attr("src", MetacatUI.root + "/img/orcid_64x64.png").addClass("orcid icon icon-on-left"),
-                  orcid = $(document.createElement("span")).text( this.model.get("subject") );
-
-              this.$el.append($(document.createElement("td")).addClass("subject").append(orcidImg, orcid) );
-            }
-            else{
-              //For other subject types, don't show an ORCID icon
-              this.$el.append($(document.createElement("td")).addClass("subject").text( this.model.get("subject") ));
+          try {
+            if (this.allowChanges) {
+              //Create the access/permission options select dropdown
+              var accessOptions = $(document.createElement("select"));
+
+              //Create option elements for each access rule type that is enabled in the app
+              _.mapObject(
+                MetacatUI.appModel.get("accessRuleOptions"),
+                function (isEnabled, optionType) {
+                  if (isEnabled) {
+                    var option = $(document.createElement("option"))
+                      .attr("value", optionType)
+                      .text(
+                        MetacatUI.appModel.get("accessRuleOptionNames")[
+                          optionType
+                        ],
+                      );
+
+                    //If this is the access type enabled in this AccessRule, then select this option
+                    if (this.model.get(optionType)) {
+                      option.prop("selected", "selected");
+                    }
+
+                    accessOptions.append(option);
+                  }
+                },
+                this,
+              );
+            } else {
+              //Create an element to display the access type
+              var accessOptions = $(document.createElement("span"));
+
+              //Create option elements for each access rule type that is enabled in the app
+              _.mapObject(
+                MetacatUI.appModel.get("accessRuleOptions"),
+                function (isEnabled, optionType) {
+                  //If this is the access type enabled in this AccessRule, then select this option
+                  if (this.model.get(optionType)) {
+                    accessOptions
+                      .text(
+                        MetacatUI.appModel.get("accessRuleOptionNames")[
+                          optionType
+                        ],
+                      )
+                      .attr("title", "This cannot be changed.");
+                  }
+                },
+                this,
+              );
             }
-          }
-          catch(e){
-            console.error("Couldn't render the subject column of the AccessRuleView: ", e);
-          }
-        }
-
-        try{
-
-          if( this.allowChanges ){
-            //Create the access/permission options select dropdown
-            var accessOptions = $(document.createElement("select"));
 
-            //Create option elements for each access rule type that is enabled in the app
-            _.mapObject(MetacatUI.appModel.get("accessRuleOptions"), function(isEnabled, optionType){
-              if( isEnabled ){
-                var option = $(document.createElement("option")).attr("value", optionType).text( MetacatUI.appModel.get("accessRuleOptionNames")[optionType] );
-
-                //If this is the access type enabled in this AccessRule, then select this option
-                if( this.model.get(optionType) ){
-                  option.prop("selected", "selected");
-                }
-
-                accessOptions.append(option);
-              }
-            }, this);
+            //Create the table cell and add the access options element
+            this.$el.append(
+              $(document.createElement("td"))
+                .addClass("access")
+                .append(accessOptions),
+            );
+          } catch (e) {
+            console.error(
+              "Couldn't render the access column of the AccessRuleView: ",
+              e,
+            );
           }
-          else{
-            //Create an element to display the access type
-            var accessOptions = $(document.createElement("span"));
-
-            //Create option elements for each access rule type that is enabled in the app
-            _.mapObject(MetacatUI.appModel.get("accessRuleOptions"), function(isEnabled, optionType){
-              //If this is the access type enabled in this AccessRule, then select this option
-              if( this.model.get(optionType) ){
-                accessOptions.text( MetacatUI.appModel.get("accessRuleOptionNames")[optionType] )
-                             .attr("title", "This cannot be changed.");
-              }
-            }, this);
-          }
-
-          //Create the table cell and add the access options element
-          this.$el.append($(document.createElement("td")).addClass("access").append(accessOptions) );
-        }
-        catch(e){
-          console.error("Couldn't render the access column of the AccessRuleView: ", e);
-        }
-
-        //Render the Remove column of the table
-        try{
 
-          if( this.isNew ){
-            var addIcon = $(document.createElement("i"))
-                            .addClass("add icon icon-plus")
-                            .attr("title", "Add this access");
-            //Create an empty table cell for "new" blank rows
-            this.$el.append($(document.createElement("td")).addClass("add-rule").append(addIcon));
-          }
-          else{
-            //Only display a remove icon if we are allowing changes to this AccessRule
-            if( this.allowChanges ){
-              //Create a remove icon
-              var userType   = this.model.isGroup()? "group" : "person",
+          //Render the Remove column of the table
+          try {
+            if (this.isNew) {
+              var addIcon = $(document.createElement("i"))
+                .addClass("add icon icon-plus")
+                .attr("title", "Add this access");
+              //Create an empty table cell for "new" blank rows
+              this.$el.append(
+                $(document.createElement("td"))
+                  .addClass("add-rule")
+                  .append(addIcon),
+              );
+            } else {
+              //Only display a remove icon if we are allowing changes to this AccessRule
+              if (this.allowChanges) {
+                //Create a remove icon
+                var userType = this.model.isGroup() ? "group" : "person",
                   removeIcon = $(document.createElement("i"))
-                                 .addClass("remove icon icon-remove")
-                                 .attr("title", "Remove access for this " + userType);
-
-              //Create a table cell and append the remove icon
-              this.$el.append($(document.createElement("td")).addClass("remove-rule").append(removeIcon) );
-            }
-            else{
-              //Add an empty table cell so the other rows don't look weird, if they have remove icons
-              this.$el.append($(document.createElement("td")));
+                    .addClass("remove icon icon-remove")
+                    .attr("title", "Remove access for this " + userType);
+
+                //Create a table cell and append the remove icon
+                this.$el.append(
+                  $(document.createElement("td"))
+                    .addClass("remove-rule")
+                    .append(removeIcon),
+                );
+              } else {
+                //Add an empty table cell so the other rows don't look weird, if they have remove icons
+                this.$el.append($(document.createElement("td")));
+              }
             }
+          } catch (e) {
+            console.error(
+              "Couldn't render a remove button for an access rule: ",
+              e,
+            );
           }
-        }
-        catch(e){
-          console.error("Couldn't render a remove button for an access rule: ", e);
-        }
-
-        //If there is no name set on this model, listen to when it may be set, and update the view
-        if( !this.model.get("name") ){
-          this.listenToOnce(this.model, "change:name", this.updateNameDisplay);
-        }
-
-        //Listen to changes on the access options and update the view if they are changed
-        this.listenTo(this.model, "change:read change:write change:changePermission", this.updateAccessDisplay);
-
-        //When the model is removed from the collection, remove this view
-        this.listenTo(this.model, "remove", this.onRemove);
-
-        //Attach the AccessRule model to the view element
-        this.$el.data("model", this.model);
-        this.$el.data("view", this);
 
-      }
-      catch(e){
-        console.error(e);
+          //If there is no name set on this model, listen to when it may be set, and update the view
+          if (!this.model.get("name")) {
+            this.listenToOnce(
+              this.model,
+              "change:name",
+              this.updateNameDisplay,
+            );
+          }
 
-        //Don't display a message to the user since this view is pretty small. Just remove it from the page.
-        this.$el.remove();
-      }
+          //Listen to changes on the access options and update the view if they are changed
+          this.listenTo(
+            this.model,
+            "change:read change:write change:changePermission",
+            this.updateAccessDisplay,
+          );
 
-    },
+          //When the model is removed from the collection, remove this view
+          this.listenTo(this.model, "remove", this.onRemove);
 
-    /**
-    * Update the name in this view with the name from the model
-    */
-    updateNameDisplay: function(){
-      //If there is no name set on the model, exit now, so that we don't show an empty string or falsey value
-      if( !this.model.get("name") ){
-        return;
-      }
+          //Attach the AccessRule model to the view element
+          this.$el.data("model", this.model);
+          this.$el.data("view", this);
+        } catch (e) {
+          console.error(e);
 
-      var name = this.model.get("name");
+          //Don't display a message to the user since this view is pretty small. Just remove it from the page.
+          this.$el.remove();
+        }
+      },
+
+      /**
+       * Update the name in this view with the name from the model
+       */
+      updateNameDisplay: function () {
+        //If there is no name set on the model, exit now, so that we don't show an empty string or falsey value
+        if (!this.model.get("name")) {
+          return;
+        }
 
-      //Display "You" next to the user's own name, for extra helpfulness
-      if( this.model.get("subject") == MetacatUI.appUserModel.get("username") ){
-        name += " (You)";
-      }
+        var name = this.model.get("name");
 
-      //Find the name element and update the text content
-      this.$(".name span").text(name);
+        //Display "You" next to the user's own name, for extra helpfulness
+        if (
+          this.model.get("subject") == MetacatUI.appUserModel.get("username")
+        ) {
+          name += " (You)";
+        }
 
-    },
+        //Find the name element and update the text content
+        this.$(".name span").text(name);
+      },
 
-    /**
-    * Update the AccessRule model with the selected access option
-    */
-    updateAccess: function(){
-      try{
-        //Get the value of the dropdown
-        var selection = this.$(".access select").val();
+      /**
+       * Update the AccessRule model with the selected access option
+       */
+      updateAccess: function () {
+        try {
+          //Get the value of the dropdown
+          var selection = this.$(".access select").val();
 
-        //If nothing was selected for some reason, exit now
-        if( !selection ){
-          return;
-        }
+          //If nothing was selected for some reason, exit now
+          if (!selection) {
+            return;
+          }
 
-        if( selection == "read" ){
-          this.model.set("read", true);
-          this.model.set("write", null);
-          this.model.set("changePermission", null);
+          if (selection == "read") {
+            this.model.set("read", true);
+            this.model.set("write", null);
+            this.model.set("changePermission", null);
+          } else if (selection == "write") {
+            this.model.set("read", true);
+            this.model.set("write", true);
+            this.model.set("changePermission", null);
+          } else if (selection == "changePermission") {
+            this.model.set("read", true);
+            this.model.set("write", true);
+            this.model.set("changePermission", true);
+          }
+        } catch (e) {
+          console.error(e);
         }
-        else if( selection == "write" ){
-          this.model.set("read", true);
-          this.model.set("write", true);
-          this.model.set("changePermission", null);
+      },
+
+      /**
+       * Update the access in this view with the access from the model
+       */
+      updateAccessDisplay: function () {
+        //Get the select dropdown menu from this view
+        var select = this.$(".access select");
+
+        //Update the select dropdown menu with the value from the model
+        if (this.model.get("changePermission")) {
+          select.val("changePermission");
+        } else if (this.model.get("write")) {
+          select.val("write");
+        } else {
+          select.val("read");
         }
-        else if( selection == "changePermission" ){
-          this.model.set("read", true);
-          this.model.set("write", true);
-          this.model.set("changePermission", true);
+      },
+
+      /**
+       * Update the subject of the AccessRule
+       */
+      updateSubject: function () {
+        //Get the subject from the hidden text input, which is populated from the
+        // jQueryUI autocomplete widget
+        var subject = this.$(".search input.hidden").val();
+
+        //If the hidden input doesn't have a value, get the value from the visible input
+        if (!subject) {
+          subject = this.$(".search input:not(.hidden)").val();
         }
 
-      }
-      catch(e){
-        console.error(e);
-      }
-    },
-
-    /**
-    * Update the access in this view with the access from the model
-    */
-    updateAccessDisplay: function(){
-
-      //Get the select dropdown menu from this view
-      var select = this.$(".access select");
-
-      //Update the select dropdown menu with the value from the model
-      if( this.model.get("changePermission") ){
-        select.val("changePermission");
-      }
-      else if( this.model.get("write") ){
-        select.val("write");
-      }
-      else{
-        select.val("read");
-      }
-
-    },
-
-    /**
-    * Update the subject of the AccessRule
-    */
-    updateSubject: function(){
-      //Get the subject from the hidden text input, which is populated from the
-      // jQueryUI autocomplete widget
-      var subject = this.$(".search input.hidden").val();
-
-      //If the hidden input doesn't have a value, get the value from the visible input
-      if( !subject ){
-        subject = this.$(".search input:not(.hidden)").val();
-      }
-
-      //If there is no subject typed in, exit
-      if( !subject ){
-        return;
-      }
-
-      //Set the subject on the model
-      this.model.set("subject", subject);
-
-      this.isNew = false;
-
-      this.render();
-    },
-
-    /**
-    * Updates the model associated with this view
-    */
-    updateModel: function(){
-
-      //Update the access and the subject
-      this.updateAccess();
-      this.updateSubject();
-
-    },
-
-    /**
-    * Remove this AccessRule from the AccessPolicy
-    */
-    onRemove: function(){
-
-      //If it is the rightsHolder of the object, don't remove the view
-      if(this.model.get("dataONEObject") && this.model.get("dataONEObject").get("rightsHolder") == this.model.get("subject")){
-        return;
-      }
-
-      //Remove this view from the page
-      this.remove();
-
-    },
-
-    /**
-    * Handles when the user has typed at least one character in the name search input
-    * @param {Event} e - The keypress event
-    */
-    listenForEnter: function(e){
-
-      try{
-
-        if( !e ){
+        //If there is no subject typed in, exit
+        if (!subject) {
           return;
         }
 
-        //If Enter was pressed,
-        if( e.keyCode == 13 ){
-          //Update the subject on this model
-          this.updateSubject();
+        //Set the subject on the model
+        this.model.set("subject", subject);
+
+        this.isNew = false;
+
+        this.render();
+      },
+
+      /**
+       * Updates the model associated with this view
+       */
+      updateModel: function () {
+        //Update the access and the subject
+        this.updateAccess();
+        this.updateSubject();
+      },
+
+      /**
+       * Remove this AccessRule from the AccessPolicy
+       */
+      onRemove: function () {
+        //If it is the rightsHolder of the object, don't remove the view
+        if (
+          this.model.get("dataONEObject") &&
+          this.model.get("dataONEObject").get("rightsHolder") ==
+            this.model.get("subject")
+        ) {
+          return;
         }
-      }
-      catch(e){
-        MetacatUI.appView.showAlert("This group or person could not be added.", "alert-error", this.$el, 3000);
-        console.error("Error while listening to the Enter key in AccessRuleView: ", e);
-      }
 
-    }
+        //Remove this view from the page
+        this.remove();
+      },
+
+      /**
+       * Handles when the user has typed at least one character in the name search input
+       * @param {Event} e - The keypress event
+       */
+      listenForEnter: function (e) {
+        try {
+          if (!e) {
+            return;
+          }
 
-  });
+          //If Enter was pressed,
+          if (e.keyCode == 13) {
+            //Update the subject on this model
+            this.updateSubject();
+          }
+        } catch (e) {
+          MetacatUI.appView.showAlert(
+            "This group or person could not be added.",
+            "alert-error",
+            this.$el,
+            3000,
+          );
+          console.error(
+            "Error while listening to the Enter key in AccessRuleView: ",
+            e,
+          );
+        }
+      },
+    },
+  );
 
   return AccessRuleView;
-
 });
 
diff --git a/docs/docs/src_js_views_AnnotationView.js.html b/docs/docs/src_js_views_AnnotationView.js.html index bffda3ad4..4d1a8298f 100644 --- a/docs/docs/src_js_views_AnnotationView.js.html +++ b/docs/docs/src_js_views_AnnotationView.js.html @@ -44,378 +44,391 @@

Source: src/js/views/AnnotationView.js

-
/*global define */
-define(['jquery',
-    'underscore',
-    'backbone',
-    'text!templates/bioportalAnnotationTemplate.html',],
-    function($, _, Backbone, AnnotationPopoverTemplate) {
-    'use strict';
-
-    /**
-    * @class AnnotationView
-    * @classdesc A view of a single semantic annotation for a metadata field. It is usually displayed on the {@link MetadataView}.
-    * @classcategory Views
-    * @extends Backbone.View
-    * @screenshot views/AnnotationView.png
-    * @constructor
-    */
-    var AnnotationView = Backbone.View.extend(
-      /** @lends AnnotationView.prototype */{
-        className: 'annotation-view',
-        annotationPopoverTemplate: _.template(AnnotationPopoverTemplate),
-
-        el: null,
-
-        events: {
-            "click" : "handleClick",
-            "click .annotation-popover-findmore" : "findMore",
-        },
-
-        /**
-         * Context string is a human-readable bit of text that comes out of the
-         * Metacat view service and describes the context of the annotation
-         * i.e., what entity, or which attribute within which entity the
-         * annotation is on
-         */
-        context: null,
-
-        // State. See initialize(), we store a bunch of info in these
-        property: null,
-        value: null,
-
-        initialize: function () {
-            // Detect legacy pill DOM structure with the old arrow,
-            // ┌───────────┬───────┬───┐
-            // │ property  │ value │ ↗ │
-            // └───────────┴───────┴───┘
-            // clean up, and disable ourselves. This can be removed at some
-            // point in the future
-            if (this.$el.find(".annotation-findmore").length > 0) {
-                this.$el.find(".annotation-findmore").remove();
-                this.$el.find(".annotation-value").attr("style", "color: white");
-
-                return;
-            }
-
-            this.property = {
-                type: "property",
-                el: null,
-                popover: null,
-                label: null,
-                uri: null,
-                definition: null,
-                ontology: null,
-                ontologyName: null,
-                resolved: false
-            };
-
-            this.value = {
-                type: "value",
-                el: null,
-                popover: null,
-                label: null,
-                uri: null,
-                definition: null,
-                ontology: null,
-                ontologyName: null,
-                resolved: false
-            };
-
-            this.property.el = this.$el.children(".annotation-property");
-            this.value.el = this.$el.children(".annotation-value");
-
-            // Bail now if things aren't set up right
-            if (!this.property.el || !this.value.el) {
-                return;
-            }
-
-            this.context = this.$el.data("context");
-            this.property.label = this.property.el.data("label");
-            this.property.uri = this.property.el.data("uri");
-            this.value.label = this.value.el.data("label");
-            this.value.uri = this.value.el.data("uri");
-
-            // Decode HTML tags in the context string, which is passed in as
-            // an HTML attribute from the XSLT so it needs encoding of some sort
-            // Note: Only supports < and > at this point
-            if (this.context) {
-              this.context = this.context.replace("&lt;", "<").replace("&gt;", ">");
-            }
-        },
-
-        /**
-         * Click handler for when the user clicks either the property or the
-         * value portion of the pill.
-         *
-         * If the popover hasn't yet been created for either, we create the
-         * popover and query BioPortal for more information. Otherwise, we do
-         * nothing and Bootstrap's default popover handling is triggered,
-         * showing the popover.
-         *
-         * @param {Event} e - Click event
-         */
-        handleClick: function (e) {
-            if (!this.property || !this.value) {
-                    return;
-            }
-
-            if (e.target.className === "annotation-property") {
-                if (this.property.popover) {
-                    return;
-                }
-
-                this.createPopover(this.property);
-                this.property.popover.popover("show");
-                this.queryAndUpdate(this.property);
-            } else if (e.target.className === "annotation-value" ||
-                e.target.className === "annotation-value-text") {
-                if (this.value.popover) {
-                    return;
-                }
-
-                this.createPopover(this.value);
-                this.value.popover.popover("show");
-                this.queryAndUpdate(this.value);
-            }
-        },
-
-        /**
-         * Update the value popover with the current state
-         *
-         * @param {Object} which - Which popover to create. Either this.property
-         * or this.value.
-         */
-        createPopover: function (which) {
-            var new_content = this.annotationPopoverTemplate({
-                context: this.context,
-                label: which.label,
-                uri: which.uri,
-                definition: which.definition,
-                ontology: which.ontology,
-                ontologyName: which.ontologyName,
-                resolved: which.resolved,
-                propertyURI: this.property.uri,
-                propertyLabel: this.property.label,
-                valueURI: this.value.uri,
-                valueLabel: this.value.label
-            });
-
-            which.el.data("content", new_content);
-
-            which.popover = which.el.popover({
-                container: which.el,
-                delay: 500,
-                trigger: "click"
-            });
-        },
-
-        /**
-         * Find a definition for the value URI either from cache or from
-         * Bioportal. Updates the popover if necessary.
-         *
-         * @param {Object} which - Which popover to create. Either this.property
-         * or this.value.
-         */
-        queryAndUpdate: function (which) {
-            if (which.resolved) {
-                return;
-            }
-
-            var viewRef = this,
-                cache = MetacatUI.appModel.get("bioportalLookupCache"),
-                token = MetacatUI.appModel.get("bioportalAPIKey");
-
-            // Attempt to grab from cache first
-            if (cache && cache[which.uri]) {
-                which.definition = cache[which.uri].definition;
-                which.ontology = cache[which.uri].links.ontology;
-
-                // Try to get a simpler name for the ontology, rather than just
-                // using the ontology URI, which is all Bioportal gives back
-                which.ontologyName = this.getFriendlyOntologyName(cache[which.uri].links.ontology);
-                which.resolved = true;
-                viewRef.updatePopover(which);
-
-                return;
-            }
-
-            // Verify token before moving on
-            if (typeof token !== "string" || token.length === 0) {
-                which.resolved = true;
-
-                return;
-            }
-
-            // Query the API and handle the response
-            // TODO: Looks like we should proxy this so the token doesn't leak
-            var url = MetacatUI.appModel.get("bioportalSearchUrl") +
-                "?q=" + encodeURIComponent(which.uri) +
-                "&apikey=" +
-                token;
-
-            $.get(url, function (data) {
-                var match = null;
-
-                // Verify response structure before trusting it
-                if (!data.collection ||
-                    !data.collection.length ||
-                    !data.collection.length > 0) {
-                    return;
-                }
-
-                // Find the first match by URI
-                match = _.find(data.collection, function(result) {
-                    return result["@id"] && result["@id"] === which.uri;
-                });
-
-                // Verify structure of response looks right and bail out if it
-                // doesn't
-                if (!match ||
-                    !match.definition ||
-                    !match.definition.length ||
-                    !match.definition.length > 0) {
-                    which.resolved = true;
-
-                    return;
-                }
-
-                which.definition = match.definition[0];
-                which.ontology = match.links.ontology;
-
-                // Try to get a simpler name for the ontology, rather than just
-                // using the ontology URI, which is all Bioportal gives back
-                which.ontologyName = viewRef.getFriendlyOntologyName(match.links.ontology);
-
-                which.resolved = true;
-                viewRef.updateCache(which.uri, match);
-                viewRef.updatePopover(which);
-            });
-        },
-
-        /**
-         * Update the popover data and raw HTML. This is necessary because
-         * we want to create the popover before we fetch the data to populate
-         * it from BioPortal and Bootstrap Popovers are designed to be static.
-         *
-         * The main trick I had to figure out here was that I could access
-         * the underlying content member of the popover with
-         * popover_data.options.content which wasn't documented in the API.
-         *
-         * @param {Object} which - Which popover to create. Either this.property
-         * or this.value.
-         */
-        updatePopover: function(which) {
-            var popover_content = $(which.popover).find(".popover-content")
-
-            var new_content = this.annotationPopoverTemplate({
-                context: this.context,
-                label: which.label,
-                uri: which.uri,
-                definition: which.definition,
-                ontology: which.ontology,
-                ontologyName: which.ontologyName,
-                resolved: which.resolved,
-                propertyURI: which.uri,
-                propertyLabel: which.label,
-                valueURI: this.value.uri,
-                valueLabel: this.value.label
-            });
-
-            // Update both the existing DOM and the underlying data
-            // attribute in order to persist the updated content between
-            // displays of the popover
-
-            // Update the Popover first
-            //
-            // This is a hack to work around the fact that we're updating the
-            // content of the popover after it is created. I read the source
-            // for Bootstrap's Popover and it showed the popover is generated
-            // from the data-popover attribute's content which has an
-            // options.content member we can modify directly
-            var popover_data = $(which.el).data('popover');
-
-            if (popover_data && popover_data.options && popover_data.options) {
-                popover_data.options.content = new_content;
-            }
-
-            $(which.el).data('popover', popover_data);
-
-            // Then update the DOM on the open popover
-            $(popover_content).html(new_content);
-        },
-
-        /**
-         * Update the cache for a given term.
-         *
-         * @param {string} term - The term URI
-         * @param {Object} match - The BioPortal match object for the term
-        */
-        updateCache: function(term, match) {
-            var cache = MetacatUI.appModel.get("bioportalLookupCache");
-
-            if (cache &&
-                typeof term === "string" &&
-                typeof match === "string") {
-                cache[term] = match;
-            }
-        },
-
-        /**
-         * Send the user to a pre-canned search for a term.
-         *
-         * @param {Event} e - Click event
-         */
-        findMore: function(e) {
-            e.preventDefault();
-
-            // Find the URI we need to filter on. Try the value first
-            var parent = $(e.target).parents(".annotation-value");
-
-            // Fall back to finding the URI from the property
-            if (parent.length <= 0) {
-                parent = $(e.target).parents(".annotation-property");
-            }
-
-            // Bail if we found neither
-            if (parent.length <= 0) {
-                return;
-            }
-
-            // Now grab the label and URI and filter
-            var label = $(parent).data("label"),
-                uri = $(parent).data("uri");
-
-            if (!label || !uri) {
-                return;
-            }
-
-            // Direct the user towards a search for the annotation
-            MetacatUI.appSearchModel.clear();
-            MetacatUI.appSearchModel.set('annotation', [{
-                label: label,
-                value: uri
-            }]);
-            MetacatUI.uiRouter.navigate('data', {trigger: true});
-        },
-
-        /**
-         * Get a friendly name (ie ECSO) from a long BioPortal URI
-         *
-         * @param {string} uri - A URI returned from the BioPortal API
-         * @return {string}
-         */
-        getFriendlyOntologyName: function(uri) {
-            if ((typeof uri === "string")) {
-                return uri;
-            }
-
-            return uri.replace("http://data.bioontology.org/ontologies/", "");
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/bioportalAnnotationTemplate.html",
+], function ($, _, Backbone, AnnotationPopoverTemplate) {
+  "use strict";
+
+  /**
+   * @class AnnotationView
+   * @classdesc A view of a single semantic annotation for a metadata field. It is usually displayed on the {@link MetadataView}.
+   * @classcategory Views
+   * @extends Backbone.View
+   * @screenshot views/AnnotationView.png
+   * @constructor
+   */
+  var AnnotationView = Backbone.View.extend(
+    /** @lends AnnotationView.prototype */ {
+      className: "annotation-view",
+      annotationPopoverTemplate: _.template(AnnotationPopoverTemplate),
+
+      el: null,
+
+      events: {
+        click: "handleClick",
+        "click .annotation-popover-findmore": "findMore",
+      },
+
+      /**
+       * Context string is a human-readable bit of text that comes out of the
+       * Metacat view service and describes the context of the annotation
+       * i.e., what entity, or which attribute within which entity the
+       * annotation is on
+       */
+      context: null,
+
+      // State. See initialize(), we store a bunch of info in these
+      property: null,
+      value: null,
+
+      initialize: function () {
+        // Detect legacy pill DOM structure with the old arrow,
+        // ┌───────────┬───────┬───┐
+        // │ property  │ value │ ↗ │
+        // └───────────┴───────┴───┘
+        // clean up, and disable ourselves. This can be removed at some
+        // point in the future
+        if (this.$el.find(".annotation-findmore").length > 0) {
+          this.$el.find(".annotation-findmore").remove();
+          this.$el.find(".annotation-value").attr("style", "color: white");
+
+          return;
         }
-    });
 
-    return AnnotationView;
-  });
+        this.property = {
+          type: "property",
+          el: null,
+          popover: null,
+          label: null,
+          uri: null,
+          definition: null,
+          ontology: null,
+          ontologyName: null,
+          resolved: false,
+        };
+
+        this.value = {
+          type: "value",
+          el: null,
+          popover: null,
+          label: null,
+          uri: null,
+          definition: null,
+          ontology: null,
+          ontologyName: null,
+          resolved: false,
+        };
+
+        this.property.el = this.$el.children(".annotation-property");
+        this.value.el = this.$el.children(".annotation-value");
+
+        // Bail now if things aren't set up right
+        if (!this.property.el || !this.value.el) {
+          return;
+        }
+
+        this.context = this.$el.data("context");
+        this.property.label = this.property.el.data("label");
+        this.property.uri = this.property.el.data("uri");
+        this.value.label = this.value.el.data("label");
+        this.value.uri = this.value.el.data("uri");
+
+        // Decode HTML tags in the context string, which is passed in as
+        // an HTML attribute from the XSLT so it needs encoding of some sort
+        // Note: Only supports < and > at this point
+        if (this.context) {
+          this.context = this.context.replace("&lt;", "<").replace("&gt;", ">");
+        }
+      },
+
+      /**
+       * Click handler for when the user clicks either the property or the
+       * value portion of the pill.
+       *
+       * If the popover hasn't yet been created for either, we create the
+       * popover and query BioPortal for more information. Otherwise, we do
+       * nothing and Bootstrap's default popover handling is triggered,
+       * showing the popover.
+       *
+       * @param {Event} e - Click event
+       */
+      handleClick: function (e) {
+        if (!this.property || !this.value) {
+          return;
+        }
+
+        if (e.target.className === "annotation-property") {
+          if (this.property.popover) {
+            return;
+          }
+
+          this.createPopover(this.property);
+          this.property.popover.popover("show");
+          this.queryAndUpdate(this.property);
+        } else if (
+          e.target.className === "annotation-value" ||
+          e.target.className === "annotation-value-text"
+        ) {
+          if (this.value.popover) {
+            return;
+          }
+
+          this.createPopover(this.value);
+          this.value.popover.popover("show");
+          this.queryAndUpdate(this.value);
+        }
+      },
+
+      /**
+       * Update the value popover with the current state
+       *
+       * @param {Object} which - Which popover to create. Either this.property
+       * or this.value.
+       */
+      createPopover: function (which) {
+        var new_content = this.annotationPopoverTemplate({
+          context: this.context,
+          label: which.label,
+          uri: which.uri,
+          definition: which.definition,
+          ontology: which.ontology,
+          ontologyName: which.ontologyName,
+          resolved: which.resolved,
+          propertyURI: this.property.uri,
+          propertyLabel: this.property.label,
+          valueURI: this.value.uri,
+          valueLabel: this.value.label,
+        });
+
+        which.el.data("content", new_content);
+
+        which.popover = which.el.popover({
+          container: which.el,
+          delay: 500,
+          trigger: "click",
+        });
+      },
+
+      /**
+       * Find a definition for the value URI either from cache or from
+       * Bioportal. Updates the popover if necessary.
+       *
+       * @param {Object} which - Which popover to create. Either this.property
+       * or this.value.
+       */
+      queryAndUpdate: function (which) {
+        if (which.resolved) {
+          return;
+        }
+
+        var viewRef = this,
+          cache = MetacatUI.appModel.get("bioportalLookupCache"),
+          token = MetacatUI.appModel.get("bioportalAPIKey");
+
+        // Attempt to grab from cache first
+        if (cache && cache[which.uri]) {
+          which.definition = cache[which.uri].definition;
+          which.ontology = cache[which.uri].links.ontology;
+
+          // Try to get a simpler name for the ontology, rather than just
+          // using the ontology URI, which is all Bioportal gives back
+          which.ontologyName = this.getFriendlyOntologyName(
+            cache[which.uri].links.ontology,
+          );
+          which.resolved = true;
+          viewRef.updatePopover(which);
+
+          return;
+        }
+
+        // Verify token before moving on
+        if (typeof token !== "string" || token.length === 0) {
+          which.resolved = true;
+
+          return;
+        }
+
+        // Query the API and handle the response
+        // TODO: Looks like we should proxy this so the token doesn't leak
+        var url =
+          MetacatUI.appModel.get("bioportalSearchUrl") +
+          "?q=" +
+          encodeURIComponent(which.uri) +
+          "&apikey=" +
+          token;
+
+        $.get(url, function (data) {
+          var match = null;
+
+          // Verify response structure before trusting it
+          if (
+            !data.collection ||
+            !data.collection.length ||
+            !data.collection.length > 0
+          ) {
+            return;
+          }
+
+          // Find the first match by URI
+          match = _.find(data.collection, function (result) {
+            return result["@id"] && result["@id"] === which.uri;
+          });
+
+          // Verify structure of response looks right and bail out if it
+          // doesn't
+          if (
+            !match ||
+            !match.definition ||
+            !match.definition.length ||
+            !match.definition.length > 0
+          ) {
+            which.resolved = true;
+
+            return;
+          }
+
+          which.definition = match.definition[0];
+          which.ontology = match.links.ontology;
+
+          // Try to get a simpler name for the ontology, rather than just
+          // using the ontology URI, which is all Bioportal gives back
+          which.ontologyName = viewRef.getFriendlyOntologyName(
+            match.links.ontology,
+          );
+
+          which.resolved = true;
+          viewRef.updateCache(which.uri, match);
+          viewRef.updatePopover(which);
+        });
+      },
+
+      /**
+       * Update the popover data and raw HTML. This is necessary because
+       * we want to create the popover before we fetch the data to populate
+       * it from BioPortal and Bootstrap Popovers are designed to be static.
+       *
+       * The main trick I had to figure out here was that I could access
+       * the underlying content member of the popover with
+       * popover_data.options.content which wasn't documented in the API.
+       *
+       * @param {Object} which - Which popover to create. Either this.property
+       * or this.value.
+       */
+      updatePopover: function (which) {
+        var popover_content = $(which.popover).find(".popover-content");
+
+        var new_content = this.annotationPopoverTemplate({
+          context: this.context,
+          label: which.label,
+          uri: which.uri,
+          definition: which.definition,
+          ontology: which.ontology,
+          ontologyName: which.ontologyName,
+          resolved: which.resolved,
+          propertyURI: which.uri,
+          propertyLabel: which.label,
+          valueURI: this.value.uri,
+          valueLabel: this.value.label,
+        });
+
+        // Update both the existing DOM and the underlying data
+        // attribute in order to persist the updated content between
+        // displays of the popover
+
+        // Update the Popover first
+        //
+        // This is a hack to work around the fact that we're updating the
+        // content of the popover after it is created. I read the source
+        // for Bootstrap's Popover and it showed the popover is generated
+        // from the data-popover attribute's content which has an
+        // options.content member we can modify directly
+        var popover_data = $(which.el).data("popover");
+
+        if (popover_data && popover_data.options && popover_data.options) {
+          popover_data.options.content = new_content;
+        }
+
+        $(which.el).data("popover", popover_data);
+
+        // Then update the DOM on the open popover
+        $(popover_content).html(new_content);
+      },
+
+      /**
+       * Update the cache for a given term.
+       *
+       * @param {string} term - The term URI
+       * @param {Object} match - The BioPortal match object for the term
+       */
+      updateCache: function (term, match) {
+        var cache = MetacatUI.appModel.get("bioportalLookupCache");
+
+        if (cache && typeof term === "string" && typeof match === "string") {
+          cache[term] = match;
+        }
+      },
+
+      /**
+       * Send the user to a pre-canned search for a term.
+       *
+       * @param {Event} e - Click event
+       */
+      findMore: function (e) {
+        e.preventDefault();
+
+        // Find the URI we need to filter on. Try the value first
+        var parent = $(e.target).parents(".annotation-value");
+
+        // Fall back to finding the URI from the property
+        if (parent.length <= 0) {
+          parent = $(e.target).parents(".annotation-property");
+        }
+
+        // Bail if we found neither
+        if (parent.length <= 0) {
+          return;
+        }
+
+        // Now grab the label and URI and filter
+        var label = $(parent).data("label"),
+          uri = $(parent).data("uri");
+
+        if (!label || !uri) {
+          return;
+        }
+
+        // Direct the user towards a search for the annotation
+        MetacatUI.appSearchModel.clear();
+        MetacatUI.appSearchModel.set("annotation", [
+          {
+            label: label,
+            value: uri,
+          },
+        ]);
+        MetacatUI.uiRouter.navigate("data", { trigger: true });
+      },
+
+      /**
+       * Get a friendly name (ie ECSO) from a long BioPortal URI
+       *
+       * @param {string} uri - A URI returned from the BioPortal API
+       * @return {string}
+       */
+      getFriendlyOntologyName: function (uri) {
+        if (typeof uri === "string") {
+          return uri;
+        }
+
+        return uri.replace("http://data.bioontology.org/ontologies/", "");
+      },
+    },
+  );
+
+  return AnnotationView;
+});
 
diff --git a/docs/docs/src_js_views_AppView.js.html b/docs/docs/src_js_views_AppView.js.html index 433d00e0c..e41b0a9aa 100644 --- a/docs/docs/src_js_views_AppView.js.html +++ b/docs/docs/src_js_views_AppView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/AppView.js

-
/*global define */
-define([
+            
define([
   "jquery",
   "underscore",
   "backbone",
@@ -70,7 +69,7 @@ 

Source: src/js/views/AppView.js

AppHeadTemplate, JsonLDTemplate, AppTemplate, - LoadingTemplate + LoadingTemplate, ) { "use strict"; @@ -115,7 +114,7 @@

Source: src/js/views/AppView.js

//Check for the LDAP sign in error message if ( window.location.search.indexOf( - "error=Unable%20to%20authenticate%20LDAP%20user" + "error=Unable%20to%20authenticate%20LDAP%20user", ) > -1 ) { window.location = @@ -129,18 +128,22 @@

Source: src/js/views/AppView.js

//Change the document title when the app changes the MetacatUI.appModel title at any time this.listenTo(MetacatUI.appModel, "change:title", this.changeTitle); - this.listenTo(MetacatUI.appModel, "change:description", this.changeDescription); + this.listenTo( + MetacatUI.appModel, + "change:description", + this.changeDescription, + ); this.checkIncompatibility(); }, /** - * The JS query selector for the element inside the AppView that contains the main view contents. When a new view is routed to - * and displayed via {@link AppView#showView}, the view will be inserted into this element. - * @type {string} - * @default "#Content" - * @since 2.22.0 - */ + * The JS query selector for the element inside the AppView that contains the main view contents. When a new view is routed to + * and displayed via {@link AppView#showView}, the view will be inserted into this element. + * @type {string} + * @default "#Content" + * @since 2.22.0 + */ contentSelector: "#Content", /** @@ -165,7 +168,7 @@

Source: src/js/views/AppView.js

changeDescription: function () { $("meta[name=description]").attr( "content", - MetacatUI.appModel.get("description") + MetacatUI.appModel.get("description"), ); }, @@ -186,7 +189,7 @@

Source: src/js/views/AppView.js

//If there is no AppView element on the page, don't render the application. if (!this.el) { console.error( - "Not rendering the UI of the app since the AppView HTML element (AppView.el) does not exist on the page. Make sure you have the AppView element included in index.html" + "Not rendering the UI of the app since the AppView HTML element (AppView.el) does not exist on the page. Make sure you have the AppView element included in index.html", ); return; } @@ -195,15 +198,15 @@

Source: src/js/views/AppView.js

$("head") .append( this.appHeadTemplate({ - theme: MetacatUI.theme - }) + theme: MetacatUI.theme, + }), ) //Add the JSON-LD to the head element .append( $(document.createElement("script")) .attr("type", "application/ld+json") .attr("id", "jsonld") - .html(this.jsonLDTemplate()) + .html(this.jsonLDTemplate()), ); // set up the body @@ -256,7 +259,7 @@

Source: src/js/views/AppView.js

showView: function (view, viewOptions) { if (!this.el) { console.error( - "Not rendering the UI of the app since the AppView HTML element (AppView.el) does not exist on the page. Make sure you have the AppView element included in index.html" + "Not rendering the UI of the app since the AppView HTML element (AppView.el) does not exist on the page. Make sure you have the AppView element included in index.html", ); return; } @@ -468,7 +471,7 @@

Source: src/js/views/AppView.js

emailOptions: emailOptions, remove: options.remove || false, includeEmail: options.includeEmail, - }).trim() + }).trim(), ); if (options.delay) { @@ -515,7 +518,7 @@

Source: src/js/views/AppView.js

classes, container, delay, - options + options, ) {}, /** @@ -559,7 +562,7 @@

Source: src/js/views/AppView.js

MetacatUI.appUserModel.get("loggedIn") ) this.listenForTimeout(); - } + }, ); return; @@ -575,7 +578,7 @@

Source: src/js/views/AppView.js

MetacatUI.appUserModel.get("loggedIn") ) this.listenForTimeout(); - } + }, ); return; @@ -636,7 +639,7 @@

Source: src/js/views/AppView.js

MetacatUI.appUserModel.get("loggedIn") ) this.listenForTimeout(); - } + }, ); } }, @@ -692,7 +695,7 @@

Source: src/js/views/AppView.js

function (browserRegEx) { var matches = navigator.userAgent.match(browserRegEx); return matches && matches.length > 0; - } + }, ); if (!isUnsupportedBrowser) { @@ -705,7 +708,7 @@

Source: src/js/views/AppView.js

"alert-warning", this.$el, false, - { remove: true } + { remove: true }, ); this.$el .children(".alert-container") @@ -765,10 +768,10 @@

Source: src/js/views/AppView.js

classes: classes, msg: MetacatUI.appModel.get("temporaryMessage"), includeEmail: MetacatUI.appModel.get( - "temporaryMessageIncludeEmail" + "temporaryMessageIncludeEmail", ), remove: true, - }) + }), ); //Add a class to the body in case we need to adjust other elements on the page @@ -877,11 +880,11 @@

Source: src/js/views/AppView.js

.stop(true, true) //stop first for it to work in FF .animate( { scrollTop: $(pageElement).offset().top - 40 - totalOffset }, - 1000 + 1000, ); return false; }, - } + }, ); return AppView; }); diff --git a/docs/docs/src_js_views_CitationHeaderView.js.html b/docs/docs/src_js_views_CitationHeaderView.js.html index cb201f9cc..939509ace 100644 --- a/docs/docs/src_js_views_CitationHeaderView.js.html +++ b/docs/docs/src_js_views_CitationHeaderView.js.html @@ -249,13 +249,13 @@

Source: src/js/views/CitationHeaderView.js

els.grp2, els.last, els.btn, - els.ellipsis + els.ellipsis, ); this.authorListIsOpen = true; } catch (error) { console.log( "Failed to expand an author list in the citation view.", - error + error, ); } }, @@ -277,13 +277,13 @@

Source: src/js/views/CitationHeaderView.js

els.ellipsis, els.last, els.btn, - els.grp2 + els.grp2, ); this.authorListIsOpen = false; } catch (error) { console.log( "Failed to collapse an author list in the citation view.", - error + error, ); } }, @@ -300,7 +300,7 @@

Source: src/js/views/CitationHeaderView.js

this.openList(); } }, - } + }, ); return CitationHeaderView; diff --git a/docs/docs/src_js_views_CitationListView.js.html b/docs/docs/src_js_views_CitationListView.js.html index ca9f30ae1..4ed2f1e51 100644 --- a/docs/docs/src_js_views_CitationListView.js.html +++ b/docs/docs/src_js_views_CitationListView.js.html @@ -44,176 +44,185 @@

Source: src/js/views/CitationListView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'collections/Citations', 'views/CitationView'],
-    function($, _, Backbone, Citations, CitationView) {
-    'use strict';
-
-    /**
-    * @class CitationListView
-    * @classdesc The CitationListView displays a list of Citation models
-    * @classcategory Views
-    * @extends Backbone.View
-    * @constructor
-    */
-    var CitationListView = Backbone.View.extend(
-      /** @lends CitationListView.prototype */{
-
-        id: 'table',
-        className: 'table',
-        citationsCollection: null,
-        emptyCitations: null,
-        citationsForDataCatalogView: null,
-
-        /**
-        * If true, the "register a citation" tool will display. This can be turned off/on
-        * with the {@link AppConfig#displayRegisterCitationTool} app configuration.
-        * @type {boolean}
-        * @since 2.15.0
-        */
-        displayRegisterCitationTool: MetacatUI.appModel.get("displayRegisterCitationTool"),
-
-        events: {
-
-        },
-
-        registerCitationTemplate:  _.template("<a class='btn register-citation' >" +
-                                                "<i class='icon icon-plus'>" +
-                                                "</i> Register Citation</a>"),
-
-        initialize: function(options) {
-            if((typeof options == "undefined")){
-                var options = {};
-                this.emptyCitations = true;
-                this.citationsForDataCatalogView = false;
-            }
-
-            if(typeof options.citations === "undefined") {
-                this.emptyCitations = true;
-            }
-
-            if( options.citationsForDataCatalogView !== "undefined" ) {
-                this.citationsForDataCatalogView = options.citationsForDataCatalogView;
-            }
-            else {
-                this.citationsForDataCatalogView = false;
-            }
-
-            // Initializing the Citation collection
-            this.citationsCollection = options.citations;
-
-        },
-
-
-        // retrun the DOM object to the calling view.
-        render: function() {
-            this.renderView();
-            return this;
-        },
-
-
-        // The renderView funciton creates a Citation table and appends every
-        // citation found in the citations collection object.
-        renderView: function() {
-            var self = this;
-
-            // Get node display name for the message
-            var nodeId = MetacatUI.appModel.get("nodeId");
-            // get the node Info
-            var nodeInfo =  _.find(MetacatUI.nodeModel.get("members"), function(nodeModel) {
-                return nodeModel.identifier.toLowerCase() == nodeId.toLowerCase();
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/Citations",
+  "views/CitationView",
+], function ($, _, Backbone, Citations, CitationView) {
+  "use strict";
+
+  /**
+   * @class CitationListView
+   * @classdesc The CitationListView displays a list of Citation models
+   * @classcategory Views
+   * @extends Backbone.View
+   * @constructor
+   */
+  var CitationListView = Backbone.View.extend(
+    /** @lends CitationListView.prototype */ {
+      id: "table",
+      className: "table",
+      citationsCollection: null,
+      emptyCitations: null,
+      citationsForDataCatalogView: null,
+
+      /**
+       * If true, the "register a citation" tool will display. This can be turned off/on
+       * with the {@link AppConfig#displayRegisterCitationTool} app configuration.
+       * @type {boolean}
+       * @since 2.15.0
+       */
+      displayRegisterCitationTool: MetacatUI.appModel.get(
+        "displayRegisterCitationTool",
+      ),
+
+      events: {},
+
+      registerCitationTemplate: _.template(
+        "<a class='btn register-citation' >" +
+          "<i class='icon icon-plus'>" +
+          "</i> Register Citation</a>",
+      ),
+
+      initialize: function (options) {
+        if (typeof options == "undefined") {
+          var options = {};
+          this.emptyCitations = true;
+          this.citationsForDataCatalogView = false;
+        }
+
+        if (typeof options.citations === "undefined") {
+          this.emptyCitations = true;
+        }
+
+        if (options.citationsForDataCatalogView !== "undefined") {
+          this.citationsForDataCatalogView =
+            options.citationsForDataCatalogView;
+        } else {
+          this.citationsForDataCatalogView = false;
+        }
+
+        // Initializing the Citation collection
+        this.citationsCollection = options.citations;
+      },
+
+      // retrun the DOM object to the calling view.
+      render: function () {
+        this.renderView();
+        return this;
+      },
+
+      // The renderView funciton creates a Citation table and appends every
+      // citation found in the citations collection object.
+      renderView: function () {
+        var self = this;
+
+        // Get node display name for the message
+        var nodeId = MetacatUI.appModel.get("nodeId");
+        // get the node Info
+        var nodeInfo = _.find(
+          MetacatUI.nodeModel.get("members"),
+          function (nodeModel) {
+            return nodeModel.identifier.toLowerCase() == nodeId.toLowerCase();
+          },
+        );
+        var nodeName = "DataONE";
+        if (nodeInfo !== undefined) var nodeName = nodeInfo.name;
+
+        if (this.emptyCitations) {
+          var $emptyList = $(document.createElement("div")).addClass(
+            "empty-citation-list",
+          );
+
+          // Dataset landing page - metadataview
+          if (self.citationsForDataCatalogView) {
+            var emptyString =
+              "We couldn't find any citations for this dataset.";
+
+            if (self.displayRegisterCitationTool)
+              emptyString +=
+                " If this dataset has been cited, you can register the citation to " +
+                nodeName +
+                ".";
+
+            var $emptyDataElement = $(document.createElement("p"))
+              .text(emptyString)
+              .addClass("empty-citation-list-text");
+
+            if (self.displayRegisterCitationTool)
+              $emptyList.append(this.registerCitationTemplate());
+
+            $emptyList.append($emptyDataElement);
+          } else {
+            var emptyString =
+              "We couldn't find any citations for these datasets. " +
+              "To report a citation of one of these datasets, " +
+              "send the citation information to our support team at ";
+
+            var $emptyDataElement = $(document.createElement("p"))
+              .text(emptyString)
+              .addClass("empty-citation-list-text");
+
+            // Adding Email link
+            var $emailLink = $("<a>", {
+              href: "mailto:" + MetacatUI.appModel.get("emailContact"),
+              text: MetacatUI.appModel.get("emailContact"),
             });
-            var nodeName = "DataONE"
-            if (nodeInfo !== undefined)
-                var nodeName = nodeInfo.name;
-
-            if (this.emptyCitations) {
-                var $emptyList = $(document.createElement("div"))
-                                            .addClass("empty-citation-list");
-
-                // Dataset landing page - metadataview
-                if ( self.citationsForDataCatalogView ) {
-                    var emptyString = "We couldn't find any citations for this dataset.";
-
-                    if (self.displayRegisterCitationTool)
-                        emptyString += " If this dataset has been cited, you can register the citation to " + nodeName + ".";
-
-                    var $emptyDataElement = $(document.createElement("p"))
-                        .text(emptyString)
-                        .addClass("empty-citation-list-text");
-
-                    if (self.displayRegisterCitationTool)
-                        $emptyList.append(this.registerCitationTemplate());
-
-                    $emptyList.append($emptyDataElement);
-
-                }
-                else {
-                    var emptyString = "We couldn't find any citations for these datasets. " +
-                        "To report a citation of one of these datasets, " +
-                        "send the citation information to our support team at " ;
-
-                    var $emptyDataElement = $(document.createElement("p"))
-                        .text(emptyString)
-                        .addClass("empty-citation-list-text");
-
-                    // Adding Email link
-                    var $emailLink = $('<a>', {
-                        href: 'mailto:' + MetacatUI.appModel.get("emailContact"),
-                        text: MetacatUI.appModel.get("emailContact")
-                    });
-                    $emptyDataElement.append($emailLink);
-
-                    $emptyList.append($emptyDataElement);
-                }
-
-                this.$el.append($emptyList);
-            }
-            else {
-
-                var $table = $(document.createElement("table"))
-                                            .addClass("metric-table table table-striped table-condensed");
-
-                var $tableBody = $(document.createElement("tbody"));
-
-                this.citationsCollection.each(
-                    function(model) {
-                        var citationView = new CitationView({
-                            model: model,
-                        });
-                        var $tableRow = $(document.createElement("tr"));
-                        var $tableCell = $(document.createElement("td"));
-                        $tableCell.append(citationView.render().$el);
-                        $tableRow.append($tableCell);
-                        $tableBody.append($tableRow);
-                    }
-                );
-
-                $table.append($tableBody);
-                this.$el.append($table);
-
-                // Dataset landing page - metadataview
-                if ( self.citationsForDataCatalogView ) {
-                    var $emptyList = $(document.createElement("div"))
-                                        .addClass("register-citation-element");
-
-                    var registerCitationString = "If this dataset has additional citations, you can now register that citation to " + nodeName + ".";
-
-                    var $registerCitationElement = $(document.createElement("p"))
-                        .text(registerCitationString)
-                        .addClass("register-citation-text");
-
-                    $emptyList.append($registerCitationElement);
-                    $emptyList.append(this.registerCitationTemplate());
-                }
-                this.$el.append($emptyList);
-            }
+            $emptyDataElement.append($emailLink);
+
+            $emptyList.append($emptyDataElement);
+          }
+
+          this.$el.append($emptyList);
+        } else {
+          var $table = $(document.createElement("table")).addClass(
+            "metric-table table table-striped table-condensed",
+          );
 
+          var $tableBody = $(document.createElement("tbody"));
+
+          this.citationsCollection.each(function (model) {
+            var citationView = new CitationView({
+              model: model,
+            });
+            var $tableRow = $(document.createElement("tr"));
+            var $tableCell = $(document.createElement("td"));
+            $tableCell.append(citationView.render().$el);
+            $tableRow.append($tableCell);
+            $tableBody.append($tableRow);
+          });
+
+          $table.append($tableBody);
+          this.$el.append($table);
+
+          // Dataset landing page - metadataview
+          if (self.citationsForDataCatalogView) {
+            var $emptyList = $(document.createElement("div")).addClass(
+              "register-citation-element",
+            );
+
+            var registerCitationString =
+              "If this dataset has additional citations, you can now register that citation to " +
+              nodeName +
+              ".";
+
+            var $registerCitationElement = $(document.createElement("p"))
+              .text(registerCitationString)
+              .addClass("register-citation-text");
+
+            $emptyList.append($registerCitationElement);
+            $emptyList.append(this.registerCitationTemplate());
+          }
+          this.$el.append($emptyList);
         }
-    });
+      },
+    },
+  );
 
-     return CitationListView;
-  });
+  return CitationListView;
+});
 
diff --git a/docs/docs/src_js_views_CitationView.js.html b/docs/docs/src_js_views_CitationView.js.html index 2f3675e9c..69eff86d9 100644 --- a/docs/docs/src_js_views_CitationView.js.html +++ b/docs/docs/src_js_views_CitationView.js.html @@ -61,7 +61,7 @@

Source: src/js/views/CitationView.js

APATemplate, ArchivedTemplate, APAInTextTemplate, - InTextArchivedTemplate + InTextArchivedTemplate, ) { "use strict"; @@ -443,7 +443,7 @@

Source: src/js/views/CitationView.js

* display all authors. * @since 2.23.0 */ - renderAPA: function (options, template, maxAuthors=20) { + renderAPA: function (options, template, maxAuthors = 20) { // Format the authors for display options.origin = this.CSLNamesToAPA(options.originArray, maxAuthors); this.el.innerHTML = template(options); @@ -488,7 +488,7 @@

Source: src/js/views/CitationView.js

*/ addCitationMetadata: function (citationMetadata) { const citationMetaEl = this.el.querySelector( - "." + this.citationMetadataClass + "." + this.citationMetadataClass, ); if (citationMetaEl) { // Render a CitationView for each citationMetadata @@ -554,7 +554,7 @@

Source: src/js/views/CitationView.js

* string for display in an APA citation. * @param {object[]} authors - An array of CSL JSON name objects * @returns {string} The formatted author string or an empty string if - * there are no authors + * there are no authors * @param {number} [maxAuthors=20] - The maximum number of authors to * display. If there are more than this number of authors, then the * remaining authors will be replaced with an ellipsis. The default is 20 @@ -562,7 +562,7 @@

Source: src/js/views/CitationView.js

* display all authors. * @since 2.23.0 */ - CSLNamesToAPA: function (authors, maxAuthors=20) { + CSLNamesToAPA: function (authors, maxAuthors = 20) { // Format authors as a proper APA style citation: if (!authors) return ""; @@ -684,7 +684,7 @@

Source: src/js/views/CitationView.js

name = name && given ? `${given} ${name}` : name; return name || literal; }, - } + }, ); return CitationView; diff --git a/docs/docs/src_js_views_ColorPaletteView.js.html b/docs/docs/src_js_views_ColorPaletteView.js.html index 313e43599..c037b00fb 100644 --- a/docs/docs/src_js_views_ColorPaletteView.js.html +++ b/docs/docs/src_js_views_ColorPaletteView.js.html @@ -44,60 +44,48 @@

Source: src/js/views/ColorPaletteView.js

-
define(['underscore',
-        'jquery',
-        'backbone'],
-function(_, $, Backbone){
-
+            
define(["underscore", "jquery", "backbone"], function (_, $, Backbone) {
   /**
-  * @class ColorPaletteView
-  * @classdesc A view that allows the user to select colors to form a color palette/scheme
-  * @classcategory Views
-  * @extends Backbone.View
-  * @constructor
-  */
+   * @class ColorPaletteView
+   * @classdesc A view that allows the user to select colors to form a color palette/scheme
+   * @classcategory Views
+   * @extends Backbone.View
+   * @constructor
+   */
   var ColorPaletteView = Backbone.View.extend(
-    /** @lends ColorPaletteView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "ColorPalette",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "color-palette",
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
+    /** @lends ColorPaletteView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "ColorPalette",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "color-palette",
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {},
+
+      /**
+       * Creates a new ColorPaletteView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {},
+
+      /**
+       * Renders this view
+       */
+      render: function () {},
     },
-
-    /**
-    * Creates a new ColorPaletteView
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options){
-
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-
-    }
-
-  });
+  );
 
   return ColorPaletteView;
-
 });
 
diff --git a/docs/docs/src_js_views_DataCatalogView.js.html b/docs/docs/src_js_views_DataCatalogView.js.html index 15c7d7bd8..2b0c5d1ad 100644 --- a/docs/docs/src_js_views_DataCatalogView.js.html +++ b/docs/docs/src_js_views_DataCatalogView.js.html @@ -44,3079 +44,3421 @@

Source: src/js/views/DataCatalogView.js

-
/*global define */
-define(["jquery",
-        "underscore",
-        "backbone",
-        "collections/SolrResults",
-        "models/Search",
-        "models/MetricsModel",
-        "common/Utilities",
-        "views/SearchResultView",
-        "views/searchSelect/AnnotationFilterView",
-        "text!templates/search.html",
-        "text!templates/statCounts.html",
-        "text!templates/pager.html",
-        "text!templates/mainContent.html",
-        "text!templates/currentFilter.html",
-        "text!templates/loading.html",
-        "gmaps",
-        "nGeohash"
-    ],
-    function(
-      $, _, Backbone, SearchResults, SearchModel,
-        MetricsModel, Utilities, SearchResultView, AnnotationFilter,
-        CatalogTemplate, CountTemplate, PagerTemplate, MainContentTemplate,
-        CurrentFilterTemplate, LoadingTemplate, gmaps, nGeohash
-    ) {
-        "use strict";
-
-        /**
-        * @class DataCatalogView
-        * @classcategory Views
-        * @extends Backbone.View
-        * @constructor
-        * @deprecated 
-        * @description This view is deprecated and will eventually be removed in a future version (likely 3.0.0)
-        */
-        var DataCatalogView = Backbone.View.extend(
-          /** @lends DataCatalogView.prototype */ {
-
-            el: "#Content",
-
-            isSubView: false,
-            filters: true, // Turn on/off the filters in this view
-
-            /**
-            * If true, the view height will be adjusted to fit the height of the window
-            * If false, the view height will be fixed via CSS
-            * @type {Boolean}
-            */
-            fixedHeight: false,
-
-            // The default global models for searching
-            searchModel: null,
-            searchResults: null,
-            statsModel: null,
-            mapModel: null,
-
-            /**
-            * The templates for this view
-            * @type {Underscore.template}
-            */
-            template: _.template(CatalogTemplate),
-            statsTemplate: _.template(CountTemplate),
-            pagerTemplate: _.template(PagerTemplate),
-            mainContentTemplate: _.template(MainContentTemplate),
-            currentFilterTemplate: _.template(CurrentFilterTemplate),
-            loadingTemplate: _.template(LoadingTemplate),
-            metricStatTemplate: _.template("<span class='metric-icon'> <i class='icon" +
-                " <%=metricIcon%>'></i> </span>" +
-                "<span class='metric-value'> <i class='icon metric-icon'>" +
-                "</i> </span>"),
-
-            // Search mode
-            mode: "map",
-
-            // Map settings and storage
-            map: null,
-            ready: false,
-            allowSearch: true,
-            hasZoomed: false,
-            hasDragged: false,
-            markers: {},
-            tiles: [],
-            tileCounts: [],
-
-            /**
-             * The general error message to show as a title in the error box when there
-             * is an error fetching results from solr
-             * @type {string}
-             * @default "Something went wrong while getting the list of datasets"
-             * @since 2.15.0
-             */
-            solrErrorTitle: "Something went wrong while getting the list of datasets",
-
-            /**
-             * The user-friendly text to show when a solr request gives a status 500
-             * error. If none is provided, then the error message that is returned from
-             * solr will be displayed.
-             * @type {string}
-             * @since 2.15.0
-             */
-            solrError500Message: null,
-
-            // Contains the geohashes for all the markers on the map (if turned on in the Map model)
-            markerGeohashes: [],
-            // Contains all the info windows for all the markers on the map (if turned on in the Map model)
-            markerInfoWindows: [],
-            // Contains all the info windows for each document in the search result list - to display on hover
-            tileInfoWindows: [],
-            // Contains all the currently visible markers on the map
-            resultMarkers: [],
-            // The geohash value for each tile drawn on the map
-            tileGeohashes: [],
-            mapFilterToggle: ".toggle-map-filter",
-
-            // Delegated events for creating new items, and clearing completed ones.
-            events: {
-                "click #results_prev": "prevpage",
-                "click #results_next": "nextpage",
-                "click #results_prev_bottom": "prevpage",
-                "click #results_next_bottom": "nextpage",
-                "click .pagerLink": "navigateToPage",
-                "click .filter.btn": "updateTextFilters",
-                "keypress input[type='text'].filter": "triggerOnEnter",
-                "focus input[type='text'].filter": "getAutocompletes",
-                "change #sortOrder": "triggerSearch",
-                "change #min_year": "updateYearRange",
-                "change #max_year": "updateYearRange",
-                "click #publish_year": "updateYearRange",
-                "click #data_year": "updateYearRange",
-                "click .remove-filter": "removeFilter",
-                "click input[type='checkbox'].filter": "updateBooleanFilters",
-                "click #clear-all": "resetFilters",
-                "click .remove-addtl-criteria": "removeAdditionalCriteria",
-                "click .collapse-me": "collapse",
-                "click .filter-contain .expand-collapse-control": "toggleFilterCollapse",
-                "click #toggle-map": "toggleMapMode",
-                "click .toggle-map": "toggleMapMode",
-                "click .toggle-list": "toggleList",
-                "click .toggle-map-filter": "toggleMapFilter",
-                "mouseover .open-marker": "showResultOnMap",
-                "mouseout .open-marker": "hideResultOnMap",
-                "mouseover .prevent-popover-runoff": "preventPopoverRunoff"
-            },
-
-            initialize: function(options) {
-                var view = this;
-
-                // Get all the options and apply them to this view
-                if (options) {
-                    var optionKeys = Object.keys(options);
-                    _.each(optionKeys, function(key, i) {
-                        view[key] = options[key];
-                    });
-                }
-            },
-
-            // Render the main view and/or re-render subviews. Don't call .html() here
-            // so we don't lose state, rather use .setElement(). Delegate rendering
-            // and event handling to sub views
-                render: function () {
-
-                // Use the global models if there are no other models specified at time of render
-                if ((MetacatUI.appModel.get("searchHistory").length > 0) &&
-                    (!this.searchModel || Object.keys(this.searchModel).length == 0)
-                ) {
-                  var lastSearchModels = _.last(MetacatUI.appModel.get("searchHistory"));
-
-                  if(lastSearchModels){
-
-                    if( lastSearchModels.search ){
-                      this.searchModel = lastSearchModels.search.clone();
-                    }
-
-                    if( lastSearchModels.map ){
-                      this.mapModel = lastSearchModels.map.clone();
-                    }
-                  }
-
-                } else if ((typeof MetacatUI.appSearchModel !== "undefined") &&
-                    (!this.searchModel || Object.keys(this.searchModel).length == 0)
-                ) {
-                    this.searchModel = MetacatUI.appSearchModel;
-                    this.mapModel = MetacatUI.mapModel;
-                    this.statsModel = MetacatUI.statsModel;
-                }
-
-                if (!this.mapModel && gmaps) {
-                    this.mapModel = MetacatUI.mapModel;
-                }
-
-                if (((typeof this.searchResults === "undefined") ||
-                    (!this.searchResults || Object.keys(this.searchResults).length == 0)) &&
-                    (MetacatUI.appSearchResults && (Object.keys(MetacatUI.appSearchResults).length > 0))
-                ) {
-                    this.searchResults = MetacatUI.appSearchResults;
-
-                    if( !this.statsModel ){
-                      this.statsModel = MetacatUI.statsModel;
-                    }
-
-                    if( !this.mapModel ){
-                      this.mapModel = MetacatUI.mapModel;
-                    }
-                }
-
-                // Get the search mode - either "map" or "list"
-                if ((typeof this.mode === "undefined") || !this.mode) {
-                    this.mode = MetacatUI.appModel.get("searchMode");
-                    if ((typeof this.mode === "undefined") || !this.mode) {
-                        this.mode = "map";
-                    }
-                    MetacatUI.appModel.set("searchMode", this.mode);
-                }
-                if ($(window).outerWidth() <= 600) {
-                    this.mode = "list";
-                    MetacatUI.appModel.set("searchMode", "list");
-                    gmaps = null;
-                }
-
-                if (!this.isSubView) {
-                    MetacatUI.appModel.set("headerType", "default");
-                    $("body").addClass("DataCatalog");
-                } else {
-                    this.$el.addClass("DataCatalog");
-                }
-
-                // Populate the search template with some model attributes
-                var loadingHTML = this.loadingTemplate({
-                    msg: "Retrieving member nodes..."
-                });
-
-                var templateVars = {
-                    gmaps: gmaps,
-                    mode: MetacatUI.appModel.get("searchMode"),
-                    useMapBounds: this.searchModel.get("useGeohash"),
-                    username: MetacatUI.appUserModel.get("username"),
-                    isMySearch: (_.indexOf(this.searchModel.get("username"), MetacatUI.appUserModel.get("username")) > -1),
-                    loading: loadingHTML,
-                    searchModelRef: this.searchModel,
-                    searchResultsRef: this.searchResults,
-                    dataSourceTitle: (MetacatUI.theme == "dataone") ? "Member Node" : "Data source"
-                }
-                var cel = this.template(_.extend(this.searchModel.toJSON(), templateVars));
-
-                this.$el.html(cel);
-
-                //Hide the filters that are disabled in the AppModel settings
-                _.each( this.$(".filter-contain[data-category]"), function(filterEl){
-                  if( ! _.contains(MetacatUI.appModel.get("defaultSearchFilters"), $(filterEl).attr("data-category")) ){
-                    $(filterEl).hide();
-                  }
-                }, this);
-
-                // Store some references to key views that we use repeatedly
-                this.$resultsview = this.$("#results-view");
-                this.$results = this.$("#results");
-
-                // Update stats
-                this.updateStats();
-
-                // Render the Google Map
-                this.renderMap();
-
-                // Initialize the tooltips
-                var tooltips = $(".tooltip-this");
-
-                // Find the tooltips that are on filter labels - add a slight delay to those
-                var groupedTooltips = _.groupBy(tooltips, function(t) {
-                    return ((($(t).prop("tagName") == "LABEL") ||
-                        ($(t).parent().prop("tagName") == "LABEL")) &&
-                        ($(t).parents(".filter-container").length > 0))
-                });
-                var forFilterLabel = true,
-                    forOtherElements = false;
-
-                $(groupedTooltips[forFilterLabel]).tooltip({
-                    delay: {
-                        show: "800"
-                    }
-                });
-                $(groupedTooltips[forOtherElements]).tooltip();
-
-                // Initialize all popover elements
-                $(".popover-this").popover();
-
-                // Initialize the resizeable content div
-                $("#content").resizable({
-                    handles: "n,s,e,w"
-                });
-
-                // Collapse the filters
-                this.toggleFilterCollapse();
-
-                // Iterate through each search model text attribute and show UI filter for each
-                var categories = ["all", "attribute", "creator", "id", "taxon", "spatial",
-                    "additionalCriteria", "annotation", "isPrivate"];
-                var thisTerm = null;
-
-                for (var i = 0; i < categories.length; i++) {
-                    thisTerm = this.searchModel.get(categories[i]);
-
-                    if (thisTerm === undefined || thisTerm === null) break;
-
-                    for (var x = 0; x < thisTerm.length; x++) {
-                        this.showFilter(categories[i], thisTerm[x]);
-                    }
-                }
-
-                // List the Member Node filters
-                var view = this;
-                _.each(_.contains(MetacatUI.appModel.get("defaultSearchFilters"), "dataSource"), function(source, i) {
-                    view.showFilter("dataSource", source);
-                });
-
-                // Add the custom query under the "Anything" filter
-                if (this.searchModel.get("customQuery")) {
-                    this.showFilter("all", this.searchModel.get("customQuery"));
-                }
-
-                // Register listeners; this is done here in render because the HTML
-                // needs to be bound before the listenTo call can be made
-                this.stopListening(this.searchResults);
-                this.stopListening(this.searchModel);
-                this.stopListening(MetacatUI.appModel);
-                this.listenTo(this.searchResults, "reset", this.cacheSearch);
-                this.listenTo(this.searchResults, "add", this.addOne);
-                this.listenTo(this.searchResults, "reset", this.addAll);
-                this.listenTo(this.searchResults, "reset", this.checkForProv);
-                this.listenTo(this.searchResults, "error", this.showError);
-
-                // List data sources
-                this.listDataSources();
-                this.listenTo(MetacatUI.nodeModel, "change:members", this.listDataSources);
-
-                // listen to the MetacatUI.appModel for the search trigger
-                this.listenTo(MetacatUI.appModel, "search", this.getResults);
-
-                this.listenTo(MetacatUI.appUserModel, "change:loggedIn", this.triggerSearch);
-
-                // and go to a certain page if we have it
-                this.getResults();
-
-                // Set a custom height on any elements that have the .auto-height class
-                if ($(".auto-height").length > 0 && !this.fixedHeight) {
-                    // Readjust the height whenever the window is resized
-                    $(window).resize(this.setAutoHeight);
-                    $(".auto-height-member").resize(this.setAutoHeight);
-                }
-
-                this.addAnnotationFilter();
-
-                return this;
-            },
-
-            /**
-             * addAnnotationFilter - Add the annotation filter to the view
-             */
-            addAnnotationFilter: function(){
-              if (MetacatUI.appModel.get("bioportalAPIKey")) {
-                var view = this;
-                var popoverTriggerSelector = "[data-category='annotation'] .expand-collapse-control";
-                if(!this.$el.find(popoverTriggerSelector)){
-                  return
-                }
-                var annotationFilter = new AnnotationFilter({
-                  popoverTriggerSelector: popoverTriggerSelector
-                });
-                this.$el
-                  .find(popoverTriggerSelector)
-                  .append(annotationFilter.el);
-                annotationFilter.render();
-                annotationFilter.off("annotationSelected");
-                annotationFilter.on("annotationSelected", function(event, item){
-                  $("#annotation_input").val(item.value);
-                  view.updateTextFilters(event, item)
-                });
-              }
-            },
-
-            // Linked Data Object for appending the jsonld into the browser DOM
-            getLinkedData: function() {
-                // Find the MN info from the CN Node list
-                var members = MetacatUI.nodeModel.get("members")
-                for (var i = 0; i < members.length; i++) {
-                    if (members[i].identifier == MetacatUI.nodeModel.get("currentMemberNode")) {
-                        var nodeModelObject = members[i];
-                    }
-                }
-
-                // JSON Linked Data Object
-                let elJSON = {
-                    "@context": {
-                        "@vocab": "http://schema.org/"
-                    },
-                    "@type": "DataCatalog",
-                };
-                if (nodeModelObject) {
-                    // "keywords": "",
-                    // "provider": "",
-                    let conditionalData = {
-                        "description": nodeModelObject.description,
-                        "identifier": nodeModelObject.identifier,
-                        "image": nodeModelObject.logo,
-                        "name": nodeModelObject.name,
-                        "url": nodeModelObject.url
-                    }
-                    $.extend(elJSON, conditionalData)
-                }
-
-
-                // Check if the jsonld already exists from the previous data view
-                // If not create a new script tag and append otherwise replace the text for the script
-                if (!document.getElementById("jsonld")) {
-                    var el = document.createElement("script");
-                    el.type = "application/ld+json";
-                    el.id = "jsonld";
-                    el.text = JSON.stringify(elJSON);
-                    document.querySelector("head").appendChild(el);
-                } else {
-                    var script = document.getElementById("jsonld");
-                    script.text = JSON.stringify(elJSON);
-                }
-                return;
-            },
-
-            /*
-             * Sets the height on elements in the main content area to fill up the entire area minus header and footer
-             */
-            setAutoHeight: function() {
-                // If we are in list mode, don't determine the height of any elements because we are not "full screen"
-                if (MetacatUI.appModel.get("searchMode") == "list" || this.fixedHeight) {
-                    MetacatUI.appView.$(".auto-height").height("auto");
-                    return;
-                }
-
-                // Get the heights of the header, navbar, and footer
-                var otherHeight = 0;
-                $(".auto-height-member").each(function(i, el) {
-                    if ($(el).css("display") != "none") {
-                        otherHeight += $(el).outerHeight(true);
-                    }
-                });
-
-                // Get the remaining height left based on the window size
-                var remainingHeight = $(window).outerHeight(true) - otherHeight;
-                if (remainingHeight < 0) remainingHeight = $(window).outerHeight(true) || 300;
-                else if (remainingHeight <= 120) remainingHeight = ($(window).outerHeight(true) - remainingHeight) || 300;
-
-                // Adjust all elements with the .auto-height class
-                $(".auto-height").height(remainingHeight);
-
-                if (($("#map-container.auto-height").length > 0) && ($("#map-canvas").length > 0)) {
-                    var otherHeight = 0;
-                    $("#map-container.auto-height").children().each(function(i, el) {
-                        if ($(el).attr("id") != "map-canvas") {
-                            otherHeight += $(el).outerHeight(true);
-                        }
-                    });
-                    var newMapHeight = remainingHeight - otherHeight;
-                    if (newMapHeight > 100) {
-                        $("#map-canvas").height(remainingHeight - otherHeight);
-                    }
-                }
-
-                // Trigger a resize for the map so that all of the map background images are loaded
-                if (gmaps && this.mapModel && this.mapModel.get("map")) {
-                    google.maps.event.trigger(this.mapModel.get("map"), "resize");
-                }
-            },
-
-            /*
-             * ==================================================================================================
-             *                                         PERFORMING SEARCH
-             * ==================================================================================================
-             */
-            triggerSearch: function() {
-
-                // Set the sort order
-                var sortOrder = $("#sortOrder").val();
-                if (sortOrder) {
-                    this.searchModel.set("sortOrder", sortOrder);
-                }
-
-                // Trigger a search to load the results
-                MetacatUI.appModel.trigger("search");
-
-                if (!this.isSubView) {
-                    // make sure the browser knows where we are
-                    var route = Backbone.history.fragment;
-                    if (route.indexOf("data") < 0) {
-                        MetacatUI.uiRouter.navigate("data", { trigger: false, replace: true });
-                    } else {
-                        MetacatUI.uiRouter.navigate(route);
-                    }
-                }
-
-                // ...but don't want to follow links
-                return false;
-            },
-
-            triggerOnEnter: function(e) {
-                if (e.keyCode != 13) return;
-
-                // Update the filters
-                this.updateTextFilters(e);
-            },
-
-
-            /**
-             * getResults gets all the current search filters from the searchModel, creates a Solr query, and runs that query.
-             * @param {number} page - The page of search results to get results for
-             */
-            getResults: function(page) {
-
-                // Set the sort order based on user choice
-                var sortOrder = this.searchModel.get("sortOrder");
-                if (sortOrder) {
-                    this.searchResults.setSort(sortOrder);
-                }
-
-                // Specify which fields to retrieve
-                var fields = "";
-                    fields += "id,";
-                    fields += "seriesId,";
-                    fields += "title,";
-                    fields += "origin,";
-                    fields += "pubDate,";
-                    fields += "dateUploaded,";
-                    fields += "abstract,";
-                    fields += "resourceMap,";
-                    fields += "beginDate,";
-                    fields += "endDate,";
-                    fields += "read_count_i,";
-                    fields += "geohash_9,";
-                    fields += "datasource,";
-                    fields += "isPublic,";
-                    fields += "documents,";
-                    fields += "sem_annotation,";
-                // Add spatial fields if the map is present
-                if ( gmaps ) {
-                    fields += "northBoundCoord,";
-                    fields += "southBoundCoord,";
-                    fields += "eastBoundCoord,";
-                    fields += "westBoundCoord";
-                }
-                // Strip the last trailing comma if needed
-                if ( fields[fields.length - 1] === "," ) {
-                    fields = fields.substr(0, fields.length - 1);
-                }
-                this.searchResults.setfields(fields);
-
-                // Get the query
-                var query = this.searchModel.getQuery();
-
-                // Specify which facets to retrieve
-                if (gmaps && this.map) { // If we have Google Maps enabled
-                    var geohashLevel = "geohash_" +
-                        this.mapModel.determineGeohashLevel(this.map.zoom);
-                    this.searchResults.facet.push(geohashLevel);
-                }
-
-                // Run the query
-                this.searchResults.setQuery(query);
-
-                // Get the page number
-                if (this.isSubView) {
-                    var page = 0;
-                } else {
-                    var page = MetacatUI.appModel.get("page");
-                    if (page == null) {
-                        page = 0;
-                    }
-                }
-                this.searchResults.start = page * this.searchResults.rows;
-
-                // Show or hide the reset filters button
-                this.toggleClearButton();
-
-                // go to the page
-                this.showPage(page);
-
-                // don't want to follow links
-                return false;
-            },
-
-            /*
-             * After the search results have been returned,
-             * check if any of them are derived data or have derivations
-             */
-            checkForProv: function() {
-
-                var maps = [],
-                    hasSources = [],
-                    hasDerivations = [],
-                    mainSearchResults = this.searchResults;
-
-                // Get a list of all the resource map IDs from the SolrResults collection
-                maps = this.searchResults.pluck("resourceMap");
-                maps = _.compact(_.flatten(maps));
-
-                // Create a new Search model with a search that finds all members of these packages/resource maps
-                var provSearchModel = new SearchModel({
-                    formatType: [{
-                        value: "DATA",
-                        label: "data",
-                        description: null
-                    }],
-                    exclude: [],
-                    resourceMap: maps
-                });
-
-                // Create a new Solr Results model to store the results of this supplemental query
-                var provSearchResults = new SearchResults(null, {
-                    query: provSearchModel.getQuery(),
-                    searchLogs: false,
-                    usePOST: true,
-                    rows: 150,
-                    fields: provSearchModel.getProvFlList() + ",id,resourceMap"
-                });
-
-                // Trigger a search on that Solr Results model
-                this.listenTo(provSearchResults, "reset", function(results) {
-                    if (results.models.length == 0) return;
-
-                    // See if any of the results have a value for a prov field
-                    results.forEach(function(result) {
-                        if ((!result.getSources().length) || (!result.getDerivations())) return;
-                        _.each(result.get("resourceMap"), function(rMapID) {
-                            if (_.contains(maps, rMapID)) {
-                                var match = mainSearchResults.filter(function(mainSearchResult) {
-                                    return _.contains(mainSearchResult.get("resourceMap"), rMapID)
-                                });
-                                if (match && match.length && (result.getSources().length > 0))
-                                  hasSources.push(match[0].get("id"));
-                                if (match && match.length && (result.getDerivations().length > 0))
-                                  hasDerivations.push(match[0].get("id"));
-                            }
-                        });
-                    });
-
-                    // Filter out the duplicates
-                    hasSources = _.uniq(hasSources);
-                    hasDerivations = _.uniq(hasDerivations);
-
-                    // If they do, find their corresponding result row here and add
-                    // the prov icon (or just change the class to active)
-                    _.each(hasSources, function(metadataID) {
-                        var metadataDoc = mainSearchResults.findWhere({
-                            id: metadataID
-                        });
-                        if (metadataDoc) {
-                            metadataDoc.set("prov_hasSources", true);
-                        }
-                    });
-                    _.each(hasDerivations, function(metadataID) {
-                        var metadataDoc = mainSearchResults.findWhere({
-                            id: metadataID
-                        });
-                        if (metadataDoc) {
-                            metadataDoc.set("prov_hasDerivations", true);
-                        }
-                    });
-                });
-                provSearchResults.toPage(0);
-            },
-
-            cacheSearch: function() {
-                MetacatUI.appModel.get("searchHistory").push({
-                    search: this.searchModel.clone(),
-                    map: this.mapModel ? this.mapModel.clone() : null
-                });
-                MetacatUI.appModel.trigger("change:searchHistory");
-            },
-
-            /*
-             * ==================================================================================================
-             *                                             FILTERS
-             * ==================================================================================================
-             */
-            updateCheckboxFilter: function(e, category, value) {
-                if (!this.filters) return;
-
-                var checkbox = e.target;
-                var checked = $(checkbox).prop("checked");
-
-                if (typeof category == "undefined") var category = $(checkbox).attr("data-category");
-                if (typeof value == "undefined") var value = $(checkbox).attr("value");
-
-                // If the user just unchecked the box, then remove this filter
-                if (!checked) {
-                    this.searchModel.removeFromModel(category, value);
-                    this.hideFilter(category, value);
-                }
-                // If the user just checked the box, then add this filter
-                else {
-                    var currentValue = this.searchModel.get(category);
-
-                    // Get the description
-                    var desc = $(checkbox).attr("data-description") || $(checkbox).attr("title");
-                    if (typeof desc == "undefined" || !desc) desc = "";
-                    // Get the label
-                    var labl = $(checkbox).attr("data-label");
-                    if (typeof labl == "undefined" || !labl) labl = "";
-
-                    // Make the filter object
-                    var filter = {
-                        description: desc,
-                        label: labl,
-                        value: value
-                    }
-
-                    // If this filter category is an array, add this value to the array
-                    if (Array.isArray(currentValue)) {
-                        currentValue.push(filter);
-                        this.searchModel.set(category, currentValue);
-                        this.searchModel.trigger("change:" + category);
-                    } else {
-                        // If it isn't an array, then just update the model with a simple value
-                        this.searchModel.set(category, filter);
-                    }
-
-                    // Show the filter element
-                    this.showFilter(category, value, true, labl);
-
-                    // Show the reset button
-                    this.showClearButton();
-                }
-
-                // Route to page 1
-                this.updatePageNumber(0);
-
-                // Trigger a new search
-                this.triggerSearch();
-            },
-
-            updateBooleanFilters: function(e) {
-                if (!this.filters) return;
-
-                // Get the category
-                var checkbox = e.target;
-                var category = $(checkbox).attr("data-category");
-                var currentValue = this.searchModel.get(category);
-
-                // If this filter is not enabled, exit this function
-                if ( !_.contains(MetacatUI.appModel.get("defaultSearchFilters"), category) ){
-                  return false;
-                }
-
-                //The year filter is handled in a different way
-                if ((category == "pubYear") || (category == "dataYear")) return;
-
-                // If the checkbox has a value, then update as a string value not boolean
-                var value = $(checkbox).attr("value");
-                if (value) {
-                    this.updateCheckboxFilter(e, category, value);
-                    return;
-                } else value = $(checkbox).prop("checked");
-
-                this.searchModel.set(category, value);
-
-                // Add the filter to the UI
-                if (value) {
-                    this.showFilter(category, "", true);
-                } else {
-                // Remove the filter from the UI
-                    value = "";
-                    this.hideFilter(category, value);
-                }
-
-                // Show the reset button
-                this.showClearButton();
-
-                // Route to page 1
-                this.updatePageNumber(0);
-
-                // Trigger a new search
-                this.triggerSearch();
-
-                // Track this event
-                MetacatUI.analytics?.trackEvent("search", "filter, " + category, value)
-            },
-
-            // Update the UI year slider and input values
-            // Also update the model
-            updateYearRange: function(e) {
-                if (!this.filters) return;
-
-                var viewRef = this,
-                    userAction = !(typeof e === "undefined"),
-                    model = this.searchModel,
-                    pubYearChecked = $("#publish_year").prop("checked"),
-                    dataYearChecked = $("#data_year").prop("checked");
-
-
-                // If the year range slider has not been created yet
-                if (!userAction && !$("#year-range").hasClass("ui-slider")) {
-
-                    var defaultMin = typeof this.searchModel.defaults == "function" ? this.searchModel.defaults().yearMin : 1800,
-                        defaultMax = typeof this.searchModel.defaults == "function" ? this.searchModel.defaults().yearMax : (new Date()).getUTCFullYear();
-
-                    //jQueryUI slider
-                    $("#year-range").slider({
-                        range: true,
-                        disabled: false,
-                        min: defaultMin, //sets the minimum on the UI slider on initialization
-                        max: defaultMax, //sets the maximum on the UI slider on initialization
-                        values: [this.searchModel.get("yearMin"), this.searchModel.get("yearMax")], //where the left and right slider handles are
-                        stop: function(event, ui) {
-
-                            // When the slider is changed, update the input values
-                            $("#min_year").val(ui.values[0]);
-                            $("#max_year").val(ui.values[1]);
-
-                            // Also update the search model
-                            model.set("yearMin", ui.values[0]);
-                            model.set("yearMax", ui.values[1]);
-
-                            // If neither the publish year or data coverage year are checked
-                            if (!$("#publish_year").prop("checked") && !$("#data_year").prop("checked")) {
-
-                                // We want to check the data coverage year on the user's behalf
-                                $("#data_year").prop("checked", "true");
-
-                                // And update the search model
-                                model.set("dataYear", true);
-                            }
-
-                            // Add the filter elements
-                            if ($("#publish_year").prop("checked")) {
-                                viewRef.showFilter($("#publish_year").attr("data-category"), true, false, ui.values[0] + " to " + ui.values[1], {
-                                    replace: true
-                                });
-                            }
-                            if ($("#data_year").prop("checked")) {
-                                viewRef.showFilter($("#data_year").attr("data-category"), true, false, ui.values[0] + " to " + ui.values[1], {
-                                    replace: true
-                                });
-                            }
-
-                            // Route to page 1
-                            viewRef.updatePageNumber(0);
-
-                            // Trigger a new search
-                            viewRef.triggerSearch();
-                        }
-                    });
-
-                    // Get the minimum and maximum years of this current search and use those as the min and max values in the slider
-                    this.statsModel.set("query", this.searchModel.getQuery());
-                    this.listenTo(this.statsModel, "change:firstBeginDate", function() {
-                        if (this.statsModel.get("firstBeginDate") == 0 || !this.statsModel.get("firstBeginDate")) {
-                            $("#year-range").slider({
-                                min: defaultMin
-                            });
-                            return;
-                        }
-                        var year = new Date(this.statsModel.get("firstBeginDate")).getUTCFullYear();
-                        if (typeof year !== "undefined") {
-                            $("#min_year").val(year);
-                            $("#year-range").slider({
-                                values: [year, $("#max_year").val()]
-                            });
-
-                            // If the slider min is still at the default value, then update with the min value found at this search
-                            if ($("#year-range").slider("option", "min") == defaultMin) {
-                                $("#year-range").slider({
-                                    min: year
-                                });
-                            }
-
-                            // Add the filter elements if this is set
-                            if (viewRef.searchModel.get("pubYear")) {
-                                viewRef.showFilter("pubYear", true, false, $("#min_year").val() + " to " + $("#max_year").val(), {
-                                    replace: true
-                                });
-                            }
-                            if (viewRef.searchModel.get("dataYear")) {
-                                viewRef.showFilter("dataYear", true, false, $("#min_year").val() + " to " + $("#max_year").val(), {
-                                    replace: true
-                                });
-                            }
-                        }
-                    });
-                    // Only when the first begin date is retrieved, set the slider min and max values
-                    this.listenTo(this.statsModel, "change:lastEndDate", function() {
-                        if (this.statsModel.get("lastEndDate") == 0 || !this.statsModel.get("lastEndDate")) {
-                            $("#year-range").slider({
-                                max: defaultMax
-                            });
-                            return;
-                        }
-                        var year = new Date(this.statsModel.get("lastEndDate")).getUTCFullYear();
-                        if (typeof year !== "undefined") {
-                            $("#max_year").val(year);
-                            $("#year-range").slider({
-                                values: [$("#min_year").val(), year]
-                            });
-
-                            // If the slider max is still at the default value, then update with the max value found at this search
-                            if ($("#year-range").slider("option", "max") == defaultMax) {
-                                $("#year-range").slider({
-                                    max: year
-                                });
-                            }
-
-                            // Add the filter elements if this is set
-                            if (viewRef.searchModel.get("pubYear")) {
-                                viewRef.showFilter("pubYear", true, false, $("#min_year").val() + " to " + $("#max_year").val(), {
-                                    replace: true
-                                });
-                            }
-                            if (viewRef.searchModel.get("dataYear")) {
-                                viewRef.showFilter("dataYear", true, false, $("#min_year").val() + " to " + $("#max_year").val(), {
-                                    replace: true
-                                });
-                            }
-                        }
-                    });
-                    this.statsModel.getFirstBeginDate();
-                    this.statsModel.getLastEndDate();
-                }
-                // If the year slider has been created and the user initiated a new search using other filters
-                else if (!userAction && (!this.searchModel.get("dataYear")) && (!this.searchModel.get("pubYear"))) {
-                    // Reset the min and max year based on this search
-                    this.statsModel.set("query", this.searchModel.getQuery());
-                    this.statsModel.getFirstBeginDate();
-                    this.statsModel.getLastEndDate();
-                }
-                // If either of the year type selectors is what brought us here, then determine whether the user
-                // is completely removing both (reset both year filters) or just one (remove just that one filter)
-                else if (userAction) {
-                    // When both year types were unchecked, assume user wants to reset the year filter
-                    if ((($(e.target).attr("id") == "data_year") || ($(e.target).attr("id") == "publish_year")) && (!pubYearChecked && !dataYearChecked)) {
-                        // Reset the search model
-                        this.searchModel.set("yearMin", defaultMin);
-                        this.searchModel.set("yearMax", defaultMax);
-                        this.searchModel.set("dataYear", false);
-                        this.searchModel.set("pubYear", false);
-
-                        // Reset the min and max year based on this search
-                        this.statsModel.set("query", this.searchModel.getQuery());
-                        this.statsModel.getFirstBeginDate();
-                        this.statsModel.getLastEndDate();
-
-                        // Slide the handles back to the defaults
-                        $("#year-range").slider("values", [defaultMin, defaultMax]);
-
-                        // Hide the filters
-                        this.hideFilter("dataYear");
-                        this.hideFilter("pubYear");
-                    }
-                    // If either of the year inputs have changed or if just one of the year types were unchecked
-                    else {
-                        var minVal = $("#min_year").val();
-                        var maxVal = $("#max_year").val();
-
-                        // Update the search model to match what is in the text inputs
-                        this.searchModel.set("yearMin", minVal);
-                        this.searchModel.set("yearMax", maxVal);
-                        this.searchModel.set("dataYear", dataYearChecked);
-                        this.searchModel.set("pubYear", pubYearChecked);
-
-                        // If neither the publish year or data coverage year are checked
-                        if (!pubYearChecked && !dataYearChecked) {
-
-                            // We want to check the data coverage year on the user's behalf
-                            $("#data_year").prop("checked", "true");
-
-                            // And update the search model
-                            model.set("dataYear", true);
-
-                            // Add the filter elements
-                            this.showFilter($("#data_year").attr("data-category"), true, true, minVal + " to " + maxVal, {
-                                replace: true
-                            });
-
-                            // Track this event
-                            MetacatUI.analytics?.trackEvent("search", "filter, Data Year", minVal + " to " + maxVal);
-
-                        } else {
-                            // Add the filter elements
-                            if (pubYearChecked) {
-                                this.showFilter($("#publish_year").attr("data-category"), true, true, minVal + " to " + maxVal, {
-                                    replace: true
-                                });
-
-                                // Track this event
-                                MetacatUI.analytics?.trackEvent("search", "filter, Publication Year", minVal + " to " + maxVal);
-
-                            } else {
-                                this.hideFilter($("#publish_year").attr("data-category"), true);
-                            }
-
-                            if (dataYearChecked) {
-                                this.showFilter($("#data_year").attr("data-category"), true, true, minVal + " to " + maxVal, {
-                                    replace: true
-                                });
-
-                                // Track this event
-                                MetacatUI.analytics?.trackEvent("search", "filter, Data Year", minVal + " to " + maxVal);
-
-                            } else {
-                                this.hideFilter($("#data_year").attr("data-category"), true);
-                            }
-                        }
-                    }
-
-                    // Route to page 1
-                    this.updatePageNumber(0);
-
-                    // Trigger a new search
-                    this.triggerSearch();
-                }
-            },
-
-            updateTextFilters: function(e, item) {
-                if (!this.filters) return;
-
-                // Get the search/filter category
-                var category = $(e.target).attr("data-category");
-
-                // Try the parent elements if not found
-                if (!category) {
-                    var parents = $(e.target).parents().each(function() {
-                        category = $(this).attr("data-category");
-                        if (category) {
-                            return false;
-                        }
-                    });
-                }
-
-                if (!category) {
-                    return false;
-                }
-
-                // Get the input element
-                var input = this.$el.find("#" + category + "_input");
-
-                // Get the value of the associated input
-                var term = (!item || !item.value) ? input.val() : item.value;
-                var label = (!item || !item.filterLabel) ? null : item.filterLabel;
-                var filterDesc = (!item || !item.desc) ? null : item.desc;
-
-                // Check that something was actually entered
-                if ((term == "") || (term == " ")) {
-                    return false;
-                }
-
-                // Close the autocomplete box
-                if (e.type == "hoverautocompleteselect") {
-                    $(input).hoverAutocomplete("close");
-                } else if ($(input).data("ui-autocomplete") != undefined) {
-                    // If the autocomplete has been initialized, then close it
-                    $(input).autocomplete("close");
-                }
-
-                // Get the current searchModel array for this category
-                var filtersArray = _.clone(this.searchModel.get(category));
-
-                if (typeof filtersArray == "undefined") {
-                    console.error("The filter category '" + category + "' does not exist in the Search model. Not sending this search term.");
-                    return false;
-                }
-
-                // Check if this entry is a duplicate
-                var duplicate = (function() {
-                    for (var i = 0; i < filtersArray.length; i++) {
-                        if (filtersArray[i].value === term) {
-                            return true;
-                        }
-                    }
-                })();
-
-                if (duplicate) {
-                    // Display a quick message
-                    if ($("#duplicate-" + category + "-alert").length <= 0) {
-                        $("#current-" + category + "-filters").prepend(
-                            "<div class='alert alert-block' id='duplicate-' + category + '-alert'>" +
-                            "You are already using that filter" +
-                            "</div>"
-                        );
-
-                        $("#duplicate-" + category + "-alert").delay(2000).fadeOut(500, function() {
-                            this.remove();
-                        });
-                    }
-
-                    return false;
-                }
-
-                // Add the new entry to the array of current filters
-                var filter = {
-                    value: term,
-                    filterLabel: label,
-                    label: label,
-                    description: filterDesc
-                };
-                filtersArray.push(filter);
-
-                // Replace the current array with the new one in the search model
-                this.searchModel.set(category, filtersArray);
-
-                // Show the UI filter
-                this.showFilter(category, filter, false, label);
-
-                // Clear the input
-                input.val("");
-
-                // Route to page 1
-                this.updatePageNumber(0);
-
-                // Trigger a new search
-                this.triggerSearch();
-
-                // Track this event
-                MetacatUI.analytics?.trackEvent("search", "filter, " + category, term);
-
-            },
-
-            // Removes a specific filter term from the searchModel
-            removeFilter: function(e) {
-                // Get the parent element that stores the filter term
-                var filterNode = $(e.target).parent();
-
-                // Find this filter's category and value
-                var category = filterNode.attr("data-category") || filterNode.parent().attr("data-category"),
-                    value = $(filterNode).attr("data-term");
-
-                // Remove this filter from the searchModel
-                this.searchModel.removeFromModel(category, value);
-
-                // Hide the filter from the UI
-                this.hideFilter(category, value);
-
-                // If there is an associated checkbox with this filter, uncheck it
-                var assocCheckbox,
-                    checkboxes = this.$("input[type='checkbox'][data-category='" + category + "']");
-
-                //If there are more than one checkboxes in this category, match by value
-                if (checkboxes.length > 1) {
-                    assocCheckbox = _.find(checkboxes, function(checkbox){
-                      return $(checkbox).val() == value;
-                    });
-                }
-                //If there is only one checkbox in this category, default to it
-                else if( checkboxes.length == 1 ){
-                  assocCheckbox = checkboxes[0];
-                }
-
-                //If there is an associated checkbox, uncheck it
-                if (assocCheckbox) {
-                  //Uncheck it
-                  $(assocCheckbox).prop("checked", false);
-                }
-
-                // Route to page 1
-                this.updatePageNumber(0);
-
-                // Trigger a new search
-                this.triggerSearch();
-
-            },
-
-            // Clear all the currently applied filters
-            resetFilters: function() {
-                var viewRef = this;
-
-                this.allowSearch = true;
-
-                // Hide all the filters in the UI
-                $.each(this.$(".current-filter"), function() {
-                    viewRef.hideEl(this);
-                });
-
-                // Hide the clear button
-                this.hideClearButton();
-
-                // Then reset the model
-                this.searchModel.clear();
-
-                //Reset the map model
-                if(this.mapModel){
-                  this.mapModel.clear();
-                }
-
-                // Reset the year slider handles
-                $("#year-range").slider("values", [this.searchModel.get("yearMin"), this.searchModel.get("yearMax")])
-                //and the year inputs
-                $("#min_year").val(this.searchModel.get("yearMin"));
-                $("#max_year").val(this.searchModel.get("yearMax"));
-
-                // Reset the checkboxes
-                $("#includes_data").prop("checked", this.searchModel.get("documents"));
-                $("#data_year").prop("checked", this.searchModel.get("dataYear"));
-                $("#publish_year").prop("checked", this.searchModel.get("pubYear"));
-                $("#is_private_data").prop("checked", this.searchModel.get("isPrivate"));
-                this.listDataSources();
-
-                // Zoom out the Google Map
-                this.resetMap();
-                this.renderMap();
-
-                // Route to page 1
-                this.updatePageNumber(0);
-
-                // Trigger a new search
-                this.triggerSearch();
-            },
-
-            hideEl: function(element) {
-                // Fade out and remove the element
-                $(element).fadeOut("slow", function() {
-                    $(element).remove();
-                });
-            },
-
-            // Removes a specified filter node from the DOM
-            hideFilter: function(category, value) {
-                if (!this.filters) return;
-
-                if (typeof value === "undefined") {
-                    var filterNode = this.$(".current-filters[data-category='" +
-                        category + "']").children(".current-filter");
-                } else {
-                    var filterNode = this.$(".current-filters[data-category='" +
-                        category + "']").children("[data-term='" + value + "']");
-                }
-
-                // Try finding it a different way
-                if (!filterNode || !filterNode.length) {
-                    filterNode = this.$(".current-filter[data-category='" + category + "']");
-                }
-
-                // Remove the filter node from the DOM
-                this.hideEl(filterNode);
-            },
-
-            // Adds a specified filter node to the DOM
-            showFilter: function(category, term, checkForDuplicates, label, options) {
-                if (!this.filters) return;
-
-                var viewRef = this;
-
-                if (typeof term === "undefined") return false;
-
-                // Get the element to add the UI filter node to
-                // The pattern is #current-<category>-filters
-                var filterContainer = this.$el.find("#current-" + category + "-filters");
-
-                // Allow the option to only display this exact filter category and term once to the DOM
-                // Helpful when adding a filter that is not stored in the search model (for display only)
-                if (checkForDuplicates) {
-                    var duplicate = false;
-
-                    // Get the current terms from the DOM and check against the new term
-                    filterContainer.children().each(function() {
-                        if ($(this).attr("data-term") == term) {
-                            duplicate = true;
-                        }
-                    });
-
-                    // If there is a duplicate, exit without adding it
-                    if (duplicate) {
-                        return;
-                    }
-                }
-
-                var value = null,
-                    desc = null;
-
-                // See if this filter is an object and extract the filter attributes
-                if (typeof term === "object") {
-                    if (typeof term.description !== "undefined") {
-                        desc = term.description;
-                    }
-                    if (typeof term.filterLabel !== "undefined") {
-                        label = term.filterLabel;
-                    } else if ((typeof term.label !== "undefined") && (term.label)) {
-                        label = term.label;
-                    } else {
-                        label = null;
-                    }
-                    if (typeof term.value !== "undefined") {
-                        value = term.value;
-                    }
-                } else {
-                    value = term;
-
-                    // Find the filter label
-                    if ((typeof label === "undefined") || !label) {
-
-                        // Use the filter value for the label, sans any leading # character
-                        if (value.indexOf("#") > 0) {
-                            label = value.substring(value.indexOf("#"));
-                        }
-                    }
-
-                    desc = label;
-                }
-
-                var categoryLabel = this.searchModel.fieldLabels[category];
-                if ((typeof categoryLabel === "undefined") && (category == "additionalCriteria")) categoryLabel = "";
-                if (typeof categoryLabel === "undefined") categoryLabel = category;
-
-                // Add a filter node to the DOM
-                var filterEl = viewRef.currentFilterTemplate({
-                    category: Utilities.encodeHTML(categoryLabel),
-                    value: Utilities.encodeHTML(value),
-                    label: Utilities.encodeHTML(label),
-                    description: Utilities.encodeHTML(desc)
-                });
-
-                // Add the filter to the page - either replace or tack on
-                if (options && options.replace) {
-                    var currentFilter = filterContainer.find(".current-filter");
-                    if (currentFilter.length > 0) {
-                        currentFilter.replaceWith(filterEl);
-                    } else {
-                        filterContainer.prepend(filterEl);
-                    }
-                } else {
-                    filterContainer.prepend(filterEl);
-                }
-
-                // Tooltips and Popovers
-                $(filterEl).tooltip({
-                    delay: {
-                        show: 800
-                    }
-                });
-
-                return;
-            },
-
-            /*
-             * Get the member node list from the model and list the members in the filter list
-             */
-            listDataSources: function() {
-                if (!this.filters) return;
-
-                if (MetacatUI.nodeModel.get("members").length < 1) return;
-
-                // Get the member nodes
-                var members = _.sortBy(MetacatUI.nodeModel.get("members"), function(m) {
-                    if (m.name) {
-                        return m.name.toLowerCase();
-                    } else {
-                        return "";
-                    }
-                });
-                var filteredMembers = _.reject(members, function(m) {
-                    return m.status != "operational"
-                });
-
-                // Get the current search filters for data source
-                var currentFilters = this.searchModel.get("dataSource");
-
-                // Create an HTML list
-                var listMax = 4,
-                    numHidden = filteredMembers.length - listMax,
-                    list = $(document.createElement("ul")).addClass("checkbox-list");
-
-                // Add a checkbox and label for each member node in the node model
-                _.each(filteredMembers, function(member, i) {
-                    var listItem = document.createElement("li"),
-                        input = document.createElement("input"),
-                        label = document.createElement("label");
-
-                    // If this member node is already a data source filter, then the checkbox is checked
-                    var checked = _.findWhere(currentFilters, {
-                        value: member.identifier
-                    }) ? true : false;
-
-                    // Create a textual label for this data source
-                    $(label).addClass("ellipsis")
-                        .attr("for", member.identifier)
-                        .html(member.name);
-
-                    // Create a checkbox for this data source
-                    $(input).addClass("filter")
-                        .attr("type", "checkbox")
-                        .attr("data-category", "dataSource")
-                        .attr("id", member.identifier)
-                        .attr("name", "dataSource")
-                        .attr("value", member.identifier)
-                        .attr("data-label", member.name)
-                        .attr("data-description", member.description);
-
-                    // Add tooltips to the label element
-                    $(label).tooltip({
-                        placement: "top",
-                        delay: {
-                            "show": 900
-                        },
-                        trigger: "hover",
-                        viewport: "#sidebar",
-                        title: member.description
-                    });
-
-                    // If this data source is already selected as a filter (from the search model), then check the checkbox
-                    if (checked) $(input).prop("checked", "checked");
-
-                    // Collapse some of the checkboxes and labels after a certain amount
-                    if (i > (listMax - 1)) {
-                        $(listItem).addClass("hidden");
-                    }
-
-                    // Insert a "More" link after a certain amount to enable users to expand the list
-                    if (i == listMax) {
-                        var moreLink = document.createElement("a");
-                        $(moreLink).html("Show " + numHidden + " more")
-                            .addClass("more-link pointer toggle-list")
-                            .append($(document.createElement("i")).addClass("icon icon-expand-alt"));
-                        $(list).append(moreLink);
-                    }
-
-                    // Add this checkbox and laebl to the list
-                    $(listItem).append(input).append(label);
-                    $(list).append(listItem);
-                });
-
-                if (numHidden > 0) {
-                    var lessLink = document.createElement("a");
-                    $(lessLink).html("Collapse member nodes")
-                        .addClass("less-link toggle-list pointer hidden")
-                        .append($(document.createElement("i")).addClass("icon icon-collapse-alt"));
-
-                    $(list).append(lessLink);
-                }
-
-                // Add the list of checkboxes to the placeholder
-                var container = $(".member-nodes-placeholder");
-                $(container).html(list);
-                $(".tooltip-this").tooltip();
-            },
-
-            resetDataSourceList: function() {
-                if (!this.filters) return;
-
-                // Reset the Member Nodes checkboxes
-                var mnFilterContainer = $("#member-nodes-container"),
-                    defaultMNs = this.searchModel.get("dataSource");
-
-                // Make sure the member node filter exists
-                if (!mnFilterContainer || mnFilterContainer.length == 0) return false;
-                if ((typeof defaultMNs === "undefined") || !defaultMNs) return false;
-
-                // Reset each member node checkbox
-                var boxes = $(mnFilterContainer).find(".filter").prop("checked", false);
-
-                // Check the member node checkboxes that are defaults in the search model
-                _.each(defaultMNs, function(member, i) {
-                    var value = null;
-
-                    // Allow for string search model filter values and object filter values
-                    if ((typeof member !== "object") && member) value = member;
-                    else if ((typeof member.value === "undefined") || !member.value) value = "";
-                    else value = member.value;
-
-                    $(mnFilterContainer).find("checkbox[value='" + value + "']").prop("checked", true);
-                });
-
-                return true;
-            },
-
-            toggleList: function(e) {
-                if (!this.filters) return;
-
-                var link = e.target,
-                    controls = $(link).parents("ul").find(".toggle-list"),
-                    list = $(link).parents("ul"),
-                    isHidden = !(list.find(".more-link").is(".hidden"));
-
-                // Hide/Show the list
-                if (isHidden) {
-                    list.children("li").slideDown();
-                } else {
-                    list.children("li.hidden").slideUp();
-                }
-
-                // Hide/Show the control links
-                controls.toggleClass("hidden");
-            },
-
-
-            // add additional criteria to the search model based on link click
-            additionalCriteria: function(e) {
-                // Get the clicked node
-                var targetNode = $(e.target);
-
-                // If this additional criteria is already applied, remove it
-                if (targetNode.hasClass("active")) {
-                    this.removeAdditionalCriteria(e);
-                    return false;
-                }
-
-                // Get the filter criteria
-                var term = targetNode.attr("data-term");
-
-                // Find this element's category in the data-category attribute
-                var category = targetNode.attr("data-category");
-
-                // style the selection
-                $(".keyword-search-link").removeClass("active");
-                $(".keyword-search-link").parent().removeClass("active");
-                targetNode.addClass("active");
-                targetNode.parent().addClass("active");
-
-                // Add this criteria to the search model
-                this.searchModel.set(category, [term]);
-
-                // Trigger the search
-                this.triggerSearch();
-
-                // prevent default action of click
-                return false;
-
-            },
-
-            removeAdditionalCriteria: function(e) {
-
-                // Get the clicked node
-                var targetNode = $(e.target);
-
-                // Reference to model
-                var model = this.searchModel;
-
-                // remove the styling
-                $(".keyword-search-link").removeClass("active");
-                $(".keyword-search-link").parent().removeClass("active");
-
-                // Get the term
-                var term = targetNode.attr("data-term");
-
-                // Get the current search model additional criteria
-                var current = this.searchModel.get("additionalCriteria");
-                // If this term is in the current search model (should be)...
-                if (_.contains(current, term)) {
-                    //then remove it
-                    var newTerms = _.without(current, term);
-                    model.set("additionalCriteria", newTerms);
-                }
-
-                // Route to page 1
-                this.updatePageNumber(0);
-
-                // Trigger a new search
-                this.triggerSearch();
-            },
-
-            // Get the facet counts
-            getAutocompletes: function(e) {
-                if (!e) return;
-
-                // Get the text input to determine the filter type
-                var input = $(e.target),
-                    category = input.attr("data-category");
-
-                if (!this.filters || !category) return;
-
-                var viewRef = this;
-
-                // Create the facet query by using our current search query
-                var facetQuery = "q=" + this.searchResults.currentquery +
-                    "&rows=0" +
-                    this.searchModel.getFacetQuery(category) +
-                    "&wt=json&";
-
-                // If we've cached these filter results, then use the cache instead of sending a new request
-                if (!MetacatUI.appSearchModel.autocompleteCache) MetacatUI.appSearchModel.autocompleteCache = {};
-                else if (MetacatUI.appSearchModel.autocompleteCache[facetQuery]) {
-                    this.setupAutocomplete(input, MetacatUI.appSearchModel.autocompleteCache[facetQuery]);
-                    return;
-                }
-
-                // Get the facet counts for the autocomplete
-                var requestSettings = {
-                    url: MetacatUI.appModel.get("queryServiceUrl") + facetQuery,
-                    type: "GET",
-                    dataType: "json",
-                    success: function(data, textStatus, xhr) {
-
-                        var suggestions = [],
-                            facetLimit = 999;
-
-                        // Get all the facet counts
-                        _.each(category.split(","), function(c) {
-                            if (typeof c == "string") c = [c];
-                            _.each(c, function(thisCategory) {
-                                // Get the field name(s)
-                                var fieldNames = MetacatUI.appSearchModel.facetNameMap[thisCategory];
-                                if (typeof fieldNames == "string") fieldNames = [fieldNames];
-
-                                // Get the facet counts
-                                _.each(fieldNames, function(fieldName) {
-                                    suggestions.push(data.facet_counts.facet_fields[fieldName]);
-                                });
-                            });
-                        });
-                        suggestions = _.flatten(suggestions);
-
-                        // Format the suggestions
-                        var rankedSuggestions = new Array();
-                        for (var i = 0; i < Math.min(suggestions.length - 1, facetLimit); i += 2) {
-
-                                      //The label is the item value
-                                      var label = suggestions[i];
-
-                                      //For all categories except the 'all' category, display the facet count
-                                      if(category != "all"){
-                                        label += " (" + suggestions[i+1] + ")";
-                                      }
-
-                                      //Push the autocomplete item to the array
-                                      rankedSuggestions.push({
-                                        value: suggestions[i],
-                                        label: label
-                                      });
-                        }
-
-                        // Save these facets in the app so we don't have to send another query
-                        MetacatUI.appSearchModel.autocompleteCache[facetQuery] = rankedSuggestions;
-
-                        // Now setup the actual autocomplete menu
-                        viewRef.setupAutocomplete(input, rankedSuggestions);
-                    }
-                }
-                $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-            },
-
-            setupAutocomplete: function(input, rankedSuggestions) {
-                var viewRef = this;
-
-                //Override the _renderItem() function which renders a single autocomplete item.
-                // We want to use the 'title' HTML attribute on each item.
-                // This method must create a new <li> element, append it to the menu, and return it.
-                $.widget( "custom.autocomplete", $.ui.autocomplete, {
-                  _renderItem: function(ul, item) {
-                    return $( document.createElement("li") )
-                            .attr( "title", item.label )
-                            .append( item.label )
-                            .appendTo( ul );
-                  }
-                });
-                input.autocomplete({
-                    source: function(request, response) {
-                        var term = $.ui.autocomplete.escapeRegex(request.term),
-                            startsWithMatcher = new RegExp("^" + term, "i"),
-                            startsWith = $.grep(rankedSuggestions, function(value) {
-                                return startsWithMatcher.test(value.label || value.value || value);
-                            }),
-                            containsMatcher = new RegExp(term, "i"),
-                            contains = $.grep(rankedSuggestions, function(value) {
-                                return $.inArray(value, startsWith) < 0 &&
-                                    containsMatcher.test(value.label || value.value || value);
-                            });
-
-                        response(startsWith.concat(contains));
-                    },
-                    select: function(event, ui) {
-                        // set the text field
-                        input.val(ui.item.value);
-                        // add to the filter immediately
-                        viewRef.updateTextFilters(event, ui.item);
-                        // prevent default action
-                        return false;
-                    },
-                    position: {
-                        my: "left top",
-                        at: "left bottom",
-                        collision: "flipfit"
-                    }
-                });
-            },
-
-            hideClearButton: function() {
-                if (!this.filters) return;
-
-                // Hide the current filters panel
-                this.$(".current-filters-container").slideUp();
-
-                // Hide the reset button
-                $("#clear-all").addClass("hidden");
-                this.setAutoHeight();
-            },
-
-            showClearButton: function() {
-                if (!this.filters) return;
-
-                // Show the current filters panel
-                if (_.difference(this.searchModel.getCurrentFilters(), this.searchModel.spatialFilters).length > 0) {
-                    this.$(".current-filters-container").slideDown();
-                }
-
-                // Show the reset button
-                $("#clear-all").removeClass("hidden");
-                this.setAutoHeight();
-            },
-
-            /*
-             * ==================================================================================================
-             *                                             NAVIGATING THE UI
-             * ==================================================================================================
-             */
-            // Update all the statistics throughout the page
-            updateStats: function() {
-                if (this.searchResults.header != null) {
-                    this.$statcounts = this.$("#statcounts");
-                    this.$statcounts.html(
-                        this.statsTemplate({
-                            start: this.searchResults.header.get("start") + 1,
-                            end: this.searchResults.header.get("start") + this.searchResults.length,
-                            numFound: this.searchResults.header.get("numFound")
-                        })
-                    );
-                }
-
-                // piggy back here
-                this.updatePager();
-            },
-
-            updatePager: function() {
-                if (this.searchResults.header != null) {
-                    var pageCount = Math.ceil(this.searchResults.header.get("numFound") / this.searchResults.header.get("rows"));
-
-                    // If no results were found, display a message instead of the list and clear the pagination.
-                    if (pageCount == 0) {
-                        this.$results.html("<p id='no-results-found'>No results found.</p>");
-
-                        this.$("#resultspager").html("");
-                        this.$(".resultspager").html("");
-                    }
-                    // Do not display the pagination if there is only one page
-                    else if (pageCount == 1) {
-                        this.$("#resultspager").html("");
-                        this.$(".resultspager").html("");
-                    } else {
-                        var pages = new Array(pageCount);
-
-                        // mark current page correctly, avoid NaN
-                        var currentPage = -1;
-                        try {
-                            currentPage = Math.floor((this.searchResults.header.get("start") / this.searchResults.header.get("numFound")) * pageCount);
-                        } catch (ex) {
-                            console.log("Exception when calculating pages:" + ex.message);
-                        }
-
-                        // Populate the pagination element in the UI
-                        this.$(".resultspager").html(
-                            this.pagerTemplate({
-                                pages: pages,
-                                currentPage: currentPage
-                            })
-                        );
-                        this.$("#resultspager").html(
-                            this.pagerTemplate({
-                                pages: pages,
-                                currentPage: currentPage
-                            })
-                        );
-                    }
-                }
-            },
-
-            updatePageNumber: function(page) {
-                MetacatUI.appModel.set("page", page);
-
-                if (!this.isSubView) {
-                    var route = Backbone.history.fragment,
-                        subroutePos = route.indexOf("/page/"),
-                        newPage = parseInt(page) + 1;
-
-                    //replace the last number with the new one
-                    if ((page > 0) && (subroutePos > -1)) {
-                        route = route.replace(/\d+$/, newPage);
-                    } else if (page > 0) {
-                        route += "/page/" + newPage;
-                    } else if (subroutePos >= 0) {
-                        route = route.substring(0, subroutePos);
-                    }
-
-                    MetacatUI.uiRouter.navigate(route);
-                }
-            },
-
-            // Next page of results
-            nextpage: function() {
-                this.loading();
-                this.searchResults.nextpage();
-                this.$resultsview.show();
-                this.updateStats();
-
-                var page = MetacatUI.appModel.get("page");
-                page++;
-                this.updatePageNumber(page);
-            },
-
-            // Previous page of results
-            prevpage: function() {
-                this.loading();
-                this.searchResults.prevpage();
-                this.$resultsview.show();
-                this.updateStats();
-
-                var page = MetacatUI.appModel.get("page");
-                page--;
-                this.updatePageNumber(page);
-            },
-
-            navigateToPage: function(event) {
-                var page = $(event.target).attr("page");
-                this.showPage(page);
-            },
-
-            showPage: function(page) {
-                this.loading();
-                this.searchResults.toPage(page);
-                this.$resultsview.show();
-                this.updateStats();
-                this.updatePageNumber(page);
-                this.updateYearRange();
-            },
-
-            /*
-             * ==================================================================================================
-             *                                             THE MAP
-             * ==================================================================================================
-             */
-                renderMap: function () {
-
-                    // If gmaps isn't enabled or loaded with an error, use list mode
-                    if (!gmaps || this.mode == "list") {
-                        this.ready = true;
-                        this.mode = "list";
-                        return;
-                    }
-
-                    if (this.isSubView) {
-                        this.$el.addClass("mapMode");
-                    } else {
-                        $("body").addClass("mapMode");
-                    }
-
-                // Get the map options and create the map
-                gmaps.visualRefresh = true;
-                var mapOptions = this.mapModel.get("mapOptions");
-                var defaultZoom = mapOptions.zoom;
-                $("#map-container").append("<div id='map-canvas'></div>");
-                this.map = new gmaps.Map($("#map-canvas")[0], mapOptions);
-                this.mapModel.set("map", this.map);
-                this.hasZoomed  = false;
-                this.hasDragged = false;
-
-                // Hide the map filter toggle element
-                this.$(this.mapFilterToggle).hide();
-
-                // Store references
-                var mapRef = this.map;
-                var viewRef = this;
-
-                google.maps.event.addListener(mapRef, "zoom_changed", function() {
-                  // If the map is zoomed in further than the default zoom level,
-                  // than we want to mark the map as zoomed in
-                  if(viewRef.map.getZoom() > defaultZoom){
-                    viewRef.hasZoomed = true;
-                  }
-                  //If we are at the default zoom level or higher, than do not mark the map
-                  // as zoomed in
-                  else{
-                    viewRef.hasZoomed = false;
-                  }
-                });
-
-                google.maps.event.addListener(mapRef, "dragend", function() {
-                  viewRef.hasDragged = true;
-                });
-
-                google.maps.event.addListener(mapRef, "idle", function(){
-                  // Remove all markers from the map
-                  for (var i = 0; i < viewRef.resultMarkers.length; i++) {
-                      viewRef.resultMarkers[i].setMap(null);
-                  }
-                  viewRef.resultMarkers = new Array();
-
-                  //Check if the user has interacted with the map just now, and if so, we
-                  // want to alter the geohash filter (changing the geohash values or resetting it completely)
-                  var alterGeohashFilter = viewRef.allowSearch || viewRef.hasZoomed || viewRef.hasDragged;
-                  if( !alterGeohashFilter ){
-                    return;
-                  }
-
-                  //Determine if the map needs to be recentered. The map only needs to be
-                  // recentered if it is not at the default lat,long center point AND it
-                  // is not zoomed in or dragged to a new center point
-                  var setGeohashFilter = viewRef.hasZoomed && viewRef.isMapFilterEnabled();
-
-                  //If we are using the geohash filter defined by this map, then
-                  // apply the filter and trigger a new search
-                  if( setGeohashFilter ){
-
-                    viewRef.$(viewRef.mapFilterToggle).show();
-
-                    // Get the Google map bounding box
-                    var boundingBox = mapRef.getBounds();
-
-                    // Set the search model spatial filters
-                    // Encode the Google Map bounding box into geohash
-                    var north = boundingBox.getNorthEast().lat(),
-                        west = boundingBox.getSouthWest().lng(),
-                        south = boundingBox.getSouthWest().lat(),
-                        east = boundingBox.getNorthEast().lng();
-
-                    viewRef.searchModel.set("north", north);
-                    viewRef.searchModel.set("west", west);
-                    viewRef.searchModel.set("south", south);
-                    viewRef.searchModel.set("east", east);
-
-                    // Save the center position and zoom level of the map
-                    viewRef.mapModel.get("mapOptions").center = mapRef.getCenter();
-                    viewRef.mapModel.get("mapOptions").zoom = mapRef.getZoom();
-
-                    // Determine the precision of geohashes to search for
-                    var zoom = mapRef.getZoom();
-
-                    var precision = viewRef.mapModel.getSearchPrecision(zoom);
-
-                    // Get all the geohash tiles contained in the map bounds
-                    var geohashBBoxes = nGeohash.bboxes(south, west, north, east, precision);
-
-                    // Save our geohash search settings
-                    viewRef.searchModel.set("geohashes", geohashBBoxes);
-                    viewRef.searchModel.set("geohashLevel", precision);
-
-                    //Start back at page 0
-                    MetacatUI.appModel.set("page", 0);
-
-                    //Mark the view as ready to start a search
-                    viewRef.ready = true;
-
-                    // Trigger a new search
-                    viewRef.triggerSearch();
-
-                    viewRef.allowSearch = false;
-                  }
-                  else{
-
-                    //Reset the map filter
-                    viewRef.resetMap();
-
-                    //Start back at page 0
-                    MetacatUI.appModel.set("page", 0);
-
-                    //Mark the view as ready to start a search
-                    viewRef.ready = true;
-
-                    // Trigger a new search
-                    viewRef.triggerSearch();
-
-                    viewRef.allowSearch = false;
-
-                    return;
-                  }
-                });
-
-            },
-
-            // Resets the model and view settings related to the map
-            resetMap: function() {
-                if (!gmaps) {
-                    return;
-                }
-
-                // First reset the model
-                // The categories pertaining to the map
-                var categories = ["east", "west", "north", "south"];
-
-                // Loop through each and remove the filters from the model
-                for (var i = 0; i < categories.length; i++) {
-                    this.searchModel.set(categories[i], null);
-                }
-
-                // Reset the map settings
-                this.searchModel.resetGeohash();
-                this.mapModel.set("mapOptions", this.mapModel.defaults().mapOptions);
-
-                this.allowSearch = false;
-            },
-
-            isMapFilterEnabled: function(){
-              var toggleInput = this.$("input" + this.mapFilterToggle);
-              if ((typeof toggleInput === "undefined") || !toggleInput) return;
-
-              return $(toggleInput).prop("checked");
-            },
-
-            toggleMapFilter: function(e, a) {
-                var toggleInput = this.$("input" + this.mapFilterToggle);
-                if ((typeof toggleInput === "undefined") || !toggleInput) return;
-
-                var isOn = $(toggleInput).prop("checked");
-
-                // If the user clicked on the label, then change the checkbox for them
-                if (e.target.tagName != "INPUT") {
-                    isOn = !isOn;
-                    toggleInput.prop("checked", isOn);
-                }
-
-                google.maps.event.trigger(this.mapModel.get("map"), "idle");
-
-                // Track this event
-                MetacatUI.analytics?.trackEvent("map", (isOn ? "on" : "off"));
-
-            },
-
-            /**
-             * Show the marker, infoWindow, and bounding coordinates polygon on
-             the map when the user hovers on the marker icon in the result list
-             * @param {Event} e
-             */
-            showResultOnMap: function(e) {
-                // Exit if maps are not in use
-                if ((this.mode != "map") || (!gmaps)) {
-                    return false;
-                }
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/SolrResults",
+  "models/Search",
+  "models/MetricsModel",
+  "common/Utilities",
+  "views/SearchResultView",
+  "views/searchSelect/AnnotationFilterView",
+  "text!templates/search.html",
+  "text!templates/statCounts.html",
+  "text!templates/pager.html",
+  "text!templates/mainContent.html",
+  "text!templates/currentFilter.html",
+  "text!templates/loading.html",
+  "gmaps",
+  "nGeohash",
+], function (
+  $,
+  _,
+  Backbone,
+  SearchResults,
+  SearchModel,
+  MetricsModel,
+  Utilities,
+  SearchResultView,
+  AnnotationFilter,
+  CatalogTemplate,
+  CountTemplate,
+  PagerTemplate,
+  MainContentTemplate,
+  CurrentFilterTemplate,
+  LoadingTemplate,
+  gmaps,
+  nGeohash,
+) {
+  "use strict";
+
+  /**
+   * @class DataCatalogView
+   * @classcategory Views
+   * @extends Backbone.View
+   * @constructor
+   * @deprecated
+   * @description This view is deprecated and will eventually be removed in a future version (likely 3.0.0)
+   */
+  var DataCatalogView = Backbone.View.extend(
+    /** @lends DataCatalogView.prototype */ {
+      el: "#Content",
+
+      isSubView: false,
+      filters: true, // Turn on/off the filters in this view
+
+      /**
+       * If true, the view height will be adjusted to fit the height of the window
+       * If false, the view height will be fixed via CSS
+       * @type {Boolean}
+       */
+      fixedHeight: false,
+
+      // The default global models for searching
+      searchModel: null,
+      searchResults: null,
+      statsModel: null,
+      mapModel: null,
+
+      /**
+       * The templates for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(CatalogTemplate),
+      statsTemplate: _.template(CountTemplate),
+      pagerTemplate: _.template(PagerTemplate),
+      mainContentTemplate: _.template(MainContentTemplate),
+      currentFilterTemplate: _.template(CurrentFilterTemplate),
+      loadingTemplate: _.template(LoadingTemplate),
+      metricStatTemplate: _.template(
+        "<span class='metric-icon'> <i class='icon" +
+          " <%=metricIcon%>'></i> </span>" +
+          "<span class='metric-value'> <i class='icon metric-icon'>" +
+          "</i> </span>",
+      ),
+
+      // Search mode
+      mode: "map",
+
+      // Map settings and storage
+      map: null,
+      ready: false,
+      allowSearch: true,
+      hasZoomed: false,
+      hasDragged: false,
+      markers: {},
+      tiles: [],
+      tileCounts: [],
+
+      /**
+       * The general error message to show as a title in the error box when there
+       * is an error fetching results from solr
+       * @type {string}
+       * @default "Something went wrong while getting the list of datasets"
+       * @since 2.15.0
+       */
+      solrErrorTitle: "Something went wrong while getting the list of datasets",
+
+      /**
+       * The user-friendly text to show when a solr request gives a status 500
+       * error. If none is provided, then the error message that is returned from
+       * solr will be displayed.
+       * @type {string}
+       * @since 2.15.0
+       */
+      solrError500Message: null,
+
+      // Contains the geohashes for all the markers on the map (if turned on in the Map model)
+      markerGeohashes: [],
+      // Contains all the info windows for all the markers on the map (if turned on in the Map model)
+      markerInfoWindows: [],
+      // Contains all the info windows for each document in the search result list - to display on hover
+      tileInfoWindows: [],
+      // Contains all the currently visible markers on the map
+      resultMarkers: [],
+      // The geohash value for each tile drawn on the map
+      tileGeohashes: [],
+      mapFilterToggle: ".toggle-map-filter",
+
+      // Delegated events for creating new items, and clearing completed ones.
+      events: {
+        "click #results_prev": "prevpage",
+        "click #results_next": "nextpage",
+        "click #results_prev_bottom": "prevpage",
+        "click #results_next_bottom": "nextpage",
+        "click .pagerLink": "navigateToPage",
+        "click .filter.btn": "updateTextFilters",
+        "keypress input[type='text'].filter": "triggerOnEnter",
+        "focus input[type='text'].filter": "getAutocompletes",
+        "change #sortOrder": "triggerSearch",
+        "change #min_year": "updateYearRange",
+        "change #max_year": "updateYearRange",
+        "click #publish_year": "updateYearRange",
+        "click #data_year": "updateYearRange",
+        "click .remove-filter": "removeFilter",
+        "click input[type='checkbox'].filter": "updateBooleanFilters",
+        "click #clear-all": "resetFilters",
+        "click .remove-addtl-criteria": "removeAdditionalCriteria",
+        "click .collapse-me": "collapse",
+        "click .filter-contain .expand-collapse-control":
+          "toggleFilterCollapse",
+        "click #toggle-map": "toggleMapMode",
+        "click .toggle-map": "toggleMapMode",
+        "click .toggle-list": "toggleList",
+        "click .toggle-map-filter": "toggleMapFilter",
+        "mouseover .open-marker": "showResultOnMap",
+        "mouseout .open-marker": "hideResultOnMap",
+        "mouseover .prevent-popover-runoff": "preventPopoverRunoff",
+      },
+
+      initialize: function (options) {
+        var view = this;
+
+        // Get all the options and apply them to this view
+        if (options) {
+          var optionKeys = Object.keys(options);
+          _.each(optionKeys, function (key, i) {
+            view[key] = options[key];
+          });
+        }
+      },
+
+      // Render the main view and/or re-render subviews. Don't call .html() here
+      // so we don't lose state, rather use .setElement(). Delegate rendering
+      // and event handling to sub views
+      render: function () {
+        // Use the global models if there are no other models specified at time of render
+        if (
+          MetacatUI.appModel.get("searchHistory").length > 0 &&
+          (!this.searchModel || Object.keys(this.searchModel).length == 0)
+        ) {
+          var lastSearchModels = _.last(
+            MetacatUI.appModel.get("searchHistory"),
+          );
+
+          if (lastSearchModels) {
+            if (lastSearchModels.search) {
+              this.searchModel = lastSearchModels.search.clone();
+            }
 
-                // Get the attributes about this dataset
-                var resultRow = e.target,
-                    id = $(resultRow).attr("data-id");
-                // The mouseover event might be triggered by a nested element, so loop through the parents to find the id
-                if (typeof id == "undefined") {
-                    $(resultRow).parents().each(function() {
-                        if (typeof $(this).attr("data-id") != "undefined") {
-                            id = $(this).attr("data-id");
-                            resultRow = this;
-                        }
-                    });
-                }
+            if (lastSearchModels.map) {
+              this.mapModel = lastSearchModels.map.clone();
+            }
+          }
+        } else if (
+          typeof MetacatUI.appSearchModel !== "undefined" &&
+          (!this.searchModel || Object.keys(this.searchModel).length == 0)
+        ) {
+          this.searchModel = MetacatUI.appSearchModel;
+          this.mapModel = MetacatUI.mapModel;
+          this.statsModel = MetacatUI.statsModel;
+        }
+
+        if (!this.mapModel && gmaps) {
+          this.mapModel = MetacatUI.mapModel;
+        }
+
+        if (
+          (typeof this.searchResults === "undefined" ||
+            !this.searchResults ||
+            Object.keys(this.searchResults).length == 0) &&
+          MetacatUI.appSearchResults &&
+          Object.keys(MetacatUI.appSearchResults).length > 0
+        ) {
+          this.searchResults = MetacatUI.appSearchResults;
+
+          if (!this.statsModel) {
+            this.statsModel = MetacatUI.statsModel;
+          }
+
+          if (!this.mapModel) {
+            this.mapModel = MetacatUI.mapModel;
+          }
+        }
+
+        // Get the search mode - either "map" or "list"
+        if (typeof this.mode === "undefined" || !this.mode) {
+          this.mode = MetacatUI.appModel.get("searchMode");
+          if (typeof this.mode === "undefined" || !this.mode) {
+            this.mode = "map";
+          }
+          MetacatUI.appModel.set("searchMode", this.mode);
+        }
+        if ($(window).outerWidth() <= 600) {
+          this.mode = "list";
+          MetacatUI.appModel.set("searchMode", "list");
+          gmaps = null;
+        }
+
+        if (!this.isSubView) {
+          MetacatUI.appModel.set("headerType", "default");
+          $("body").addClass("DataCatalog");
+        } else {
+          this.$el.addClass("DataCatalog");
+        }
+
+        // Populate the search template with some model attributes
+        var loadingHTML = this.loadingTemplate({
+          msg: "Retrieving member nodes...",
+        });
 
-                // Find the tile for this data set and highlight it on the map
-                var resultGeohashes = this.searchResults.findWhere({
-                    id: id
-                }).get("geohash_9");
-                for (var i = 0; i < resultGeohashes.length; i++) {
-                    var thisGeohash = resultGeohashes[i],
-                        latLong = nGeohash.decode(thisGeohash),
-                        position = new google.maps.LatLng(latLong.latitude, latLong.longitude),
-                        containingTileGeohash = _.find(this.tileGeohashes, function(g) {
-                            return thisGeohash.indexOf(g) == 0
-                        }),
-                        containingTile = _.findWhere(this.tiles, {
-                            geohash: containingTileGeohash
-                        });
-
-                    // If this is a geohash for a georegion outside the map, do not highlight a tile or display a marker
-                    if (typeof containingTile === "undefined") continue;
-
-                    this.highlightTile(containingTile);
-
-                    // Set up the options for each marker
-                    var markerOptions = {
-                        position: position,
-                        icon: this.mapModel.get("markerImage"),
-                        zIndex: 99999,
-                        map: this.map
-                    };
-
-                    // Create the marker and add to the map
-                    var marker = new google.maps.Marker(markerOptions);
-
-                    this.resultMarkers.push(marker);
+        var templateVars = {
+          gmaps: gmaps,
+          mode: MetacatUI.appModel.get("searchMode"),
+          useMapBounds: this.searchModel.get("useGeohash"),
+          username: MetacatUI.appUserModel.get("username"),
+          isMySearch:
+            _.indexOf(
+              this.searchModel.get("username"),
+              MetacatUI.appUserModel.get("username"),
+            ) > -1,
+          loading: loadingHTML,
+          searchModelRef: this.searchModel,
+          searchResultsRef: this.searchResults,
+          dataSourceTitle:
+            MetacatUI.theme == "dataone" ? "Member Node" : "Data source",
+        };
+        var cel = this.template(
+          _.extend(this.searchModel.toJSON(), templateVars),
+        );
+
+        this.$el.html(cel);
+
+        //Hide the filters that are disabled in the AppModel settings
+        _.each(
+          this.$(".filter-contain[data-category]"),
+          function (filterEl) {
+            if (
+              !_.contains(
+                MetacatUI.appModel.get("defaultSearchFilters"),
+                $(filterEl).attr("data-category"),
+              )
+            ) {
+              $(filterEl).hide();
+            }
+          },
+          this,
+        );
+
+        // Store some references to key views that we use repeatedly
+        this.$resultsview = this.$("#results-view");
+        this.$results = this.$("#results");
+
+        // Update stats
+        this.updateStats();
+
+        // Render the Google Map
+        this.renderMap();
+
+        // Initialize the tooltips
+        var tooltips = $(".tooltip-this");
+
+        // Find the tooltips that are on filter labels - add a slight delay to those
+        var groupedTooltips = _.groupBy(tooltips, function (t) {
+          return (
+            ($(t).prop("tagName") == "LABEL" ||
+              $(t).parent().prop("tagName") == "LABEL") &&
+            $(t).parents(".filter-container").length > 0
+          );
+        });
+        var forFilterLabel = true,
+          forOtherElements = false;
 
-                }
-            },
+        $(groupedTooltips[forFilterLabel]).tooltip({
+          delay: {
+            show: "800",
+          },
+        });
+        $(groupedTooltips[forOtherElements]).tooltip();
 
-            /**
-             * Hide the marker, infoWindow, and bounding coordinates polygon on
-             the map when the user stops hovering on the marker icon in the result list
-             * @param {Event} e - The event that brought us to this function
-             */
-            hideResultOnMap: function(e) {
-                // Exit if maps are not in use
-                if ((this.mode != "map") || (!gmaps)) {
-                    return false;
-                }
+        // Initialize all popover elements
+        $(".popover-this").popover();
 
-                // Get the attributes about this dataset
-                var resultRow = e.target,
-                    id = $(resultRow).attr("data-id");
-                // The mouseover event might be triggered by a nested element, so loop through the parents to find the id
-                if (typeof id == "undefined") {
-                    $(e.target).parents().each(function() {
-                        if (typeof $(this).attr("data-id") != "undefined") {
-                            id = $(this).attr("data-id");
-                            resultRow = this;
-                        }
-                    });
-                }
+        // Initialize the resizeable content div
+        $("#content").resizable({
+          handles: "n,s,e,w",
+        });
 
-                // Get the map tile for this result and un-highlight it
-                var resultGeohashes = this.searchResults.findWhere({
-                    id: id
-                }).get("geohash_9");
-                for (var i = 0; i < resultGeohashes.length; i++) {
-                    var thisGeohash = resultGeohashes[i],
-                        containingTileGeohash = _.find(this.tileGeohashes, function(g) {
-                            return thisGeohash.indexOf(g) == 0
-                        }),
-                        containingTile = _.findWhere(this.tiles, {
-                            geohash: containingTileGeohash
-                        });
-
-                    // If this is a geohash for a georegion outside the map, do not unhighlight a tile
-                    if (typeof containingTile === "undefined") continue;
-
-                    // Unhighlight the tile
-                    this.unhighlightTile(containingTile);
-                }
+        // Collapse the filters
+        this.toggleFilterCollapse();
+
+        // Iterate through each search model text attribute and show UI filter for each
+        var categories = [
+          "all",
+          "attribute",
+          "creator",
+          "id",
+          "taxon",
+          "spatial",
+          "additionalCriteria",
+          "annotation",
+          "isPrivate",
+        ];
+        var thisTerm = null;
+
+        for (var i = 0; i < categories.length; i++) {
+          thisTerm = this.searchModel.get(categories[i]);
+
+          if (thisTerm === undefined || thisTerm === null) break;
+
+          for (var x = 0; x < thisTerm.length; x++) {
+            this.showFilter(categories[i], thisTerm[x]);
+          }
+        }
+
+        // List the Member Node filters
+        var view = this;
+        _.each(
+          _.contains(
+            MetacatUI.appModel.get("defaultSearchFilters"),
+            "dataSource",
+          ),
+          function (source, i) {
+            view.showFilter("dataSource", source);
+          },
+        );
+
+        // Add the custom query under the "Anything" filter
+        if (this.searchModel.get("customQuery")) {
+          this.showFilter("all", this.searchModel.get("customQuery"));
+        }
+
+        // Register listeners; this is done here in render because the HTML
+        // needs to be bound before the listenTo call can be made
+        this.stopListening(this.searchResults);
+        this.stopListening(this.searchModel);
+        this.stopListening(MetacatUI.appModel);
+        this.listenTo(this.searchResults, "reset", this.cacheSearch);
+        this.listenTo(this.searchResults, "add", this.addOne);
+        this.listenTo(this.searchResults, "reset", this.addAll);
+        this.listenTo(this.searchResults, "reset", this.checkForProv);
+        this.listenTo(this.searchResults, "error", this.showError);
+
+        // List data sources
+        this.listDataSources();
+        this.listenTo(
+          MetacatUI.nodeModel,
+          "change:members",
+          this.listDataSources,
+        );
+
+        // listen to the MetacatUI.appModel for the search trigger
+        this.listenTo(MetacatUI.appModel, "search", this.getResults);
+
+        this.listenTo(
+          MetacatUI.appUserModel,
+          "change:loggedIn",
+          this.triggerSearch,
+        );
+
+        // and go to a certain page if we have it
+        this.getResults();
+
+        // Set a custom height on any elements that have the .auto-height class
+        if ($(".auto-height").length > 0 && !this.fixedHeight) {
+          // Readjust the height whenever the window is resized
+          $(window).resize(this.setAutoHeight);
+          $(".auto-height-member").resize(this.setAutoHeight);
+        }
+
+        this.addAnnotationFilter();
+
+        return this;
+      },
+
+      /**
+       * addAnnotationFilter - Add the annotation filter to the view
+       */
+      addAnnotationFilter: function () {
+        if (MetacatUI.appModel.get("bioportalAPIKey")) {
+          var view = this;
+          var popoverTriggerSelector =
+            "[data-category='annotation'] .expand-collapse-control";
+          if (!this.$el.find(popoverTriggerSelector)) {
+            return;
+          }
+          var annotationFilter = new AnnotationFilter({
+            popoverTriggerSelector: popoverTriggerSelector,
+          });
+          this.$el.find(popoverTriggerSelector).append(annotationFilter.el);
+          annotationFilter.render();
+          annotationFilter.off("annotationSelected");
+          annotationFilter.on("annotationSelected", function (event, item) {
+            $("#annotation_input").val(item.value);
+            view.updateTextFilters(event, item);
+          });
+        }
+      },
+
+      // Linked Data Object for appending the jsonld into the browser DOM
+      getLinkedData: function () {
+        // Find the MN info from the CN Node list
+        var members = MetacatUI.nodeModel.get("members");
+        for (var i = 0; i < members.length; i++) {
+          if (
+            members[i].identifier ==
+            MetacatUI.nodeModel.get("currentMemberNode")
+          ) {
+            var nodeModelObject = members[i];
+          }
+        }
+
+        // JSON Linked Data Object
+        let elJSON = {
+          "@context": {
+            "@vocab": "http://schema.org/",
+          },
+          "@type": "DataCatalog",
+        };
+        if (nodeModelObject) {
+          // "keywords": "",
+          // "provider": "",
+          let conditionalData = {
+            description: nodeModelObject.description,
+            identifier: nodeModelObject.identifier,
+            image: nodeModelObject.logo,
+            name: nodeModelObject.name,
+            url: nodeModelObject.url,
+          };
+          $.extend(elJSON, conditionalData);
+        }
+
+        // Check if the jsonld already exists from the previous data view
+        // If not create a new script tag and append otherwise replace the text for the script
+        if (!document.getElementById("jsonld")) {
+          var el = document.createElement("script");
+          el.type = "application/ld+json";
+          el.id = "jsonld";
+          el.text = JSON.stringify(elJSON);
+          document.querySelector("head").appendChild(el);
+        } else {
+          var script = document.getElementById("jsonld");
+          script.text = JSON.stringify(elJSON);
+        }
+        return;
+      },
+
+      /*
+       * Sets the height on elements in the main content area to fill up the entire area minus header and footer
+       */
+      setAutoHeight: function () {
+        // If we are in list mode, don't determine the height of any elements because we are not "full screen"
+        if (
+          MetacatUI.appModel.get("searchMode") == "list" ||
+          this.fixedHeight
+        ) {
+          MetacatUI.appView.$(".auto-height").height("auto");
+          return;
+        }
+
+        // Get the heights of the header, navbar, and footer
+        var otherHeight = 0;
+        $(".auto-height-member").each(function (i, el) {
+          if ($(el).css("display") != "none") {
+            otherHeight += $(el).outerHeight(true);
+          }
+        });
 
-                // Remove all markers from the map
-                _.each(this.resultMarkers, function(marker) {
-                    marker.setMap(null);
-                });
-                this.resultMarkers = new Array();
+        // Get the remaining height left based on the window size
+        var remainingHeight = $(window).outerHeight(true) - otherHeight;
+        if (remainingHeight < 0)
+          remainingHeight = $(window).outerHeight(true) || 300;
+        else if (remainingHeight <= 120)
+          remainingHeight =
+            $(window).outerHeight(true) - remainingHeight || 300;
+
+        // Adjust all elements with the .auto-height class
+        $(".auto-height").height(remainingHeight);
+
+        if (
+          $("#map-container.auto-height").length > 0 &&
+          $("#map-canvas").length > 0
+        ) {
+          var otherHeight = 0;
+          $("#map-container.auto-height")
+            .children()
+            .each(function (i, el) {
+              if ($(el).attr("id") != "map-canvas") {
+                otherHeight += $(el).outerHeight(true);
+              }
+            });
+          var newMapHeight = remainingHeight - otherHeight;
+          if (newMapHeight > 100) {
+            $("#map-canvas").height(remainingHeight - otherHeight);
+          }
+        }
+
+        // Trigger a resize for the map so that all of the map background images are loaded
+        if (gmaps && this.mapModel && this.mapModel.get("map")) {
+          google.maps.event.trigger(this.mapModel.get("map"), "resize");
+        }
+      },
+
+      /*
+       * ==================================================================================================
+       *                                         PERFORMING SEARCH
+       * ==================================================================================================
+       */
+      triggerSearch: function () {
+        // Set the sort order
+        var sortOrder = $("#sortOrder").val();
+        if (sortOrder) {
+          this.searchModel.set("sortOrder", sortOrder);
+        }
+
+        // Trigger a search to load the results
+        MetacatUI.appModel.trigger("search");
+
+        if (!this.isSubView) {
+          // make sure the browser knows where we are
+          var route = Backbone.history.fragment;
+          if (route.indexOf("data") < 0) {
+            MetacatUI.uiRouter.navigate("data", {
+              trigger: false,
+              replace: true,
+            });
+          } else {
+            MetacatUI.uiRouter.navigate(route);
+          }
+        }
+
+        // ...but don't want to follow links
+        return false;
+      },
+
+      triggerOnEnter: function (e) {
+        if (e.keyCode != 13) return;
+
+        // Update the filters
+        this.updateTextFilters(e);
+      },
+
+      /**
+       * getResults gets all the current search filters from the searchModel, creates a Solr query, and runs that query.
+       * @param {number} page - The page of search results to get results for
+       */
+      getResults: function (page) {
+        // Set the sort order based on user choice
+        var sortOrder = this.searchModel.get("sortOrder");
+        if (sortOrder) {
+          this.searchResults.setSort(sortOrder);
+        }
+
+        // Specify which fields to retrieve
+        var fields = "";
+        fields += "id,";
+        fields += "seriesId,";
+        fields += "title,";
+        fields += "origin,";
+        fields += "pubDate,";
+        fields += "dateUploaded,";
+        fields += "abstract,";
+        fields += "resourceMap,";
+        fields += "beginDate,";
+        fields += "endDate,";
+        fields += "read_count_i,";
+        fields += "geohash_9,";
+        fields += "datasource,";
+        fields += "isPublic,";
+        fields += "documents,";
+        fields += "sem_annotation,";
+        // Add spatial fields if the map is present
+        if (gmaps) {
+          fields += "northBoundCoord,";
+          fields += "southBoundCoord,";
+          fields += "eastBoundCoord,";
+          fields += "westBoundCoord";
+        }
+        // Strip the last trailing comma if needed
+        if (fields[fields.length - 1] === ",") {
+          fields = fields.substr(0, fields.length - 1);
+        }
+        this.searchResults.setfields(fields);
+
+        // Get the query
+        var query = this.searchModel.getQuery();
+
+        // Specify which facets to retrieve
+        if (gmaps && this.map) {
+          // If we have Google Maps enabled
+          var geohashLevel =
+            "geohash_" + this.mapModel.determineGeohashLevel(this.map.zoom);
+          this.searchResults.facet.push(geohashLevel);
+        }
+
+        // Run the query
+        this.searchResults.setQuery(query);
+
+        // Get the page number
+        if (this.isSubView) {
+          var page = 0;
+        } else {
+          var page = MetacatUI.appModel.get("page");
+          if (page == null) {
+            page = 0;
+          }
+        }
+        this.searchResults.start = page * this.searchResults.rows;
+
+        // Show or hide the reset filters button
+        this.toggleClearButton();
+
+        // go to the page
+        this.showPage(page);
+
+        // don't want to follow links
+        return false;
+      },
+
+      /*
+       * After the search results have been returned,
+       * check if any of them are derived data or have derivations
+       */
+      checkForProv: function () {
+        var maps = [],
+          hasSources = [],
+          hasDerivations = [],
+          mainSearchResults = this.searchResults;
+
+        // Get a list of all the resource map IDs from the SolrResults collection
+        maps = this.searchResults.pluck("resourceMap");
+        maps = _.compact(_.flatten(maps));
+
+        // Create a new Search model with a search that finds all members of these packages/resource maps
+        var provSearchModel = new SearchModel({
+          formatType: [
+            {
+              value: "DATA",
+              label: "data",
+              description: null,
             },
+          ],
+          exclude: [],
+          resourceMap: maps,
+        });
 
-            /**
-             * Create a tile for each geohash facet. A separate tile label is added to the map with the count of the facet.
-             **/
-            drawTiles: function () {
-
-                // Exit if maps are not in use
-                if ((this.mode != "map") || (!gmaps)) {
-                    return false;
-                }
-
-                TextOverlay.prototype = new google.maps.OverlayView();
-
-                function TextOverlay(options) {
-                    // Now initialize all properties.
-                    this.bounds_ = options.bounds;
-                    this.map_ = options.map;
-                    this.text = options.text;
-                    this.color = options.color;
-
-                    var length = options.text.toString().length;
-                    if (length == 1) this.width = 8;
-                    else if (length == 2) this.width = 17;
-                    else if (length == 3) this.width = 25;
-                    else if (length == 4) this.width = 32;
-                    else if (length == 5) this.width = 40;
-
-                    // We define a property to hold the image's div. We'll
-                    // actually create this div upon receipt of the onAdd()
-                    // method so we'll leave it null for now.
-                    this.div_ = null;
-
-                    // Explicitly call setMap on this overlay
-                    this.setMap(options.map);
-                }
-
-                TextOverlay.prototype.onAdd = function() {
-
-                    // Create the DIV and set some basic attributes.
-                    var div = document.createElement("div");
-                    div.style.color = this.color;
-                    div.style.fontSize = "15px";
-                    div.style.position = "absolute";
-                    div.style.zIndex = "999";
-                    div.style.fontWeight = "bold";
-
-                    // Create an IMG element and attach it to the DIV.
-                    div.innerHTML = this.text;
-
-                    // Set the overlay's div_ property to this DIV
-                    this.div_ = div;
-
-                    // We add an overlay to a map via one of the map's panes.
-                    // We'll add this overlay to the overlayLayer pane.
-                    var panes = this.getPanes();
-                    panes.overlayLayer.appendChild(div);
-                }
-
-                TextOverlay.prototype.draw = function() {
-                    // Size and position the overlay. We use a southwest and northeast
-                    // position of the overlay to peg it to the correct position and size.
-                    // We need to retrieve the projection from this overlay to do this.
-                    var overlayProjection = this.getProjection();
-
-                    // Retrieve the southwest and northeast coordinates of this overlay
-                    // in latlngs and convert them to pixels coordinates.
-                    // We'll use these coordinates to resize the DIV.
-                    var sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest());
-                    var ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast());
-                    // Resize the image's DIV to fit the indicated dimensions.
-                    var div = this.div_;
-                    var width = this.width;
-                    var height = 20;
-
-                    div.style.left = (sw.x - width / 2) + "px";
-                    div.style.top = (ne.y - height / 2) + "px";
-                    div.style.width = width + "px";
-                    div.style.height = height + "px";
-                    div.style.width = width + "px";
-                    div.style.height = height + "px";
-
-                }
-
-                TextOverlay.prototype.onRemove = function() {
-                    this.div_.parentNode.removeChild(this.div_);
-                    this.div_ = null;
-                }
-
-                // Determine the geohash level we will use to draw tiles
-                var currentZoom = this.map.getZoom(),
-                    geohashLevelNum = this.mapModel.determineGeohashLevel(currentZoom),
-                    geohashLevel = "geohash_" + geohashLevelNum,
-                    geohashes = this.searchResults.facetCounts[geohashLevel];
-
-                // Save the current geohash level in the map model
-                this.mapModel.set("tileGeohashLevel", geohashLevelNum);
-
-                // Get all the geohashes contained in the map
-                var mapBBoxes = _.flatten(_.values(this.searchModel.get("geohashGroups")));
-
-                // Geohashes may be returned that are part of datasets with multiple geographic areas. Some of these may be outside this map.
-                // So we will want to filter out geohashes that are not contained in this map.
-                if (mapBBoxes.length == 0) {
-                    var filteredTileGeohashes = geohashes;
-                } else if( geohashes ){
-                    var filteredTileGeohashes = [];
-                    for (var i = 0; i < geohashes.length - 1; i += 2) {
-
-                        // Get the geohash for this tile
-                        var tileGeohash = geohashes[i],
-                            isInsideMap = false,
-                            index = 0,
-                            searchString = tileGeohash;
-
-                        // Find if any of the bounding boxes/geohashes inside our map contain this tile geohash
-                        while ((!isInsideMap) && (searchString.length > 0)) {
-                            searchString = tileGeohash.substring(0, tileGeohash.length - index);
-                            if (_.contains(mapBBoxes, searchString)) isInsideMap = true;
-                            index++;
-                        }
-
-                        if (isInsideMap) {
-                            filteredTileGeohashes.push(tileGeohash);
-                            filteredTileGeohashes.push(geohashes[i + 1]);
-                        }
-                    }
-                }
-
-                //If there are no tiles on the page, the map may have failed to render, so exit.
-                if( typeof filteredTileGeohashes == "undefined" || !filteredTileGeohashes.length ){
-                  return;
-                }
-
-                // Make a copy of the array that is geohash counts only
-                var countsOnly = [];
-                for (var i = 1; i < filteredTileGeohashes.length; i += 2) {
-                    countsOnly.push(filteredTileGeohashes[i]);
-                }
-
-                // Create a range of lightness to make different colors on the tiles
-                var lightnessMin = this.mapModel.get("tileLightnessMin"),
-                    lightnessMax = this.mapModel.get("tileLightnessMax"),
-                    lightnessRange = lightnessMax - lightnessMin;
-
-                // Get some stats on our tile counts so we can normalize them to create a color scale
-                var findMedian = function(nums) {
-                    if (nums.length % 2 == 0) {
-                        return (nums[(nums.length / 2) - 1] + nums[(nums.length / 2)]) / 2;
-                    } else {
-                        return nums[(nums.length / 2) - 0.5];
-                    }
-                }
-                var sortedCounts = countsOnly.sort(function(a, b) {
-                        return a - b;
-                    }),
-                    maxCount = sortedCounts[sortedCounts.length - 1],
-                    minCount = sortedCounts[0];
-
-                var viewRef = this;
-
-                // Now draw a tile for each geohash facet
-                for (var i = 0; i < filteredTileGeohashes.length - 1; i += 2) {
-
-                    // Convert this geohash to lat,long values
-                    var tileGeohash = filteredTileGeohashes[i],
-                        decodedGeohash = nGeohash.decode(tileGeohash),
-                        latLngCenter = new google.maps.LatLng(decodedGeohash.latitude, decodedGeohash.longitude),
-                        geohashBox = nGeohash.decode_bbox(tileGeohash),
-                        swLatLng = new google.maps.LatLng(geohashBox[0], geohashBox[1]),
-                        neLatLng = new google.maps.LatLng(geohashBox[2], geohashBox[3]),
-                        bounds = new google.maps.LatLngBounds(swLatLng, neLatLng),
-                        tileCount = filteredTileGeohashes[i + 1],
-                        drawMarkers = this.mapModel.get("drawMarkers"),
-                        marker,
-                        count,
-                        color;
-
-                    // Normalize the range of tiles counts and convert them to a lightness domain of 20-70% lightness.
-                    if (maxCount - minCount == 0) {
-                        var lightness = lightnessRange;
-                    } else {
-                        var lightness = (((tileCount - minCount) / (maxCount - minCount)) * lightnessRange) + lightnessMin;
-                    }
+        // Create a new Solr Results model to store the results of this supplemental query
+        var provSearchResults = new SearchResults(null, {
+          query: provSearchModel.getQuery(),
+          searchLogs: false,
+          usePOST: true,
+          rows: 150,
+          fields: provSearchModel.getProvFlList() + ",id,resourceMap",
+        });
 
-                    var color = "hsl(" + this.mapModel.get("tileHue") + "," + lightness + "%,50%)";
-
-                    // Add the count to the tile
-                    var countLocation = new google.maps.LatLngBounds(latLngCenter, latLngCenter);
-
-                    // Draw the tile label with the dataset count
-                    count = new TextOverlay({
-                        bounds: countLocation,
-                        map: this.map,
-                        text: tileCount,
-                        color: this.mapModel.get("tileLabelColor")
-                    });
-
-                    // Set up the default tile options
-                    var tileOptions = {
-                        fillColor: color,
-                        strokeColor: color,
-                        map: this.map,
-                        visible: true,
-                        bounds: bounds
-                    };
-
-                    // Merge these options with any tile options set in the map model
-                    var modelTileOptions = this.mapModel.get("tileOptions");
-                    for (var attr in modelTileOptions) {
-                        tileOptions[attr] = modelTileOptions[attr];
-                    }
+        // Trigger a search on that Solr Results model
+        this.listenTo(provSearchResults, "reset", function (results) {
+          if (results.models.length == 0) return;
+
+          // See if any of the results have a value for a prov field
+          results.forEach(function (result) {
+            if (!result.getSources().length || !result.getDerivations()) return;
+            _.each(result.get("resourceMap"), function (rMapID) {
+              if (_.contains(maps, rMapID)) {
+                var match = mainSearchResults.filter(
+                  function (mainSearchResult) {
+                    return _.contains(
+                      mainSearchResult.get("resourceMap"),
+                      rMapID,
+                    );
+                  },
+                );
+                if (match && match.length && result.getSources().length > 0)
+                  hasSources.push(match[0].get("id"));
+                if (match && match.length && result.getDerivations().length > 0)
+                  hasDerivations.push(match[0].get("id"));
+              }
+            });
+          });
+
+          // Filter out the duplicates
+          hasSources = _.uniq(hasSources);
+          hasDerivations = _.uniq(hasDerivations);
+
+          // If they do, find their corresponding result row here and add
+          // the prov icon (or just change the class to active)
+          _.each(hasSources, function (metadataID) {
+            var metadataDoc = mainSearchResults.findWhere({
+              id: metadataID,
+            });
+            if (metadataDoc) {
+              metadataDoc.set("prov_hasSources", true);
+            }
+          });
+          _.each(hasDerivations, function (metadataID) {
+            var metadataDoc = mainSearchResults.findWhere({
+              id: metadataID,
+            });
+            if (metadataDoc) {
+              metadataDoc.set("prov_hasDerivations", true);
+            }
+          });
+        });
+        provSearchResults.toPage(0);
+      },
 
-                    // Draw this tile
-                    var tile = this.drawTile(tileOptions, tileGeohash, count);
+      cacheSearch: function () {
+        MetacatUI.appModel.get("searchHistory").push({
+          search: this.searchModel.clone(),
+          map: this.mapModel ? this.mapModel.clone() : null,
+        });
+        MetacatUI.appModel.trigger("change:searchHistory");
+      },
+
+      /*
+       * ==================================================================================================
+       *                                             FILTERS
+       * ==================================================================================================
+       */
+      updateCheckboxFilter: function (e, category, value) {
+        if (!this.filters) return;
+
+        var checkbox = e.target;
+        var checked = $(checkbox).prop("checked");
+
+        if (typeof category == "undefined")
+          var category = $(checkbox).attr("data-category");
+        if (typeof value == "undefined") var value = $(checkbox).attr("value");
+
+        // If the user just unchecked the box, then remove this filter
+        if (!checked) {
+          this.searchModel.removeFromModel(category, value);
+          this.hideFilter(category, value);
+        }
+        // If the user just checked the box, then add this filter
+        else {
+          var currentValue = this.searchModel.get(category);
+
+          // Get the description
+          var desc =
+            $(checkbox).attr("data-description") || $(checkbox).attr("title");
+          if (typeof desc == "undefined" || !desc) desc = "";
+          // Get the label
+          var labl = $(checkbox).attr("data-label");
+          if (typeof labl == "undefined" || !labl) labl = "";
+
+          // Make the filter object
+          var filter = {
+            description: desc,
+            label: labl,
+            value: value,
+          };
+
+          // If this filter category is an array, add this value to the array
+          if (Array.isArray(currentValue)) {
+            currentValue.push(filter);
+            this.searchModel.set(category, currentValue);
+            this.searchModel.trigger("change:" + category);
+          } else {
+            // If it isn't an array, then just update the model with a simple value
+            this.searchModel.set(category, filter);
+          }
+
+          // Show the filter element
+          this.showFilter(category, value, true, labl);
+
+          // Show the reset button
+          this.showClearButton();
+        }
+
+        // Route to page 1
+        this.updatePageNumber(0);
+
+        // Trigger a new search
+        this.triggerSearch();
+      },
+
+      updateBooleanFilters: function (e) {
+        if (!this.filters) return;
+
+        // Get the category
+        var checkbox = e.target;
+        var category = $(checkbox).attr("data-category");
+        var currentValue = this.searchModel.get(category);
+
+        // If this filter is not enabled, exit this function
+        if (
+          !_.contains(MetacatUI.appModel.get("defaultSearchFilters"), category)
+        ) {
+          return false;
+        }
+
+        //The year filter is handled in a different way
+        if (category == "pubYear" || category == "dataYear") return;
+
+        // If the checkbox has a value, then update as a string value not boolean
+        var value = $(checkbox).attr("value");
+        if (value) {
+          this.updateCheckboxFilter(e, category, value);
+          return;
+        } else value = $(checkbox).prop("checked");
+
+        this.searchModel.set(category, value);
+
+        // Add the filter to the UI
+        if (value) {
+          this.showFilter(category, "", true);
+        } else {
+          // Remove the filter from the UI
+          value = "";
+          this.hideFilter(category, value);
+        }
+
+        // Show the reset button
+        this.showClearButton();
+
+        // Route to page 1
+        this.updatePageNumber(0);
+
+        // Trigger a new search
+        this.triggerSearch();
+
+        // Track this event
+        MetacatUI.analytics?.trackEvent("search", "filter, " + category, value);
+      },
+
+      // Update the UI year slider and input values
+      // Also update the model
+      updateYearRange: function (e) {
+        if (!this.filters) return;
+
+        var viewRef = this,
+          userAction = !(typeof e === "undefined"),
+          model = this.searchModel,
+          pubYearChecked = $("#publish_year").prop("checked"),
+          dataYearChecked = $("#data_year").prop("checked");
+
+        // If the year range slider has not been created yet
+        if (!userAction && !$("#year-range").hasClass("ui-slider")) {
+          var defaultMin =
+              typeof this.searchModel.defaults == "function"
+                ? this.searchModel.defaults().yearMin
+                : 1800,
+            defaultMax =
+              typeof this.searchModel.defaults == "function"
+                ? this.searchModel.defaults().yearMax
+                : new Date().getUTCFullYear();
+
+          //jQueryUI slider
+          $("#year-range").slider({
+            range: true,
+            disabled: false,
+            min: defaultMin, //sets the minimum on the UI slider on initialization
+            max: defaultMax, //sets the maximum on the UI slider on initialization
+            values: [
+              this.searchModel.get("yearMin"),
+              this.searchModel.get("yearMax"),
+            ], //where the left and right slider handles are
+            stop: function (event, ui) {
+              // When the slider is changed, update the input values
+              $("#min_year").val(ui.values[0]);
+              $("#max_year").val(ui.values[1]);
+
+              // Also update the search model
+              model.set("yearMin", ui.values[0]);
+              model.set("yearMax", ui.values[1]);
+
+              // If neither the publish year or data coverage year are checked
+              if (
+                !$("#publish_year").prop("checked") &&
+                !$("#data_year").prop("checked")
+              ) {
+                // We want to check the data coverage year on the user's behalf
+                $("#data_year").prop("checked", "true");
+
+                // And update the search model
+                model.set("dataYear", true);
+              }
 
-                    // Save the geohashes for tiles in the view for later
-                    this.tileGeohashes.push(tileGeohash);
-                }
+              // Add the filter elements
+              if ($("#publish_year").prop("checked")) {
+                viewRef.showFilter(
+                  $("#publish_year").attr("data-category"),
+                  true,
+                  false,
+                  ui.values[0] + " to " + ui.values[1],
+                  {
+                    replace: true,
+                  },
+                );
+              }
+              if ($("#data_year").prop("checked")) {
+                viewRef.showFilter(
+                  $("#data_year").attr("data-category"),
+                  true,
+                  false,
+                  ui.values[0] + " to " + ui.values[1],
+                  {
+                    replace: true,
+                  },
+                );
+              }
 
-                // Create an info window for each marker that is on the map, to display when it is clicked on
-                if (this.markerGeohashes.length > 0) this.addMarkers();
+              // Route to page 1
+              viewRef.updatePageNumber(0);
 
-                // If the map is zoomed all the way in, draw info windows for each tile that will be displayed when they are clicked on
-                if (this.mapModel.isMaxZoom(this.map)) this.addTileInfoWindows();
+              // Trigger a new search
+              viewRef.triggerSearch();
             },
-
-            /**
-             * With the options and label object given, add a single tile to the map and set its event listeners
-             * @param {object} options
-             * @param {string} geohash
-             * @param {string} label
-             **/
-            drawTile: function(options, geohash, label) {
-                // Exit if maps are not in use
-                if ((this.mode != "map") || (!gmaps)) {
-                    return false;
-                }
-
-                // Add the tile for these datasets to the map
-                var tile = new google.maps.Rectangle(options);
-
-                var viewRef = this;
-
-                // Save our tiles in the view
-                var tileObject = {
-                    text: label,
-                    shape: tile,
-                    geohash: geohash,
-                    options: options
-                };
-                this.tiles.push(tileObject);
-
-                // Change styles when the tile is hovered on
-                google.maps.event.addListener(tile, "mouseover", function(event) {
-                    viewRef.highlightTile(tileObject);
+          });
+
+          // Get the minimum and maximum years of this current search and use those as the min and max values in the slider
+          this.statsModel.set("query", this.searchModel.getQuery());
+          this.listenTo(this.statsModel, "change:firstBeginDate", function () {
+            if (
+              this.statsModel.get("firstBeginDate") == 0 ||
+              !this.statsModel.get("firstBeginDate")
+            ) {
+              $("#year-range").slider({
+                min: defaultMin,
+              });
+              return;
+            }
+            var year = new Date(
+              this.statsModel.get("firstBeginDate"),
+            ).getUTCFullYear();
+            if (typeof year !== "undefined") {
+              $("#min_year").val(year);
+              $("#year-range").slider({
+                values: [year, $("#max_year").val()],
+              });
+
+              // If the slider min is still at the default value, then update with the min value found at this search
+              if ($("#year-range").slider("option", "min") == defaultMin) {
+                $("#year-range").slider({
+                  min: year,
                 });
+              }
 
-                // Change the styles back after the tile is hovered on
-                google.maps.event.addListener(tile, "mouseout", function(event) {
-                    viewRef.unhighlightTile(tileObject);
+              // Add the filter elements if this is set
+              if (viewRef.searchModel.get("pubYear")) {
+                viewRef.showFilter(
+                  "pubYear",
+                  true,
+                  false,
+                  $("#min_year").val() + " to " + $("#max_year").val(),
+                  {
+                    replace: true,
+                  },
+                );
+              }
+              if (viewRef.searchModel.get("dataYear")) {
+                viewRef.showFilter(
+                  "dataYear",
+                  true,
+                  false,
+                  $("#min_year").val() + " to " + $("#max_year").val(),
+                  {
+                    replace: true,
+                  },
+                );
+              }
+            }
+          });
+          // Only when the first begin date is retrieved, set the slider min and max values
+          this.listenTo(this.statsModel, "change:lastEndDate", function () {
+            if (
+              this.statsModel.get("lastEndDate") == 0 ||
+              !this.statsModel.get("lastEndDate")
+            ) {
+              $("#year-range").slider({
+                max: defaultMax,
+              });
+              return;
+            }
+            var year = new Date(
+              this.statsModel.get("lastEndDate"),
+            ).getUTCFullYear();
+            if (typeof year !== "undefined") {
+              $("#max_year").val(year);
+              $("#year-range").slider({
+                values: [$("#min_year").val(), year],
+              });
+
+              // If the slider max is still at the default value, then update with the max value found at this search
+              if ($("#year-range").slider("option", "max") == defaultMax) {
+                $("#year-range").slider({
+                  max: year,
                 });
+              }
 
-                // If we are at the max zoom, we will display an info window. If not, we will zoom in.
-                if (!this.mapModel.isMaxZoom(viewRef.map)) {
-
-                    /** Set up some helper functions for zooming in on the map **/
-                    var myFitBounds = function(myMap, bounds) {
-                        myMap.fitBounds(bounds); // calling fitBounds() here to center the map for the bounds
-
-                        var overlayHelper = new google.maps.OverlayView();
-                        overlayHelper.draw = function() {
-                            if (!this.ready) {
-                                var extraZoom = getExtraZoom(this.getProjection(), bounds, myMap.getBounds());
-                                if (extraZoom > 0) {
-                                    myMap.setZoom(myMap.getZoom() + extraZoom);
-                                }
-                                this.ready = true;
-                                google.maps.event.trigger(this, "ready");
-                            }
-                        };
-                        overlayHelper.setMap(myMap);
-                    }
-                    var getExtraZoom = function(projection, expectedBounds, actualBounds) {
-
-                        // in: LatLngBounds bounds -> out: height and width as a Point
-                        var getSizeInPixels = function(bounds) {
-                            var sw = projection.fromLatLngToContainerPixel(bounds.getSouthWest());
-                            var ne = projection.fromLatLngToContainerPixel(bounds.getNorthEast());
-                            return new google.maps.Point(Math.abs(sw.y - ne.y), Math.abs(sw.x - ne.x));
-                        }
-
-                        var expectedSize = getSizeInPixels(expectedBounds),
-                            actualSize = getSizeInPixels(actualBounds);
-
-                        if (Math.floor(expectedSize.x) == 0 || Math.floor(expectedSize.y) == 0) {
-                            return 0;
-                        }
-
-                        var qx = actualSize.x / expectedSize.x;
-                        var qy = actualSize.y / expectedSize.y;
-                        var min = Math.min(qx, qy);
-
-                        if (min < 1) {
-                            return 0;
-                        }
-
-                        return Math.floor(Math.log(min) / Math.LN2 /* = log2(min) */ );
-                    }
-
-                    // Zoom in when the tile is clicked on
-                    gmaps.event.addListener(tile, "click", function(clickEvent) {
-                        // Change the center
-                        viewRef.map.panTo(clickEvent.latLng);
-
-                        // Get this tile's bounds
-                        var tileBounds = tile.getBounds();
-                        // Get the current map bounds
-                        var mapBounds = viewRef.map.getBounds();
-
-                        // Change the zoom
-                        //viewRef.map.fitBounds(tileBounds);
-                        myFitBounds(viewRef.map, tileBounds);
-
+              // Add the filter elements if this is set
+              if (viewRef.searchModel.get("pubYear")) {
+                viewRef.showFilter(
+                  "pubYear",
+                  true,
+                  false,
+                  $("#min_year").val() + " to " + $("#max_year").val(),
+                  {
+                    replace: true,
+                  },
+                );
+              }
+              if (viewRef.searchModel.get("dataYear")) {
+                viewRef.showFilter(
+                  "dataYear",
+                  true,
+                  false,
+                  $("#min_year").val() + " to " + $("#max_year").val(),
+                  {
+                    replace: true,
+                  },
+                );
+              }
+            }
+          });
+          this.statsModel.getFirstBeginDate();
+          this.statsModel.getLastEndDate();
+        }
+        // If the year slider has been created and the user initiated a new search using other filters
+        else if (
+          !userAction &&
+          !this.searchModel.get("dataYear") &&
+          !this.searchModel.get("pubYear")
+        ) {
+          // Reset the min and max year based on this search
+          this.statsModel.set("query", this.searchModel.getQuery());
+          this.statsModel.getFirstBeginDate();
+          this.statsModel.getLastEndDate();
+        }
+        // If either of the year type selectors is what brought us here, then determine whether the user
+        // is completely removing both (reset both year filters) or just one (remove just that one filter)
+        else if (userAction) {
+          // When both year types were unchecked, assume user wants to reset the year filter
+          if (
+            ($(e.target).attr("id") == "data_year" ||
+              $(e.target).attr("id") == "publish_year") &&
+            !pubYearChecked &&
+            !dataYearChecked
+          ) {
+            // Reset the search model
+            this.searchModel.set("yearMin", defaultMin);
+            this.searchModel.set("yearMax", defaultMax);
+            this.searchModel.set("dataYear", false);
+            this.searchModel.set("pubYear", false);
+
+            // Reset the min and max year based on this search
+            this.statsModel.set("query", this.searchModel.getQuery());
+            this.statsModel.getFirstBeginDate();
+            this.statsModel.getLastEndDate();
+
+            // Slide the handles back to the defaults
+            $("#year-range").slider("values", [defaultMin, defaultMax]);
+
+            // Hide the filters
+            this.hideFilter("dataYear");
+            this.hideFilter("pubYear");
+          }
+          // If either of the year inputs have changed or if just one of the year types were unchecked
+          else {
+            var minVal = $("#min_year").val();
+            var maxVal = $("#max_year").val();
+
+            // Update the search model to match what is in the text inputs
+            this.searchModel.set("yearMin", minVal);
+            this.searchModel.set("yearMax", maxVal);
+            this.searchModel.set("dataYear", dataYearChecked);
+            this.searchModel.set("pubYear", pubYearChecked);
+
+            // If neither the publish year or data coverage year are checked
+            if (!pubYearChecked && !dataYearChecked) {
+              // We want to check the data coverage year on the user's behalf
+              $("#data_year").prop("checked", "true");
+
+              // And update the search model
+              model.set("dataYear", true);
+
+              // Add the filter elements
+              this.showFilter(
+                $("#data_year").attr("data-category"),
+                true,
+                true,
+                minVal + " to " + maxVal,
+                {
+                  replace: true,
+                },
+              );
 
-                        // Track this event
-                        MetacatUI.analytics?.trackEvent("map", "clickTile", "geohash : " + tileObject.geohash);
+              // Track this event
+              MetacatUI.analytics?.trackEvent(
+                "search",
+                "filter, Data Year",
+                minVal + " to " + maxVal,
+              );
+            } else {
+              // Add the filter elements
+              if (pubYearChecked) {
+                this.showFilter(
+                  $("#publish_year").attr("data-category"),
+                  true,
+                  true,
+                  minVal + " to " + maxVal,
+                  {
+                    replace: true,
+                  },
+                );
 
-                    });
-                }
+                // Track this event
+                MetacatUI.analytics?.trackEvent(
+                  "search",
+                  "filter, Publication Year",
+                  minVal + " to " + maxVal,
+                );
+              } else {
+                this.hideFilter($("#publish_year").attr("data-category"), true);
+              }
 
-                return tile;
-            },
+              if (dataYearChecked) {
+                this.showFilter(
+                  $("#data_year").attr("data-category"),
+                  true,
+                  true,
+                  minVal + " to " + maxVal,
+                  {
+                    replace: true,
+                  },
+                );
 
-            highlightTile: function(tile) {
-                // Change the tile style on hover
-                tile.shape.setOptions(this.mapModel.get("tileOnHover"));
+                // Track this event
+                MetacatUI.analytics?.trackEvent(
+                  "search",
+                  "filter, Data Year",
+                  minVal + " to " + maxVal,
+                );
+              } else {
+                this.hideFilter($("#data_year").attr("data-category"), true);
+              }
+            }
+          }
 
-                // Change the label color on hover
-                var div = tile.text.div_;
-                if(div){
-                  div.style.color = this.mapModel.get("tileLabelColorOnHover");
-                  tile.text.div_ = div;
-                  $(div).css("color", this.mapModel.get("tileLabelColorOnHover"));
-                }
-            },
+          // Route to page 1
+          this.updatePageNumber(0);
 
-            unhighlightTile: function(tile) {
-                // Change back the tile to it's original styling
-                tile.shape.setOptions(tile.options);
+          // Trigger a new search
+          this.triggerSearch();
+        }
+      },
 
-                // Change back the label color
-                var div = tile.text.div_;
-                div.style.color = this.mapModel.get("tileLabelColor");
-                tile.text.div_ = div;
-                $(div).css("color", this.mapModel.get("tileLabelColor"));
-            },
+      updateTextFilters: function (e, item) {
+        if (!this.filters) return;
 
-            /**
-             * Get the details on each marker
-             * And create an infowindow for that marker
-             */
-            addMarkers: function() {
-                // Exit if maps are not in use
-                if ((this.mode != "map") || (!gmaps)) {
-                    return false;
-                }
+        // Get the search/filter category
+        var category = $(e.target).attr("data-category");
 
-                // Clone the Search model
-                var searchModelClone = this.searchModel.clone(),
-                    geohashLevel = this.mapModel.get("tileGeohashLevel"),
-                    viewRef = this,
-                    markers = this.markers;
-
-                // Change the geohash filter to match our tiles
-                searchModelClone.set("geohashLevel", geohashLevel);
-                searchModelClone.set("geohashes", this.markerGeohashes);
-
-                // Now run a query to get a list of documents that are represented by our markers
-                var query = "q=" + searchModelClone.getQuery() +
-                    "&fl=id,title,geohash_9,abstract,geohash_" + geohashLevel +
-                    "&rows=1000" +
-                    "&wt=json";
-
-                var requestSettings = {
-                    url: MetacatUI.appModel.get("queryServiceUrl") + query,
-                    success: function(data, textStatus, xhr) {
-                        var docs = data.response.docs;
-                        var uniqueGeohashes = viewRef.markerGeohashes;
-
-                        // Create a marker and infoWindow for each document
-                        _.each(docs, function(doc, key, list) {
-
-                            var marker,
-                                drawMarkersAt = [];
-
-                            // Find the tile place that this document belongs to
-                            // For each geohash value at the current geohash level for this document,
-                            _.each(doc.geohash_9, function(geohash, key, list) {
-                                // Loop through each unique tile location to find its match
-                                for (var i = 0; i <= uniqueGeohashes.length; i++) {
-                                    if (uniqueGeohashes[i] == geohash.substr(0, geohashLevel)) {
-                                        drawMarkersAt.push(geohash);
-                                        uniqueGeohashes = _.without(uniqueGeohashes, geohash);
-                                    }
-                                }
-                            });
-
-                            _.each(drawMarkersAt, function(markerGeohash, key, list) {
-
-                                var decodedGeohash = nGeohash.decode(markerGeohash),
-                                    latLng = new google.maps.LatLng(decodedGeohash.latitude, decodedGeohash.longitude);
-
-                                // Set up the options for each marker
-                                var markerOptions = {
-                                    position: latLng,
-                                    icon: this.mapModel.get("markerImage"),
-                                    zIndex: 99999,
-                                    map: viewRef.map
-                                };
-
-                                // Create the marker and add to the map
-                                var marker = new google.maps.Marker(markerOptions);
-                            });
-                        });
-                    }
-                }
-                $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
+        // Try the parent elements if not found
+        if (!category) {
+          var parents = $(e.target)
+            .parents()
+            .each(function () {
+              category = $(this).attr("data-category");
+              if (category) {
+                return false;
+              }
+            });
+        }
+
+        if (!category) {
+          return false;
+        }
+
+        // Get the input element
+        var input = this.$el.find("#" + category + "_input");
+
+        // Get the value of the associated input
+        var term = !item || !item.value ? input.val() : item.value;
+        var label = !item || !item.filterLabel ? null : item.filterLabel;
+        var filterDesc = !item || !item.desc ? null : item.desc;
+
+        // Check that something was actually entered
+        if (term == "" || term == " ") {
+          return false;
+        }
+
+        // Close the autocomplete box
+        if (e.type == "hoverautocompleteselect") {
+          $(input).hoverAutocomplete("close");
+        } else if ($(input).data("ui-autocomplete") != undefined) {
+          // If the autocomplete has been initialized, then close it
+          $(input).autocomplete("close");
+        }
+
+        // Get the current searchModel array for this category
+        var filtersArray = _.clone(this.searchModel.get(category));
+
+        if (typeof filtersArray == "undefined") {
+          console.error(
+            "The filter category '" +
+              category +
+              "' does not exist in the Search model. Not sending this search term.",
+          );
+          return false;
+        }
+
+        // Check if this entry is a duplicate
+        var duplicate = (function () {
+          for (var i = 0; i < filtersArray.length; i++) {
+            if (filtersArray[i].value === term) {
+              return true;
+            }
+          }
+        })();
+
+        if (duplicate) {
+          // Display a quick message
+          if ($("#duplicate-" + category + "-alert").length <= 0) {
+            $("#current-" + category + "-filters").prepend(
+              "<div class='alert alert-block' id='duplicate-' + category + '-alert'>" +
+                "You are already using that filter" +
+                "</div>",
+            );
+
+            $("#duplicate-" + category + "-alert")
+              .delay(2000)
+              .fadeOut(500, function () {
+                this.remove();
+              });
+          }
+
+          return false;
+        }
+
+        // Add the new entry to the array of current filters
+        var filter = {
+          value: term,
+          filterLabel: label,
+          label: label,
+          description: filterDesc,
+        };
+        filtersArray.push(filter);
+
+        // Replace the current array with the new one in the search model
+        this.searchModel.set(category, filtersArray);
+
+        // Show the UI filter
+        this.showFilter(category, filter, false, label);
+
+        // Clear the input
+        input.val("");
+
+        // Route to page 1
+        this.updatePageNumber(0);
+
+        // Trigger a new search
+        this.triggerSearch();
+
+        // Track this event
+        MetacatUI.analytics?.trackEvent("search", "filter, " + category, term);
+      },
+
+      // Removes a specific filter term from the searchModel
+      removeFilter: function (e) {
+        // Get the parent element that stores the filter term
+        var filterNode = $(e.target).parent();
+
+        // Find this filter's category and value
+        var category =
+            filterNode.attr("data-category") ||
+            filterNode.parent().attr("data-category"),
+          value = $(filterNode).attr("data-term");
+
+        // Remove this filter from the searchModel
+        this.searchModel.removeFromModel(category, value);
+
+        // Hide the filter from the UI
+        this.hideFilter(category, value);
+
+        // If there is an associated checkbox with this filter, uncheck it
+        var assocCheckbox,
+          checkboxes = this.$(
+            "input[type='checkbox'][data-category='" + category + "']",
+          );
+
+        //If there are more than one checkboxes in this category, match by value
+        if (checkboxes.length > 1) {
+          assocCheckbox = _.find(checkboxes, function (checkbox) {
+            return $(checkbox).val() == value;
+          });
+        }
+        //If there is only one checkbox in this category, default to it
+        else if (checkboxes.length == 1) {
+          assocCheckbox = checkboxes[0];
+        }
+
+        //If there is an associated checkbox, uncheck it
+        if (assocCheckbox) {
+          //Uncheck it
+          $(assocCheckbox).prop("checked", false);
+        }
+
+        // Route to page 1
+        this.updatePageNumber(0);
+
+        // Trigger a new search
+        this.triggerSearch();
+      },
+
+      // Clear all the currently applied filters
+      resetFilters: function () {
+        var viewRef = this;
+
+        this.allowSearch = true;
+
+        // Hide all the filters in the UI
+        $.each(this.$(".current-filter"), function () {
+          viewRef.hideEl(this);
+        });
 
-            },
+        // Hide the clear button
+        this.hideClearButton();
+
+        // Then reset the model
+        this.searchModel.clear();
+
+        //Reset the map model
+        if (this.mapModel) {
+          this.mapModel.clear();
+        }
+
+        // Reset the year slider handles
+        $("#year-range").slider("values", [
+          this.searchModel.get("yearMin"),
+          this.searchModel.get("yearMax"),
+        ]);
+        //and the year inputs
+        $("#min_year").val(this.searchModel.get("yearMin"));
+        $("#max_year").val(this.searchModel.get("yearMax"));
+
+        // Reset the checkboxes
+        $("#includes_data").prop("checked", this.searchModel.get("documents"));
+        $("#data_year").prop("checked", this.searchModel.get("dataYear"));
+        $("#publish_year").prop("checked", this.searchModel.get("pubYear"));
+        $("#is_private_data").prop(
+          "checked",
+          this.searchModel.get("isPrivate"),
+        );
+        this.listDataSources();
+
+        // Zoom out the Google Map
+        this.resetMap();
+        this.renderMap();
+
+        // Route to page 1
+        this.updatePageNumber(0);
+
+        // Trigger a new search
+        this.triggerSearch();
+      },
+
+      hideEl: function (element) {
+        // Fade out and remove the element
+        $(element).fadeOut("slow", function () {
+          $(element).remove();
+        });
+      },
+
+      // Removes a specified filter node from the DOM
+      hideFilter: function (category, value) {
+        if (!this.filters) return;
+
+        if (typeof value === "undefined") {
+          var filterNode = this.$(
+            ".current-filters[data-category='" + category + "']",
+          ).children(".current-filter");
+        } else {
+          var filterNode = this.$(
+            ".current-filters[data-category='" + category + "']",
+          ).children("[data-term='" + value + "']");
+        }
+
+        // Try finding it a different way
+        if (!filterNode || !filterNode.length) {
+          filterNode = this.$(
+            ".current-filter[data-category='" + category + "']",
+          );
+        }
+
+        // Remove the filter node from the DOM
+        this.hideEl(filterNode);
+      },
+
+      // Adds a specified filter node to the DOM
+      showFilter: function (
+        category,
+        term,
+        checkForDuplicates,
+        label,
+        options,
+      ) {
+        if (!this.filters) return;
+
+        var viewRef = this;
+
+        if (typeof term === "undefined") return false;
+
+        // Get the element to add the UI filter node to
+        // The pattern is #current-<category>-filters
+        var filterContainer = this.$el.find(
+          "#current-" + category + "-filters",
+        );
+
+        // Allow the option to only display this exact filter category and term once to the DOM
+        // Helpful when adding a filter that is not stored in the search model (for display only)
+        if (checkForDuplicates) {
+          var duplicate = false;
+
+          // Get the current terms from the DOM and check against the new term
+          filterContainer.children().each(function () {
+            if ($(this).attr("data-term") == term) {
+              duplicate = true;
+            }
+          });
+
+          // If there is a duplicate, exit without adding it
+          if (duplicate) {
+            return;
+          }
+        }
+
+        var value = null,
+          desc = null;
+
+        // See if this filter is an object and extract the filter attributes
+        if (typeof term === "object") {
+          if (typeof term.description !== "undefined") {
+            desc = term.description;
+          }
+          if (typeof term.filterLabel !== "undefined") {
+            label = term.filterLabel;
+          } else if (typeof term.label !== "undefined" && term.label) {
+            label = term.label;
+          } else {
+            label = null;
+          }
+          if (typeof term.value !== "undefined") {
+            value = term.value;
+          }
+        } else {
+          value = term;
+
+          // Find the filter label
+          if (typeof label === "undefined" || !label) {
+            // Use the filter value for the label, sans any leading # character
+            if (value.indexOf("#") > 0) {
+              label = value.substring(value.indexOf("#"));
+            }
+          }
+
+          desc = label;
+        }
+
+        var categoryLabel = this.searchModel.fieldLabels[category];
+        if (
+          typeof categoryLabel === "undefined" &&
+          category == "additionalCriteria"
+        )
+          categoryLabel = "";
+        if (typeof categoryLabel === "undefined") categoryLabel = category;
+
+        // Add a filter node to the DOM
+        var filterEl = viewRef.currentFilterTemplate({
+          category: Utilities.encodeHTML(categoryLabel),
+          value: Utilities.encodeHTML(value),
+          label: Utilities.encodeHTML(label),
+          description: Utilities.encodeHTML(desc),
+        });
 
-            /**
-             * Get the details on each tile - a list of ids and titles for each dataset contained in that tile
-             * And create an infowindow for that tile
-             */
-            addTileInfoWindows: function() {
-                // Exit if maps are not in use
-                if ((this.mode != "map") || (!gmaps)) {
-                    return false;
-                }
+        // Add the filter to the page - either replace or tack on
+        if (options && options.replace) {
+          var currentFilter = filterContainer.find(".current-filter");
+          if (currentFilter.length > 0) {
+            currentFilter.replaceWith(filterEl);
+          } else {
+            filterContainer.prepend(filterEl);
+          }
+        } else {
+          filterContainer.prepend(filterEl);
+        }
+
+        // Tooltips and Popovers
+        $(filterEl).tooltip({
+          delay: {
+            show: 800,
+          },
+        });
 
-                // Clone the Search model
-                var searchModelClone = this.searchModel.clone(),
-                    geohashLevel = this.mapModel.get("tileGeohashLevel"),
-                    geohashName = "geohash_" + geohashLevel,
-                    viewRef = this,
-                    infoWindows = [];
-
-                // Change the geohash filter to match our tiles
-                searchModelClone.set("geohashLevel", geohashLevel);
-                searchModelClone.set("geohashes", this.tileGeohashes);
-
-                // Now run a query to get a list of documents that are represented by our tiles
-                var query = "q=" + searchModelClone.getQuery() +
-                    "&fl=id,title,geohash_9," + geohashName +
-                    "&rows=1000" +
-                    "&wt=json";
-
-                var requestSettings = {
-                    url: MetacatUI.appModel.get("queryServiceUrl") + query,
-                    success: function(data, textStatus, xhr) {
-                        // Make an infoWindow for each doc
-                        var docs = data.response.docs;
-
-                        // For each tile, loop through the docs to find which ones to include in its infoWindow
-                        _.each(viewRef.tiles, function(tile, key, list) {
-
-                            var infoWindowContent = "";
-
-                            _.each(docs, function(doc, key, list) {
-
-                              var docGeohashes = doc[geohashName];
-
-                              if(docGeohashes){
-                                // Is this document in this tile?
-                                for (var i = 0; i < docGeohashes.length; i++) {
-                                    if (docGeohashes[i] == tile.geohash) {
-                                        // Add this doc to the infoWindow content
-                                        infoWindowContent += "<a href='" + MetacatUI.root + "/view/" + encodeURIComponent(doc.id) + "'>" + doc.title + "</a> (" + doc.id + ") <br/>"
-                                        break;
-                                    }
-                                }
-                              }
-                            });
-
-                            // The center of the tile
-                            var decodedGeohash = nGeohash.decode(tile.geohash),
-                                tileCenter = new google.maps.LatLng(decodedGeohash.latitude, decodedGeohash.longitude);
-
-                            // The infowindow
-                            var infoWindow = new gmaps.InfoWindow({
-                                content: "<div class='gmaps-infowindow'>" +
-                                    "<h4> Datasets located here </h4>" +
-                                    "<p>" + infoWindowContent + "</p>" +
-                                    "</div>",
-                                isOpen: false,
-                                disableAutoPan: false,
-                                maxWidth: 250,
-                                position: tileCenter
-                            });
-
-                            viewRef.tileInfoWindows.push(infoWindow);
-
-                            // Zoom in when the tile is clicked on
-                            gmaps.event.addListener(tile.shape, "click", function(clickEvent) {
-
-                                //--- We are at max zoom, display an infowindow ----//
-                                if (this.mapModel.isMaxZoom(viewRef.map)) {
-
-                                    // Find the infowindow that belongs to this tile in the view
-                                    infoWindow.open(viewRef.map);
-                                    infoWindow.isOpen = true;
-
-                                    // Close all other infowindows
-                                    viewRef.closeInfoWindows(infoWindow);
-                                }
-
-                                //------ We are not at max zoom, so zoom into this tile ----//
-                                else {
-                                    // Change the center
-                                    viewRef.map.panTo(clickEvent.latLng);
-
-                                    // Get this tile's bounds
-                                    var bounds = tile.shape.getBounds();
-
-                                    // Change the zoom
-                                    viewRef.map.fitBounds(bounds);
-                                }
-                            });
-
-                            // Close the infowindow upon any click on the map
-                            gmaps.event.addListener(viewRef.map, "click", function() {
-                                infoWindow.close();
-                                infoWindow.isOpen = false;
-                            });
-
-                            infoWindows[tile.geohash] = infoWindow;
-                        });
-
-                        viewRef.infoWindows = infoWindows;
-                    }
-                }
-                $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-            },
+        return;
+      },
+
+      /*
+       * Get the member node list from the model and list the members in the filter list
+       */
+      listDataSources: function () {
+        if (!this.filters) return;
+
+        if (MetacatUI.nodeModel.get("members").length < 1) return;
+
+        // Get the member nodes
+        var members = _.sortBy(
+          MetacatUI.nodeModel.get("members"),
+          function (m) {
+            if (m.name) {
+              return m.name.toLowerCase();
+            } else {
+              return "";
+            }
+          },
+        );
+        var filteredMembers = _.reject(members, function (m) {
+          return m.status != "operational";
+        });
 
-            /**
-             * Iterate over each infowindow that we have stored in the view and close it.
-             * Pass an infoWindow object to this function to keep that infoWindow open/skip it
-             * @param {infoWindow} - An infoWindow to keep open
-             */
-            closeInfoWindows: function(except) {
-                var infoWindowLists = [this.markerInfoWindows, this.tileInfoWindows];
-
-                _.each(infoWindowLists, function(infoWindows, key, list) {
-                    // Iterate over all the marker infowindows and close all of them except for this one
-                    for (var i = 0; i < infoWindows.length; i++) {
-                        if ((infoWindows[i].isOpen) && (infoWindows[i] != except)) {
-                            // Close this info window and stop looking, since only one of each kind should be open anyway
-                            infoWindows[i].close();
-                            infoWindows[i].isOpen = false;
-                            i = infoWindows.length;
-                        }
-                    }
-                });
+        // Get the current search filters for data source
+        var currentFilters = this.searchModel.get("dataSource");
+
+        // Create an HTML list
+        var listMax = 4,
+          numHidden = filteredMembers.length - listMax,
+          list = $(document.createElement("ul")).addClass("checkbox-list");
+
+        // Add a checkbox and label for each member node in the node model
+        _.each(filteredMembers, function (member, i) {
+          var listItem = document.createElement("li"),
+            input = document.createElement("input"),
+            label = document.createElement("label");
+
+          // If this member node is already a data source filter, then the checkbox is checked
+          var checked = _.findWhere(currentFilters, {
+            value: member.identifier,
+          })
+            ? true
+            : false;
+
+          // Create a textual label for this data source
+          $(label)
+            .addClass("ellipsis")
+            .attr("for", member.identifier)
+            .html(member.name);
+
+          // Create a checkbox for this data source
+          $(input)
+            .addClass("filter")
+            .attr("type", "checkbox")
+            .attr("data-category", "dataSource")
+            .attr("id", member.identifier)
+            .attr("name", "dataSource")
+            .attr("value", member.identifier)
+            .attr("data-label", member.name)
+            .attr("data-description", member.description);
+
+          // Add tooltips to the label element
+          $(label).tooltip({
+            placement: "top",
+            delay: {
+              show: 900,
             },
+            trigger: "hover",
+            viewport: "#sidebar",
+            title: member.description,
+          });
+
+          // If this data source is already selected as a filter (from the search model), then check the checkbox
+          if (checked) $(input).prop("checked", "checked");
+
+          // Collapse some of the checkboxes and labels after a certain amount
+          if (i > listMax - 1) {
+            $(listItem).addClass("hidden");
+          }
+
+          // Insert a "More" link after a certain amount to enable users to expand the list
+          if (i == listMax) {
+            var moreLink = document.createElement("a");
+            $(moreLink)
+              .html("Show " + numHidden + " more")
+              .addClass("more-link pointer toggle-list")
+              .append(
+                $(document.createElement("i")).addClass("icon icon-expand-alt"),
+              );
+            $(list).append(moreLink);
+          }
 
-            /**
-             * Remove all the tiles and text from the map
-             **/
-            removeTiles: function() {
-                // Exit if maps are not in use
-                if ((this.mode != "map") || (!gmaps)) {
-                    return false;
-                }
-
-                // Remove the tile from the map
-                _.each(this.tiles, function(tile, key, list) {
-                    if (tile.shape) tile.shape.setMap(null);
-                    if (tile.text) tile.text.setMap(null);
-                });
+          // Add this checkbox and laebl to the list
+          $(listItem).append(input).append(label);
+          $(list).append(listItem);
+        });
 
-                // Reset the tile storage in the view
-                this.tiles = [];
-                this.tileGeohashes = [];
-                this.tileInfoWindows = [];
-            },
+        if (numHidden > 0) {
+          var lessLink = document.createElement("a");
+          $(lessLink)
+            .html("Collapse member nodes")
+            .addClass("less-link toggle-list pointer hidden")
+            .append(
+              $(document.createElement("i")).addClass("icon icon-collapse-alt"),
+            );
+
+          $(list).append(lessLink);
+        }
+
+        // Add the list of checkboxes to the placeholder
+        var container = $(".member-nodes-placeholder");
+        $(container).html(list);
+        $(".tooltip-this").tooltip();
+      },
+
+      resetDataSourceList: function () {
+        if (!this.filters) return;
+
+        // Reset the Member Nodes checkboxes
+        var mnFilterContainer = $("#member-nodes-container"),
+          defaultMNs = this.searchModel.get("dataSource");
+
+        // Make sure the member node filter exists
+        if (!mnFilterContainer || mnFilterContainer.length == 0) return false;
+        if (typeof defaultMNs === "undefined" || !defaultMNs) return false;
+
+        // Reset each member node checkbox
+        var boxes = $(mnFilterContainer).find(".filter").prop("checked", false);
+
+        // Check the member node checkboxes that are defaults in the search model
+        _.each(defaultMNs, function (member, i) {
+          var value = null;
+
+          // Allow for string search model filter values and object filter values
+          if (typeof member !== "object" && member) value = member;
+          else if (typeof member.value === "undefined" || !member.value)
+            value = "";
+          else value = member.value;
+
+          $(mnFilterContainer)
+            .find("checkbox[value='" + value + "']")
+            .prop("checked", true);
+        });
 
-            /**
-             * Iterate over all the markers in the view and remove them from the map and view
-             */
-            removeMarkers: function() {
-                // Exit if maps are not in use
-                if ((this.mode != "map") || (!gmaps)) {
-                    return false;
-                }
+        return true;
+      },
+
+      toggleList: function (e) {
+        if (!this.filters) return;
 
-                // Remove the marker from the map
-                _.each(this.markers, function(marker, key, list) {
-                    marker.marker.setMap(null);
+        var link = e.target,
+          controls = $(link).parents("ul").find(".toggle-list"),
+          list = $(link).parents("ul"),
+          isHidden = !list.find(".more-link").is(".hidden");
+
+        // Hide/Show the list
+        if (isHidden) {
+          list.children("li").slideDown();
+        } else {
+          list.children("li.hidden").slideUp();
+        }
+
+        // Hide/Show the control links
+        controls.toggleClass("hidden");
+      },
+
+      // add additional criteria to the search model based on link click
+      additionalCriteria: function (e) {
+        // Get the clicked node
+        var targetNode = $(e.target);
+
+        // If this additional criteria is already applied, remove it
+        if (targetNode.hasClass("active")) {
+          this.removeAdditionalCriteria(e);
+          return false;
+        }
+
+        // Get the filter criteria
+        var term = targetNode.attr("data-term");
+
+        // Find this element's category in the data-category attribute
+        var category = targetNode.attr("data-category");
+
+        // style the selection
+        $(".keyword-search-link").removeClass("active");
+        $(".keyword-search-link").parent().removeClass("active");
+        targetNode.addClass("active");
+        targetNode.parent().addClass("active");
+
+        // Add this criteria to the search model
+        this.searchModel.set(category, [term]);
+
+        // Trigger the search
+        this.triggerSearch();
+
+        // prevent default action of click
+        return false;
+      },
+
+      removeAdditionalCriteria: function (e) {
+        // Get the clicked node
+        var targetNode = $(e.target);
+
+        // Reference to model
+        var model = this.searchModel;
+
+        // remove the styling
+        $(".keyword-search-link").removeClass("active");
+        $(".keyword-search-link").parent().removeClass("active");
+
+        // Get the term
+        var term = targetNode.attr("data-term");
+
+        // Get the current search model additional criteria
+        var current = this.searchModel.get("additionalCriteria");
+        // If this term is in the current search model (should be)...
+        if (_.contains(current, term)) {
+          //then remove it
+          var newTerms = _.without(current, term);
+          model.set("additionalCriteria", newTerms);
+        }
+
+        // Route to page 1
+        this.updatePageNumber(0);
+
+        // Trigger a new search
+        this.triggerSearch();
+      },
+
+      // Get the facet counts
+      getAutocompletes: function (e) {
+        if (!e) return;
+
+        // Get the text input to determine the filter type
+        var input = $(e.target),
+          category = input.attr("data-category");
+
+        if (!this.filters || !category) return;
+
+        var viewRef = this;
+
+        // Create the facet query by using our current search query
+        var facetQuery =
+          "q=" +
+          this.searchResults.currentquery +
+          "&rows=0" +
+          this.searchModel.getFacetQuery(category) +
+          "&wt=json&";
+
+        // If we've cached these filter results, then use the cache instead of sending a new request
+        if (!MetacatUI.appSearchModel.autocompleteCache)
+          MetacatUI.appSearchModel.autocompleteCache = {};
+        else if (MetacatUI.appSearchModel.autocompleteCache[facetQuery]) {
+          this.setupAutocomplete(
+            input,
+            MetacatUI.appSearchModel.autocompleteCache[facetQuery],
+          );
+          return;
+        }
+
+        // Get the facet counts for the autocomplete
+        var requestSettings = {
+          url: MetacatUI.appModel.get("queryServiceUrl") + facetQuery,
+          type: "GET",
+          dataType: "json",
+          success: function (data, textStatus, xhr) {
+            var suggestions = [],
+              facetLimit = 999;
+
+            // Get all the facet counts
+            _.each(category.split(","), function (c) {
+              if (typeof c == "string") c = [c];
+              _.each(c, function (thisCategory) {
+                // Get the field name(s)
+                var fieldNames =
+                  MetacatUI.appSearchModel.facetNameMap[thisCategory];
+                if (typeof fieldNames == "string") fieldNames = [fieldNames];
+
+                // Get the facet counts
+                _.each(fieldNames, function (fieldName) {
+                  suggestions.push(data.facet_counts.facet_fields[fieldName]);
                 });
+              });
+            });
+            suggestions = _.flatten(suggestions);
+
+            // Format the suggestions
+            var rankedSuggestions = new Array();
+            for (
+              var i = 0;
+              i < Math.min(suggestions.length - 1, facetLimit);
+              i += 2
+            ) {
+              //The label is the item value
+              var label = suggestions[i];
+
+              //For all categories except the 'all' category, display the facet count
+              if (category != "all") {
+                label += " (" + suggestions[i + 1] + ")";
+              }
 
-                // Reset the marker storage in the view
-                this.markers = [];
-                this.markerGeohashes = [];
-                this.markerInfoWindows = [];
-            },
-
-
-            /*
-             * ==================================================================================================
-             *                                             ADDING RESULTS
-             * ==================================================================================================
-             */
-
-            /** Add all items in the **SearchResults** collection
-             * This loads the first 25, then waits for the map to be
-             * fully loaded and then loads the remaining items.
-             * Without this delay, the app waits until all records are processed
-             */
-            addAll: function() {
-                // After the map is done loading, then load the rest of the results into the list
-                if (this.ready) this.renderAll();
-                else {
-                    var viewRef = this;
-                    var intervalID = setInterval(function() {
-                        if (viewRef.ready) {
-                            clearInterval(intervalID);
-                            viewRef.renderAll();
-                        }
-                    }, 500);
-                }
-
-                // After all the results are loaded, query for our facet counts in the background
-                //this.getAutocompletes();
-            },
+              //Push the autocomplete item to the array
+              rankedSuggestions.push({
+                value: suggestions[i],
+                label: label,
+              });
+            }
 
-            renderAll: function() {
-                // do this first to indicate coming results
-                this.updateStats();
+            // Save these facets in the app so we don't have to send another query
+            MetacatUI.appSearchModel.autocompleteCache[facetQuery] =
+              rankedSuggestions;
+
+            // Now setup the actual autocomplete menu
+            viewRef.setupAutocomplete(input, rankedSuggestions);
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      setupAutocomplete: function (input, rankedSuggestions) {
+        var viewRef = this;
+
+        //Override the _renderItem() function which renders a single autocomplete item.
+        // We want to use the 'title' HTML attribute on each item.
+        // This method must create a new <li> element, append it to the menu, and return it.
+        $.widget("custom.autocomplete", $.ui.autocomplete, {
+          _renderItem: function (ul, item) {
+            return $(document.createElement("li"))
+              .attr("title", item.label)
+              .append(item.label)
+              .appendTo(ul);
+          },
+        });
+        input.autocomplete({
+          source: function (request, response) {
+            var term = $.ui.autocomplete.escapeRegex(request.term),
+              startsWithMatcher = new RegExp("^" + term, "i"),
+              startsWith = $.grep(rankedSuggestions, function (value) {
+                return startsWithMatcher.test(
+                  value.label || value.value || value,
+                );
+              }),
+              containsMatcher = new RegExp(term, "i"),
+              contains = $.grep(rankedSuggestions, function (value) {
+                return (
+                  $.inArray(value, startsWith) < 0 &&
+                  containsMatcher.test(value.label || value.value || value)
+                );
+              });
+
+            response(startsWith.concat(contains));
+          },
+          select: function (event, ui) {
+            // set the text field
+            input.val(ui.item.value);
+            // add to the filter immediately
+            viewRef.updateTextFilters(event, ui.item);
+            // prevent default action
+            return false;
+          },
+          position: {
+            my: "left top",
+            at: "left bottom",
+            collision: "flipfit",
+          },
+        });
+      },
+
+      hideClearButton: function () {
+        if (!this.filters) return;
+
+        // Hide the current filters panel
+        this.$(".current-filters-container").slideUp();
+
+        // Hide the reset button
+        $("#clear-all").addClass("hidden");
+        this.setAutoHeight();
+      },
+
+      showClearButton: function () {
+        if (!this.filters) return;
+
+        // Show the current filters panel
+        if (
+          _.difference(
+            this.searchModel.getCurrentFilters(),
+            this.searchModel.spatialFilters,
+          ).length > 0
+        ) {
+          this.$(".current-filters-container").slideDown();
+        }
+
+        // Show the reset button
+        $("#clear-all").removeClass("hidden");
+        this.setAutoHeight();
+      },
+
+      /*
+       * ==================================================================================================
+       *                                             NAVIGATING THE UI
+       * ==================================================================================================
+       */
+      // Update all the statistics throughout the page
+      updateStats: function () {
+        if (this.searchResults.header != null) {
+          this.$statcounts = this.$("#statcounts");
+          this.$statcounts.html(
+            this.statsTemplate({
+              start: this.searchResults.header.get("start") + 1,
+              end:
+                this.searchResults.header.get("start") +
+                this.searchResults.length,
+              numFound: this.searchResults.header.get("numFound"),
+            }),
+          );
+        }
+
+        // piggy back here
+        this.updatePager();
+      },
+
+      updatePager: function () {
+        if (this.searchResults.header != null) {
+          var pageCount = Math.ceil(
+            this.searchResults.header.get("numFound") /
+              this.searchResults.header.get("rows"),
+          );
+
+          // If no results were found, display a message instead of the list and clear the pagination.
+          if (pageCount == 0) {
+            this.$results.html(
+              "<p id='no-results-found'>No results found.</p>",
+            );
+
+            this.$("#resultspager").html("");
+            this.$(".resultspager").html("");
+          }
+          // Do not display the pagination if there is only one page
+          else if (pageCount == 1) {
+            this.$("#resultspager").html("");
+            this.$(".resultspager").html("");
+          } else {
+            var pages = new Array(pageCount);
+
+            // mark current page correctly, avoid NaN
+            var currentPage = -1;
+            try {
+              currentPage = Math.floor(
+                (this.searchResults.header.get("start") /
+                  this.searchResults.header.get("numFound")) *
+                  pageCount,
+              );
+            } catch (ex) {
+              console.log("Exception when calculating pages:" + ex.message);
+            }
 
-                // Remove all the existing tiles on the map
-                this.removeTiles();
-                this.removeMarkers();
+            // Populate the pagination element in the UI
+            this.$(".resultspager").html(
+              this.pagerTemplate({
+                pages: pages,
+                currentPage: currentPage,
+              }),
+            );
+            this.$("#resultspager").html(
+              this.pagerTemplate({
+                pages: pages,
+                currentPage: currentPage,
+              }),
+            );
+          }
+        }
+      },
+
+      updatePageNumber: function (page) {
+        MetacatUI.appModel.set("page", page);
+
+        if (!this.isSubView) {
+          var route = Backbone.history.fragment,
+            subroutePos = route.indexOf("/page/"),
+            newPage = parseInt(page) + 1;
+
+          //replace the last number with the new one
+          if (page > 0 && subroutePos > -1) {
+            route = route.replace(/\d+$/, newPage);
+          } else if (page > 0) {
+            route += "/page/" + newPage;
+          } else if (subroutePos >= 0) {
+            route = route.substring(0, subroutePos);
+          }
+
+          MetacatUI.uiRouter.navigate(route);
+        }
+      },
+
+      // Next page of results
+      nextpage: function () {
+        this.loading();
+        this.searchResults.nextpage();
+        this.$resultsview.show();
+        this.updateStats();
+
+        var page = MetacatUI.appModel.get("page");
+        page++;
+        this.updatePageNumber(page);
+      },
+
+      // Previous page of results
+      prevpage: function () {
+        this.loading();
+        this.searchResults.prevpage();
+        this.$resultsview.show();
+        this.updateStats();
+
+        var page = MetacatUI.appModel.get("page");
+        page--;
+        this.updatePageNumber(page);
+      },
+
+      navigateToPage: function (event) {
+        var page = $(event.target).attr("page");
+        this.showPage(page);
+      },
+
+      showPage: function (page) {
+        this.loading();
+        this.searchResults.toPage(page);
+        this.$resultsview.show();
+        this.updateStats();
+        this.updatePageNumber(page);
+        this.updateYearRange();
+      },
+
+      /*
+       * ==================================================================================================
+       *                                             THE MAP
+       * ==================================================================================================
+       */
+      renderMap: function () {
+        // If gmaps isn't enabled or loaded with an error, use list mode
+        if (!gmaps || this.mode == "list") {
+          this.ready = true;
+          this.mode = "list";
+          return;
+        }
+
+        if (this.isSubView) {
+          this.$el.addClass("mapMode");
+        } else {
+          $("body").addClass("mapMode");
+        }
+
+        // Get the map options and create the map
+        gmaps.visualRefresh = true;
+        var mapOptions = this.mapModel.get("mapOptions");
+        var defaultZoom = mapOptions.zoom;
+        $("#map-container").append("<div id='map-canvas'></div>");
+        this.map = new gmaps.Map($("#map-canvas")[0], mapOptions);
+        this.mapModel.set("map", this.map);
+        this.hasZoomed = false;
+        this.hasDragged = false;
+
+        // Hide the map filter toggle element
+        this.$(this.mapFilterToggle).hide();
+
+        // Store references
+        var mapRef = this.map;
+        var viewRef = this;
+
+        google.maps.event.addListener(mapRef, "zoom_changed", function () {
+          // If the map is zoomed in further than the default zoom level,
+          // than we want to mark the map as zoomed in
+          if (viewRef.map.getZoom() > defaultZoom) {
+            viewRef.hasZoomed = true;
+          }
+          //If we are at the default zoom level or higher, than do not mark the map
+          // as zoomed in
+          else {
+            viewRef.hasZoomed = false;
+          }
+        });
 
-                // Remove the loading class and styling
-                this.$results.removeClass("loading");
+        google.maps.event.addListener(mapRef, "dragend", function () {
+          viewRef.hasDragged = true;
+        });
 
-                // If there are no results, display so
-                var numFound = this.searchResults.length;
-                if (numFound == 0) {
+        google.maps.event.addListener(mapRef, "idle", function () {
+          // Remove all markers from the map
+          for (var i = 0; i < viewRef.resultMarkers.length; i++) {
+            viewRef.resultMarkers[i].setMap(null);
+          }
+          viewRef.resultMarkers = new Array();
+
+          //Check if the user has interacted with the map just now, and if so, we
+          // want to alter the geohash filter (changing the geohash values or resetting it completely)
+          var alterGeohashFilter =
+            viewRef.allowSearch || viewRef.hasZoomed || viewRef.hasDragged;
+          if (!alterGeohashFilter) {
+            return;
+          }
+
+          //Determine if the map needs to be recentered. The map only needs to be
+          // recentered if it is not at the default lat,long center point AND it
+          // is not zoomed in or dragged to a new center point
+          var setGeohashFilter =
+            viewRef.hasZoomed && viewRef.isMapFilterEnabled();
+
+          //If we are using the geohash filter defined by this map, then
+          // apply the filter and trigger a new search
+          if (setGeohashFilter) {
+            viewRef.$(viewRef.mapFilterToggle).show();
+
+            // Get the Google map bounding box
+            var boundingBox = mapRef.getBounds();
+
+            // Set the search model spatial filters
+            // Encode the Google Map bounding box into geohash
+            var north = boundingBox.getNorthEast().lat(),
+              west = boundingBox.getSouthWest().lng(),
+              south = boundingBox.getSouthWest().lat(),
+              east = boundingBox.getNorthEast().lng();
+
+            viewRef.searchModel.set("north", north);
+            viewRef.searchModel.set("west", west);
+            viewRef.searchModel.set("south", south);
+            viewRef.searchModel.set("east", east);
+
+            // Save the center position and zoom level of the map
+            viewRef.mapModel.get("mapOptions").center = mapRef.getCenter();
+            viewRef.mapModel.get("mapOptions").zoom = mapRef.getZoom();
+
+            // Determine the precision of geohashes to search for
+            var zoom = mapRef.getZoom();
+
+            var precision = viewRef.mapModel.getSearchPrecision(zoom);
+
+            // Get all the geohash tiles contained in the map bounds
+            var geohashBBoxes = nGeohash.bboxes(
+              south,
+              west,
+              north,
+              east,
+              precision,
+            );
+
+            // Save our geohash search settings
+            viewRef.searchModel.set("geohashes", geohashBBoxes);
+            viewRef.searchModel.set("geohashLevel", precision);
+
+            //Start back at page 0
+            MetacatUI.appModel.set("page", 0);
+
+            //Mark the view as ready to start a search
+            viewRef.ready = true;
+
+            // Trigger a new search
+            viewRef.triggerSearch();
+
+            viewRef.allowSearch = false;
+          } else {
+            //Reset the map filter
+            viewRef.resetMap();
+
+            //Start back at page 0
+            MetacatUI.appModel.set("page", 0);
+
+            //Mark the view as ready to start a search
+            viewRef.ready = true;
+
+            // Trigger a new search
+            viewRef.triggerSearch();
+
+            viewRef.allowSearch = false;
+
+            return;
+          }
+        });
+      },
 
-                    // Add a No Results Found message
-                    this.$results.html("<p id='no-results-found'>No results found.</p>");
+      // Resets the model and view settings related to the map
+      resetMap: function () {
+        if (!gmaps) {
+          return;
+        }
 
-                    // Remove the loading styles from the map
-                    if (gmaps && this.mapModel) {
-                        $("#map-container").removeClass("loading");
-                    }
+        // First reset the model
+        // The categories pertaining to the map
+        var categories = ["east", "west", "north", "south"];
 
-                    if (MetacatUI.theme == "arctic") {
-                        // When we get new results, check if the user is searching for their own datasets and display a message
-                        if ((MetacatUI.appView.dataCatalogView && MetacatUI.appView.dataCatalogView.searchModel.getQuery() == MetacatUI.appUserModel.get("searchModel").getQuery()) && !MetacatUI.appSearchResults.length) {
-                            $("#no-results-found").after("<h3>Where are my data sets?</h3><p>If you are a previous ACADIS Gateway user, " +
-                                "you will need to take additional steps to access your data sets in the new NSF Arctic Data Center." +
-                                "<a href='mailto:support@arcticdata.io'>Send us a message at support@arcticdata.io</a> with your old ACADIS " +
-                                "Gateway username and your ORCID identifier (" + MetacatUI.appUserModel.get("username") + "), we will help.</p>");
-                        }
-                    }
-                    return;
-                }
+        // Loop through each and remove the filters from the model
+        for (var i = 0; i < categories.length; i++) {
+          this.searchModel.set(categories[i], null);
+        }
 
-                // Clear the results list before we start adding new rows
-                this.$results.html("");
+        // Reset the map settings
+        this.searchModel.resetGeohash();
+        this.mapModel.set("mapOptions", this.mapModel.defaults().mapOptions);
 
-                //--First map all the results--
-                if (gmaps && this.mapModel) {
-                    // Draw all the tiles on the map to represent the datasets
-                    this.drawTiles();
+        this.allowSearch = false;
+      },
 
-                    // Remove the loading styles from the map
-                    $("#map-container").removeClass("loading");
-                }
+      isMapFilterEnabled: function () {
+        var toggleInput = this.$("input" + this.mapFilterToggle);
+        if (typeof toggleInput === "undefined" || !toggleInput) return;
 
-                var pid_list = new Array();
+        return $(toggleInput).prop("checked");
+      },
 
-                //--- Add all the results to the list ---
-                for (i = 0; i < this.searchResults.length; i++) {
-                    pid_list.push(this.searchResults.models[i].get("id"));
-                };
+      toggleMapFilter: function (e, a) {
+        var toggleInput = this.$("input" + this.mapFilterToggle);
+        if (typeof toggleInput === "undefined" || !toggleInput) return;
 
-                if (MetacatUI.appModel.get("displayDatasetMetrics")) {
-                    var metricsModel = new MetricsModel({
-                        pid_list: pid_list,
-                        type: "catalog"
-                    });
-                    metricsModel.fetch();
-                    this.metricsModel = metricsModel;
-                }
+        var isOn = $(toggleInput).prop("checked");
 
-                //--- Add all the results to the list ---
-                for (i = 0; i < this.searchResults.length; i++) {
-                    var element = this.searchResults.models[i];
-                    if (typeof element !== "undefined") this.addOne(element, this.metricsModel);
-                };
+        // If the user clicked on the label, then change the checkbox for them
+        if (e.target.tagName != "INPUT") {
+          isOn = !isOn;
+          toggleInput.prop("checked", isOn);
+        }
 
-                // Initialize any tooltips within the result item
-                $(".tooltip-this").tooltip();
-                $(".popover-this").popover();
+        google.maps.event.trigger(this.mapModel.get("map"), "idle");
 
-                // Set the autoheight
-                this.setAutoHeight();
-            },
+        // Track this event
+        MetacatUI.analytics?.trackEvent("map", isOn ? "on" : "off");
+      },
 
-            /**
-             * Add a single SolrResult item to the list by creating a view for it and appending its element to the DOM.
+      /**
+             * Show the marker, infoWindow, and bounding coordinates polygon on
+             the map when the user hovers on the marker icon in the result list
+             * @param {Event} e
              */
-            addOne: function(result) {
-                // Get the view and package service URL's
-                this.$view_service = MetacatUI.appModel.get("viewServiceUrl");
-                this.$package_service = MetacatUI.appModel.get("packageServiceUrl");
-                result.set({
-                    view_service: this.$view_service,
-                    package_service: this.$package_service
-                });
-
-                var view = new SearchResultView({
-                  model: result,
-                  metricsModel: this.metricsModel
-                });
-
-                // Add this item to the list
-                this.$results.append(view.render().el);
-
-                // map it
-                if (gmaps && this.mapModel && (typeof result.get("geohash_9") != "undefined") && (result.get("geohash_9") != null)) {
-                    var title = result.get("title");
-
-                    for (var i = 0; i < result.get("geohash_9").length; i++) {
-                        var centerGeohash = result.get("geohash_9")[i],
-                            decodedGeohash = nGeohash.decode(centerGeohash),
-                            position = new google.maps.LatLng(decodedGeohash.latitude, decodedGeohash.longitude),
-                            marker = new gmaps.Marker({
-                                position: position,
-                                icon: this.mapModel.get("markerImage"),
-                                zIndex: 99999
-                            });
-                    }
-                }
-            },
-
-            /**
-            * When the SearchResults collection has an error getting the results,
-            * show an error message instead of search results
-            * @param {SolrResult} model
-            * @param {XMLHttpRequest.response} response
-            */
-            showError: function(model, response){
-
-              var errorMessage = "";
-              var statusCode = response.status;
-
-              if(!statusCode){
-                  statusCode = parseInt(response.statusText)
+      showResultOnMap: function (e) {
+        // Exit if maps are not in use
+        if (this.mode != "map" || !gmaps) {
+          return false;
+        }
+
+        // Get the attributes about this dataset
+        var resultRow = e.target,
+          id = $(resultRow).attr("data-id");
+        // The mouseover event might be triggered by a nested element, so loop through the parents to find the id
+        if (typeof id == "undefined") {
+          $(resultRow)
+            .parents()
+            .each(function () {
+              if (typeof $(this).attr("data-id") != "undefined") {
+                id = $(this).attr("data-id");
+                resultRow = this;
               }
-
-              if(statusCode == 500 && this.solrError500Message){
-                errorMessage = this.solrError500Message;
-              } else {
-                try{
-                    errorMessage = $(response.responseText).text();
-                  }
-                  catch(e){
-                    try{
-                      errorMessage = JSON.parse(response.responseText).error.msg;
-                    }
-                    catch(e){
-                      errorMessage = "";
-                    }
-                  }
-                  finally{
-                    if( typeof errorMessage == "string" && errorMessage.length ){
-                      errorMessage = "<p>Error details: " + errorMessage + "</p>";
-                    }
-                  }
+            });
+        }
+
+        // Find the tile for this data set and highlight it on the map
+        var resultGeohashes = this.searchResults
+          .findWhere({
+            id: id,
+          })
+          .get("geohash_9");
+        for (var i = 0; i < resultGeohashes.length; i++) {
+          var thisGeohash = resultGeohashes[i],
+            latLong = nGeohash.decode(thisGeohash),
+            position = new google.maps.LatLng(
+              latLong.latitude,
+              latLong.longitude,
+            ),
+            containingTileGeohash = _.find(this.tileGeohashes, function (g) {
+              return thisGeohash.indexOf(g) == 0;
+            }),
+            containingTile = _.findWhere(this.tiles, {
+              geohash: containingTileGeohash,
+            });
+
+          // If this is a geohash for a georegion outside the map, do not highlight a tile or display a marker
+          if (typeof containingTile === "undefined") continue;
+
+          this.highlightTile(containingTile);
+
+          // Set up the options for each marker
+          var markerOptions = {
+            position: position,
+            icon: this.mapModel.get("markerImage"),
+            zIndex: 99999,
+            map: this.map,
+          };
+
+          // Create the marker and add to the map
+          var marker = new google.maps.Marker(markerOptions);
+
+          this.resultMarkers.push(marker);
+        }
+      },
+
+      /**
+             * Hide the marker, infoWindow, and bounding coordinates polygon on
+             the map when the user stops hovering on the marker icon in the result list
+             * @param {Event} e - The event that brought us to this function
+             */
+      hideResultOnMap: function (e) {
+        // Exit if maps are not in use
+        if (this.mode != "map" || !gmaps) {
+          return false;
+        }
+
+        // Get the attributes about this dataset
+        var resultRow = e.target,
+          id = $(resultRow).attr("data-id");
+        // The mouseover event might be triggered by a nested element, so loop through the parents to find the id
+        if (typeof id == "undefined") {
+          $(e.target)
+            .parents()
+            .each(function () {
+              if (typeof $(this).attr("data-id") != "undefined") {
+                id = $(this).attr("data-id");
+                resultRow = this;
               }
-
-              MetacatUI.appView.showAlert(
-                "<h4><i class='icon icon-frown'></i>" + this.solrErrorTitle + ".</h4>" + errorMessage,
-                "alert-error",
-                this.$results
+            });
+        }
+
+        // Get the map tile for this result and un-highlight it
+        var resultGeohashes = this.searchResults
+          .findWhere({
+            id: id,
+          })
+          .get("geohash_9");
+        for (var i = 0; i < resultGeohashes.length; i++) {
+          var thisGeohash = resultGeohashes[i],
+            containingTileGeohash = _.find(this.tileGeohashes, function (g) {
+              return thisGeohash.indexOf(g) == 0;
+            }),
+            containingTile = _.findWhere(this.tiles, {
+              geohash: containingTileGeohash,
+            });
+
+          // If this is a geohash for a georegion outside the map, do not unhighlight a tile
+          if (typeof containingTile === "undefined") continue;
+
+          // Unhighlight the tile
+          this.unhighlightTile(containingTile);
+        }
+
+        // Remove all markers from the map
+        _.each(this.resultMarkers, function (marker) {
+          marker.setMap(null);
+        });
+        this.resultMarkers = new Array();
+      },
+
+      /**
+       * Create a tile for each geohash facet. A separate tile label is added to the map with the count of the facet.
+       **/
+      drawTiles: function () {
+        // Exit if maps are not in use
+        if (this.mode != "map" || !gmaps) {
+          return false;
+        }
+
+        TextOverlay.prototype = new google.maps.OverlayView();
+
+        function TextOverlay(options) {
+          // Now initialize all properties.
+          this.bounds_ = options.bounds;
+          this.map_ = options.map;
+          this.text = options.text;
+          this.color = options.color;
+
+          var length = options.text.toString().length;
+          if (length == 1) this.width = 8;
+          else if (length == 2) this.width = 17;
+          else if (length == 3) this.width = 25;
+          else if (length == 4) this.width = 32;
+          else if (length == 5) this.width = 40;
+
+          // We define a property to hold the image's div. We'll
+          // actually create this div upon receipt of the onAdd()
+          // method so we'll leave it null for now.
+          this.div_ = null;
+
+          // Explicitly call setMap on this overlay
+          this.setMap(options.map);
+        }
+
+        TextOverlay.prototype.onAdd = function () {
+          // Create the DIV and set some basic attributes.
+          var div = document.createElement("div");
+          div.style.color = this.color;
+          div.style.fontSize = "15px";
+          div.style.position = "absolute";
+          div.style.zIndex = "999";
+          div.style.fontWeight = "bold";
+
+          // Create an IMG element and attach it to the DIV.
+          div.innerHTML = this.text;
+
+          // Set the overlay's div_ property to this DIV
+          this.div_ = div;
+
+          // We add an overlay to a map via one of the map's panes.
+          // We'll add this overlay to the overlayLayer pane.
+          var panes = this.getPanes();
+          panes.overlayLayer.appendChild(div);
+        };
+
+        TextOverlay.prototype.draw = function () {
+          // Size and position the overlay. We use a southwest and northeast
+          // position of the overlay to peg it to the correct position and size.
+          // We need to retrieve the projection from this overlay to do this.
+          var overlayProjection = this.getProjection();
+
+          // Retrieve the southwest and northeast coordinates of this overlay
+          // in latlngs and convert them to pixels coordinates.
+          // We'll use these coordinates to resize the DIV.
+          var sw = overlayProjection.fromLatLngToDivPixel(
+            this.bounds_.getSouthWest(),
+          );
+          var ne = overlayProjection.fromLatLngToDivPixel(
+            this.bounds_.getNorthEast(),
+          );
+          // Resize the image's DIV to fit the indicated dimensions.
+          var div = this.div_;
+          var width = this.width;
+          var height = 20;
+
+          div.style.left = sw.x - width / 2 + "px";
+          div.style.top = ne.y - height / 2 + "px";
+          div.style.width = width + "px";
+          div.style.height = height + "px";
+          div.style.width = width + "px";
+          div.style.height = height + "px";
+        };
+
+        TextOverlay.prototype.onRemove = function () {
+          this.div_.parentNode.removeChild(this.div_);
+          this.div_ = null;
+        };
+
+        // Determine the geohash level we will use to draw tiles
+        var currentZoom = this.map.getZoom(),
+          geohashLevelNum = this.mapModel.determineGeohashLevel(currentZoom),
+          geohashLevel = "geohash_" + geohashLevelNum,
+          geohashes = this.searchResults.facetCounts[geohashLevel];
+
+        // Save the current geohash level in the map model
+        this.mapModel.set("tileGeohashLevel", geohashLevelNum);
+
+        // Get all the geohashes contained in the map
+        var mapBBoxes = _.flatten(
+          _.values(this.searchModel.get("geohashGroups")),
+        );
+
+        // Geohashes may be returned that are part of datasets with multiple geographic areas. Some of these may be outside this map.
+        // So we will want to filter out geohashes that are not contained in this map.
+        if (mapBBoxes.length == 0) {
+          var filteredTileGeohashes = geohashes;
+        } else if (geohashes) {
+          var filteredTileGeohashes = [];
+          for (var i = 0; i < geohashes.length - 1; i += 2) {
+            // Get the geohash for this tile
+            var tileGeohash = geohashes[i],
+              isInsideMap = false,
+              index = 0,
+              searchString = tileGeohash;
+
+            // Find if any of the bounding boxes/geohashes inside our map contain this tile geohash
+            while (!isInsideMap && searchString.length > 0) {
+              searchString = tileGeohash.substring(
+                0,
+                tileGeohash.length - index,
               );
+              if (_.contains(mapBBoxes, searchString)) isInsideMap = true;
+              index++;
+            }
 
-              this.$results.find(".loading").remove();
-
-            },
-
-
-            /*
-             * ==================================================================================================
-             *                                             STYLING THE UI
-             * ==================================================================================================
-             */
-            toggleMapMode: function(e) {
-                if (typeof e === "object") {
-                    e.preventDefault();
-                }
+            if (isInsideMap) {
+              filteredTileGeohashes.push(tileGeohash);
+              filteredTileGeohashes.push(geohashes[i + 1]);
+            }
+          }
+        }
+
+        //If there are no tiles on the page, the map may have failed to render, so exit.
+        if (
+          typeof filteredTileGeohashes == "undefined" ||
+          !filteredTileGeohashes.length
+        ) {
+          return;
+        }
+
+        // Make a copy of the array that is geohash counts only
+        var countsOnly = [];
+        for (var i = 1; i < filteredTileGeohashes.length; i += 2) {
+          countsOnly.push(filteredTileGeohashes[i]);
+        }
+
+        // Create a range of lightness to make different colors on the tiles
+        var lightnessMin = this.mapModel.get("tileLightnessMin"),
+          lightnessMax = this.mapModel.get("tileLightnessMax"),
+          lightnessRange = lightnessMax - lightnessMin;
+
+        // Get some stats on our tile counts so we can normalize them to create a color scale
+        var findMedian = function (nums) {
+          if (nums.length % 2 == 0) {
+            return (nums[nums.length / 2 - 1] + nums[nums.length / 2]) / 2;
+          } else {
+            return nums[nums.length / 2 - 0.5];
+          }
+        };
+        var sortedCounts = countsOnly.sort(function (a, b) {
+            return a - b;
+          }),
+          maxCount = sortedCounts[sortedCounts.length - 1],
+          minCount = sortedCounts[0];
+
+        var viewRef = this;
+
+        // Now draw a tile for each geohash facet
+        for (var i = 0; i < filteredTileGeohashes.length - 1; i += 2) {
+          // Convert this geohash to lat,long values
+          var tileGeohash = filteredTileGeohashes[i],
+            decodedGeohash = nGeohash.decode(tileGeohash),
+            latLngCenter = new google.maps.LatLng(
+              decodedGeohash.latitude,
+              decodedGeohash.longitude,
+            ),
+            geohashBox = nGeohash.decode_bbox(tileGeohash),
+            swLatLng = new google.maps.LatLng(geohashBox[0], geohashBox[1]),
+            neLatLng = new google.maps.LatLng(geohashBox[2], geohashBox[3]),
+            bounds = new google.maps.LatLngBounds(swLatLng, neLatLng),
+            tileCount = filteredTileGeohashes[i + 1],
+            drawMarkers = this.mapModel.get("drawMarkers"),
+            marker,
+            count,
+            color;
+
+          // Normalize the range of tiles counts and convert them to a lightness domain of 20-70% lightness.
+          if (maxCount - minCount == 0) {
+            var lightness = lightnessRange;
+          } else {
+            var lightness =
+              ((tileCount - minCount) / (maxCount - minCount)) *
+                lightnessRange +
+              lightnessMin;
+          }
+
+          var color =
+            "hsl(" + this.mapModel.get("tileHue") + "," + lightness + "%,50%)";
+
+          // Add the count to the tile
+          var countLocation = new google.maps.LatLngBounds(
+            latLngCenter,
+            latLngCenter,
+          );
+
+          // Draw the tile label with the dataset count
+          count = new TextOverlay({
+            bounds: countLocation,
+            map: this.map,
+            text: tileCount,
+            color: this.mapModel.get("tileLabelColor"),
+          });
+
+          // Set up the default tile options
+          var tileOptions = {
+            fillColor: color,
+            strokeColor: color,
+            map: this.map,
+            visible: true,
+            bounds: bounds,
+          };
+
+          // Merge these options with any tile options set in the map model
+          var modelTileOptions = this.mapModel.get("tileOptions");
+          for (var attr in modelTileOptions) {
+            tileOptions[attr] = modelTileOptions[attr];
+          }
+
+          // Draw this tile
+          var tile = this.drawTile(tileOptions, tileGeohash, count);
+
+          // Save the geohashes for tiles in the view for later
+          this.tileGeohashes.push(tileGeohash);
+        }
+
+        // Create an info window for each marker that is on the map, to display when it is clicked on
+        if (this.markerGeohashes.length > 0) this.addMarkers();
+
+        // If the map is zoomed all the way in, draw info windows for each tile that will be displayed when they are clicked on
+        if (this.mapModel.isMaxZoom(this.map)) this.addTileInfoWindows();
+      },
+
+      /**
+       * With the options and label object given, add a single tile to the map and set its event listeners
+       * @param {object} options
+       * @param {string} geohash
+       * @param {string} label
+       **/
+      drawTile: function (options, geohash, label) {
+        // Exit if maps are not in use
+        if (this.mode != "map" || !gmaps) {
+          return false;
+        }
+
+        // Add the tile for these datasets to the map
+        var tile = new google.maps.Rectangle(options);
+
+        var viewRef = this;
+
+        // Save our tiles in the view
+        var tileObject = {
+          text: label,
+          shape: tile,
+          geohash: geohash,
+          options: options,
+        };
+        this.tiles.push(tileObject);
+
+        // Change styles when the tile is hovered on
+        google.maps.event.addListener(tile, "mouseover", function (event) {
+          viewRef.highlightTile(tileObject);
+        });
 
-                if (gmaps) {
-                    $(".mapMode").toggleClass("mapMode");
-                }
+        // Change the styles back after the tile is hovered on
+        google.maps.event.addListener(tile, "mouseout", function (event) {
+          viewRef.unhighlightTile(tileObject);
+        });
 
-                if (this.mode == "map") {
-                    MetacatUI.appModel.set("searchMode", "list");
-                    this.mode = "list";
-                    this.$("#map-canvas").detach();
-                    this.setAutoHeight();
-                    this.getResults();
-                } else if (this.mode == "list") {
-                    MetacatUI.appModel.set("searchMode", "map");
-                    this.mode = "map";
-                    this.renderMap();
-                    this.setAutoHeight();
-                    this.getResults();
-                }
-            },
+        // If we are at the max zoom, we will display an info window. If not, we will zoom in.
+        if (!this.mapModel.isMaxZoom(viewRef.map)) {
+          /** Set up some helper functions for zooming in on the map **/
+          var myFitBounds = function (myMap, bounds) {
+            myMap.fitBounds(bounds); // calling fitBounds() here to center the map for the bounds
+
+            var overlayHelper = new google.maps.OverlayView();
+            overlayHelper.draw = function () {
+              if (!this.ready) {
+                var extraZoom = getExtraZoom(
+                  this.getProjection(),
+                  bounds,
+                  myMap.getBounds(),
+                );
+                if (extraZoom > 0) {
+                  myMap.setZoom(myMap.getZoom() + extraZoom);
+                }
+                this.ready = true;
+                google.maps.event.trigger(this, "ready");
+              }
+            };
+            overlayHelper.setMap(myMap);
+          };
+          var getExtraZoom = function (
+            projection,
+            expectedBounds,
+            actualBounds,
+          ) {
+            // in: LatLngBounds bounds -> out: height and width as a Point
+            var getSizeInPixels = function (bounds) {
+              var sw = projection.fromLatLngToContainerPixel(
+                bounds.getSouthWest(),
+              );
+              var ne = projection.fromLatLngToContainerPixel(
+                bounds.getNorthEast(),
+              );
+              return new google.maps.Point(
+                Math.abs(sw.y - ne.y),
+                Math.abs(sw.x - ne.x),
+              );
+            };
 
-            // Communicate that the page is loading
-            loading: function() {
-                $("#map-container").addClass("loading");
-                this.$results.addClass("loading");
+            var expectedSize = getSizeInPixels(expectedBounds),
+              actualSize = getSizeInPixels(actualBounds);
 
-                this.$results.html(this.loadingTemplate({
-                    msg: "Searching for data..."
-                }));
-            },
+            if (
+              Math.floor(expectedSize.x) == 0 ||
+              Math.floor(expectedSize.y) == 0
+            ) {
+              return 0;
+            }
 
-            // Toggles the collapseable filters sidebar and result list in the default theme
-            collapse: function(e) {
-                var id = $(e.target).attr("data-collapse");
+            var qx = actualSize.x / expectedSize.x;
+            var qy = actualSize.y / expectedSize.y;
+            var min = Math.min(qx, qy);
 
-                $("#" + id).toggleClass("collapsed");
-            },
+            if (min < 1) {
+              return 0;
+            }
 
-            toggleFilterCollapse: function(e) {
-                if (typeof e !== "undefined") {
-                    var container = $(e.target).parents(".filter-contain.collapsable");
-                } else {
-                    var container = this.$(".filter-contain.collapsable");
+            return Math.floor(Math.log(min) / Math.LN2 /* = log2(min) */);
+          };
+
+          // Zoom in when the tile is clicked on
+          gmaps.event.addListener(tile, "click", function (clickEvent) {
+            // Change the center
+            viewRef.map.panTo(clickEvent.latLng);
+
+            // Get this tile's bounds
+            var tileBounds = tile.getBounds();
+            // Get the current map bounds
+            var mapBounds = viewRef.map.getBounds();
+
+            // Change the zoom
+            //viewRef.map.fitBounds(tileBounds);
+            myFitBounds(viewRef.map, tileBounds);
+
+            // Track this event
+            MetacatUI.analytics?.trackEvent(
+              "map",
+              "clickTile",
+              "geohash : " + tileObject.geohash,
+            );
+          });
+        }
+
+        return tile;
+      },
+
+      highlightTile: function (tile) {
+        // Change the tile style on hover
+        tile.shape.setOptions(this.mapModel.get("tileOnHover"));
+
+        // Change the label color on hover
+        var div = tile.text.div_;
+        if (div) {
+          div.style.color = this.mapModel.get("tileLabelColorOnHover");
+          tile.text.div_ = div;
+          $(div).css("color", this.mapModel.get("tileLabelColorOnHover"));
+        }
+      },
+
+      unhighlightTile: function (tile) {
+        // Change back the tile to it's original styling
+        tile.shape.setOptions(tile.options);
+
+        // Change back the label color
+        var div = tile.text.div_;
+        div.style.color = this.mapModel.get("tileLabelColor");
+        tile.text.div_ = div;
+        $(div).css("color", this.mapModel.get("tileLabelColor"));
+      },
+
+      /**
+       * Get the details on each marker
+       * And create an infowindow for that marker
+       */
+      addMarkers: function () {
+        // Exit if maps are not in use
+        if (this.mode != "map" || !gmaps) {
+          return false;
+        }
+
+        // Clone the Search model
+        var searchModelClone = this.searchModel.clone(),
+          geohashLevel = this.mapModel.get("tileGeohashLevel"),
+          viewRef = this,
+          markers = this.markers;
+
+        // Change the geohash filter to match our tiles
+        searchModelClone.set("geohashLevel", geohashLevel);
+        searchModelClone.set("geohashes", this.markerGeohashes);
+
+        // Now run a query to get a list of documents that are represented by our markers
+        var query =
+          "q=" +
+          searchModelClone.getQuery() +
+          "&fl=id,title,geohash_9,abstract,geohash_" +
+          geohashLevel +
+          "&rows=1000" +
+          "&wt=json";
+
+        var requestSettings = {
+          url: MetacatUI.appModel.get("queryServiceUrl") + query,
+          success: function (data, textStatus, xhr) {
+            var docs = data.response.docs;
+            var uniqueGeohashes = viewRef.markerGeohashes;
+
+            // Create a marker and infoWindow for each document
+            _.each(docs, function (doc, key, list) {
+              var marker,
+                drawMarkersAt = [];
+
+              // Find the tile place that this document belongs to
+              // For each geohash value at the current geohash level for this document,
+              _.each(doc.geohash_9, function (geohash, key, list) {
+                // Loop through each unique tile location to find its match
+                for (var i = 0; i <= uniqueGeohashes.length; i++) {
+                  if (uniqueGeohashes[i] == geohash.substr(0, geohashLevel)) {
+                    drawMarkersAt.push(geohash);
+                    uniqueGeohashes = _.without(uniqueGeohashes, geohash);
+                  }
                 }
+              });
+
+              _.each(drawMarkersAt, function (markerGeohash, key, list) {
+                var decodedGeohash = nGeohash.decode(markerGeohash),
+                  latLng = new google.maps.LatLng(
+                    decodedGeohash.latitude,
+                    decodedGeohash.longitude,
+                  );
+
+                // Set up the options for each marker
+                var markerOptions = {
+                  position: latLng,
+                  icon: this.mapModel.get("markerImage"),
+                  zIndex: 99999,
+                  map: viewRef.map,
+                };
 
-                // If we can't find a container, then don't do anything
-                if (container.length < 1) return;
-
-                // Expand
-                if ($(container).is(".collapsed")) {
-                    // Toggle the visibility of the collapse/expand icons
-                    $(container).find(".expand").hide();
-                    $(container).find(".collapse").show();
-
-                    // Cache the height of this element so we can reset it on collapse
-                    $(container).attr("data-height", $(container).css("height"));
-
-                    // Increase the height of the container to expand it
-                    $(container).css("max-height", "3000px");
-                }
-                // Collapse
-                else {
-                    // Toggle the visibility of the collapse/expand icons
-                    $(container).find(".collapse").hide();
-                    $(container).find(".expand").show();
-
-                    // Decrease the height of the container to collapse it
-                    if ($(container).attr("data-height")) {
-                        $(container).css("max-height", $(container).attr("data-height"));
-                    } else {
-                        $(container).css("max-height", "1.5em");
+                // Create the marker and add to the map
+                var marker = new google.maps.Marker(markerOptions);
+              });
+            });
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      /**
+       * Get the details on each tile - a list of ids and titles for each dataset contained in that tile
+       * And create an infowindow for that tile
+       */
+      addTileInfoWindows: function () {
+        // Exit if maps are not in use
+        if (this.mode != "map" || !gmaps) {
+          return false;
+        }
+
+        // Clone the Search model
+        var searchModelClone = this.searchModel.clone(),
+          geohashLevel = this.mapModel.get("tileGeohashLevel"),
+          geohashName = "geohash_" + geohashLevel,
+          viewRef = this,
+          infoWindows = [];
+
+        // Change the geohash filter to match our tiles
+        searchModelClone.set("geohashLevel", geohashLevel);
+        searchModelClone.set("geohashes", this.tileGeohashes);
+
+        // Now run a query to get a list of documents that are represented by our tiles
+        var query =
+          "q=" +
+          searchModelClone.getQuery() +
+          "&fl=id,title,geohash_9," +
+          geohashName +
+          "&rows=1000" +
+          "&wt=json";
+
+        var requestSettings = {
+          url: MetacatUI.appModel.get("queryServiceUrl") + query,
+          success: function (data, textStatus, xhr) {
+            // Make an infoWindow for each doc
+            var docs = data.response.docs;
+
+            // For each tile, loop through the docs to find which ones to include in its infoWindow
+            _.each(viewRef.tiles, function (tile, key, list) {
+              var infoWindowContent = "";
+
+              _.each(docs, function (doc, key, list) {
+                var docGeohashes = doc[geohashName];
+
+                if (docGeohashes) {
+                  // Is this document in this tile?
+                  for (var i = 0; i < docGeohashes.length; i++) {
+                    if (docGeohashes[i] == tile.geohash) {
+                      // Add this doc to the infoWindow content
+                      infoWindowContent +=
+                        "<a href='" +
+                        MetacatUI.root +
+                        "/view/" +
+                        encodeURIComponent(doc.id) +
+                        "'>" +
+                        doc.title +
+                        "</a> (" +
+                        doc.id +
+                        ") <br/>";
+                      break;
                     }
+                  }
                 }
+              });
+
+              // The center of the tile
+              var decodedGeohash = nGeohash.decode(tile.geohash),
+                tileCenter = new google.maps.LatLng(
+                  decodedGeohash.latitude,
+                  decodedGeohash.longitude,
+                );
+
+              // The infowindow
+              var infoWindow = new gmaps.InfoWindow({
+                content:
+                  "<div class='gmaps-infowindow'>" +
+                  "<h4> Datasets located here </h4>" +
+                  "<p>" +
+                  infoWindowContent +
+                  "</p>" +
+                  "</div>",
+                isOpen: false,
+                disableAutoPan: false,
+                maxWidth: 250,
+                position: tileCenter,
+              });
+
+              viewRef.tileInfoWindows.push(infoWindow);
+
+              // Zoom in when the tile is clicked on
+              gmaps.event.addListener(
+                tile.shape,
+                "click",
+                function (clickEvent) {
+                  //--- We are at max zoom, display an infowindow ----//
+                  if (this.mapModel.isMaxZoom(viewRef.map)) {
+                    // Find the infowindow that belongs to this tile in the view
+                    infoWindow.open(viewRef.map);
+                    infoWindow.isOpen = true;
+
+                    // Close all other infowindows
+                    viewRef.closeInfoWindows(infoWindow);
+                  }
 
-                $(container).toggleClass("collapsed");
-            },
-
-            /*
-             * Either hides or shows the "clear all filters" button
-             */
-            toggleClearButton: function() {
-                if (this.searchModel.filterCount() > 0) {
-                    this.showClearButton();
-                } else {
-                    this.hideClearButton();
-                }
-            },
-
-            // Move the popover element up the page a bit if it runs off the bottom of the page
-            preventPopoverRunoff: function(e) {
+                  //------ We are not at max zoom, so zoom into this tile ----//
+                  else {
+                    // Change the center
+                    viewRef.map.panTo(clickEvent.latLng);
 
-                // In map view only (because all elements are fixed and you can't scroll)
-                if (this.mode == "map") {
-                    var viewportHeight = $("#map-container").outerHeight();
-                } else {
-                    return false;
-                }
+                    // Get this tile's bounds
+                    var bounds = tile.shape.getBounds();
 
-                if ($(".popover").length > 0) {
-                    var offset = $(".popover").offset();
-                    var popoverHeight = $(".popover").outerHeight();
-                    var topPosition = offset.top;
-
-                    // If pixels are cut off the top of the page, readjust its vertical position
-                    if (topPosition < 0) {
-                        $(".popover").offset({
-                            top: 10
-                        });
-                    } else {
-                        // Else, let's check if it is cut off at the bottom
-                        var totalHeight = topPosition + popoverHeight;
-
-                        var pixelsHidden = totalHeight - viewportHeight;
-
-                        var newTopPosition = topPosition - pixelsHidden - 40;
-
-                        // If pixels are cut off the bottom of the page, readjust its vertical position
-                        if (pixelsHidden > 0) {
-                            $(".popover").offset({
-                                top: newTopPosition
-                            });
-                        }
-                    }
-                }
+                    // Change the zoom
+                    viewRef.map.fitBounds(bounds);
+                  }
+                },
+              );
 
-            },
+              // Close the infowindow upon any click on the map
+              gmaps.event.addListener(viewRef.map, "click", function () {
+                infoWindow.close();
+                infoWindow.isOpen = false;
+              });
+
+              infoWindows[tile.geohash] = infoWindow;
+            });
+
+            viewRef.infoWindows = infoWindows;
+          },
+        };
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      /**
+       * Iterate over each infowindow that we have stored in the view and close it.
+       * Pass an infoWindow object to this function to keep that infoWindow open/skip it
+       * @param {infoWindow} - An infoWindow to keep open
+       */
+      closeInfoWindows: function (except) {
+        var infoWindowLists = [this.markerInfoWindows, this.tileInfoWindows];
+
+        _.each(infoWindowLists, function (infoWindows, key, list) {
+          // Iterate over all the marker infowindows and close all of them except for this one
+          for (var i = 0; i < infoWindows.length; i++) {
+            if (infoWindows[i].isOpen && infoWindows[i] != except) {
+              // Close this info window and stop looking, since only one of each kind should be open anyway
+              infoWindows[i].close();
+              infoWindows[i].isOpen = false;
+              i = infoWindows.length;
+            }
+          }
+        });
+      },
+
+      /**
+       * Remove all the tiles and text from the map
+       **/
+      removeTiles: function () {
+        // Exit if maps are not in use
+        if (this.mode != "map" || !gmaps) {
+          return false;
+        }
+
+        // Remove the tile from the map
+        _.each(this.tiles, function (tile, key, list) {
+          if (tile.shape) tile.shape.setMap(null);
+          if (tile.text) tile.text.setMap(null);
+        });
 
-            onClose: function() {
-                this.stopListening();
+        // Reset the tile storage in the view
+        this.tiles = [];
+        this.tileGeohashes = [];
+        this.tileInfoWindows = [];
+      },
+
+      /**
+       * Iterate over all the markers in the view and remove them from the map and view
+       */
+      removeMarkers: function () {
+        // Exit if maps are not in use
+        if (this.mode != "map" || !gmaps) {
+          return false;
+        }
+
+        // Remove the marker from the map
+        _.each(this.markers, function (marker, key, list) {
+          marker.marker.setMap(null);
+        });
 
-                $(".DataCatalog").removeClass("DataCatalog");
-                $(".mapMode").removeClass("mapMode");
+        // Reset the marker storage in the view
+        this.markers = [];
+        this.markerGeohashes = [];
+        this.markerInfoWindows = [];
+      },
+
+      /*
+       * ==================================================================================================
+       *                                             ADDING RESULTS
+       * ==================================================================================================
+       */
+
+      /** Add all items in the **SearchResults** collection
+       * This loads the first 25, then waits for the map to be
+       * fully loaded and then loads the remaining items.
+       * Without this delay, the app waits until all records are processed
+       */
+      addAll: function () {
+        // After the map is done loading, then load the rest of the results into the list
+        if (this.ready) this.renderAll();
+        else {
+          var viewRef = this;
+          var intervalID = setInterval(function () {
+            if (viewRef.ready) {
+              clearInterval(intervalID);
+              viewRef.renderAll();
+            }
+          }, 500);
+        }
+
+        // After all the results are loaded, query for our facet counts in the background
+        //this.getAutocompletes();
+      },
+
+      renderAll: function () {
+        // do this first to indicate coming results
+        this.updateStats();
+
+        // Remove all the existing tiles on the map
+        this.removeTiles();
+        this.removeMarkers();
+
+        // Remove the loading class and styling
+        this.$results.removeClass("loading");
+
+        // If there are no results, display so
+        var numFound = this.searchResults.length;
+        if (numFound == 0) {
+          // Add a No Results Found message
+          this.$results.html("<p id='no-results-found'>No results found.</p>");
+
+          // Remove the loading styles from the map
+          if (gmaps && this.mapModel) {
+            $("#map-container").removeClass("loading");
+          }
+
+          if (MetacatUI.theme == "arctic") {
+            // When we get new results, check if the user is searching for their own datasets and display a message
+            if (
+              MetacatUI.appView.dataCatalogView &&
+              MetacatUI.appView.dataCatalogView.searchModel.getQuery() ==
+                MetacatUI.appUserModel.get("searchModel").getQuery() &&
+              !MetacatUI.appSearchResults.length
+            ) {
+              $("#no-results-found").after(
+                "<h3>Where are my data sets?</h3><p>If you are a previous ACADIS Gateway user, " +
+                  "you will need to take additional steps to access your data sets in the new NSF Arctic Data Center." +
+                  "<a href='mailto:support@arcticdata.io'>Send us a message at support@arcticdata.io</a> with your old ACADIS " +
+                  "Gateway username and your ORCID identifier (" +
+                  MetacatUI.appUserModel.get("username") +
+                  "), we will help.</p>",
+              );
+            }
+          }
+          return;
+        }
+
+        // Clear the results list before we start adding new rows
+        this.$results.html("");
+
+        //--First map all the results--
+        if (gmaps && this.mapModel) {
+          // Draw all the tiles on the map to represent the datasets
+          this.drawTiles();
+
+          // Remove the loading styles from the map
+          $("#map-container").removeClass("loading");
+        }
+
+        var pid_list = new Array();
+
+        //--- Add all the results to the list ---
+        for (i = 0; i < this.searchResults.length; i++) {
+          pid_list.push(this.searchResults.models[i].get("id"));
+        }
+
+        if (MetacatUI.appModel.get("displayDatasetMetrics")) {
+          var metricsModel = new MetricsModel({
+            pid_list: pid_list,
+            type: "catalog",
+          });
+          metricsModel.fetch();
+          this.metricsModel = metricsModel;
+        }
+
+        //--- Add all the results to the list ---
+        for (i = 0; i < this.searchResults.length; i++) {
+          var element = this.searchResults.models[i];
+          if (typeof element !== "undefined")
+            this.addOne(element, this.metricsModel);
+        }
+
+        // Initialize any tooltips within the result item
+        $(".tooltip-this").tooltip();
+        $(".popover-this").popover();
+
+        // Set the autoheight
+        this.setAutoHeight();
+      },
+
+      /**
+       * Add a single SolrResult item to the list by creating a view for it and appending its element to the DOM.
+       */
+      addOne: function (result) {
+        // Get the view and package service URL's
+        this.$view_service = MetacatUI.appModel.get("viewServiceUrl");
+        this.$package_service = MetacatUI.appModel.get("packageServiceUrl");
+        result.set({
+          view_service: this.$view_service,
+          package_service: this.$package_service,
+        });
 
-                if (gmaps) {
-                    // unset map mode
-                    $("body").removeClass("mapMode");
-                    $("#map-canvas").remove();
-                }
+        var view = new SearchResultView({
+          model: result,
+          metricsModel: this.metricsModel,
+        });
 
-                // remove everything so we don't get a flicker
-                this.$el.html("");
+        // Add this item to the list
+        this.$results.append(view.render().el);
+
+        // map it
+        if (
+          gmaps &&
+          this.mapModel &&
+          typeof result.get("geohash_9") != "undefined" &&
+          result.get("geohash_9") != null
+        ) {
+          var title = result.get("title");
+
+          for (var i = 0; i < result.get("geohash_9").length; i++) {
+            var centerGeohash = result.get("geohash_9")[i],
+              decodedGeohash = nGeohash.decode(centerGeohash),
+              position = new google.maps.LatLng(
+                decodedGeohash.latitude,
+                decodedGeohash.longitude,
+              ),
+              marker = new gmaps.Marker({
+                position: position,
+                icon: this.mapModel.get("markerImage"),
+                zIndex: 99999,
+              });
+          }
+        }
+      },
+
+      /**
+       * When the SearchResults collection has an error getting the results,
+       * show an error message instead of search results
+       * @param {SolrResult} model
+       * @param {XMLHttpRequest.response} response
+       */
+      showError: function (model, response) {
+        var errorMessage = "";
+        var statusCode = response.status;
+
+        if (!statusCode) {
+          statusCode = parseInt(response.statusText);
+        }
+
+        if (statusCode == 500 && this.solrError500Message) {
+          errorMessage = this.solrError500Message;
+        } else {
+          try {
+            errorMessage = $(response.responseText).text();
+          } catch (e) {
+            try {
+              errorMessage = JSON.parse(response.responseText).error.msg;
+            } catch (e) {
+              errorMessage = "";
             }
-        });
-        return DataCatalogView;
-    });
+          } finally {
+            if (typeof errorMessage == "string" && errorMessage.length) {
+              errorMessage = "<p>Error details: " + errorMessage + "</p>";
+            }
+          }
+        }
+
+        MetacatUI.appView.showAlert(
+          "<h4><i class='icon icon-frown'></i>" +
+            this.solrErrorTitle +
+            ".</h4>" +
+            errorMessage,
+          "alert-error",
+          this.$results,
+        );
+
+        this.$results.find(".loading").remove();
+      },
+
+      /*
+       * ==================================================================================================
+       *                                             STYLING THE UI
+       * ==================================================================================================
+       */
+      toggleMapMode: function (e) {
+        if (typeof e === "object") {
+          e.preventDefault();
+        }
+
+        if (gmaps) {
+          $(".mapMode").toggleClass("mapMode");
+        }
+
+        if (this.mode == "map") {
+          MetacatUI.appModel.set("searchMode", "list");
+          this.mode = "list";
+          this.$("#map-canvas").detach();
+          this.setAutoHeight();
+          this.getResults();
+        } else if (this.mode == "list") {
+          MetacatUI.appModel.set("searchMode", "map");
+          this.mode = "map";
+          this.renderMap();
+          this.setAutoHeight();
+          this.getResults();
+        }
+      },
+
+      // Communicate that the page is loading
+      loading: function () {
+        $("#map-container").addClass("loading");
+        this.$results.addClass("loading");
+
+        this.$results.html(
+          this.loadingTemplate({
+            msg: "Searching for data...",
+          }),
+        );
+      },
+
+      // Toggles the collapseable filters sidebar and result list in the default theme
+      collapse: function (e) {
+        var id = $(e.target).attr("data-collapse");
+
+        $("#" + id).toggleClass("collapsed");
+      },
+
+      toggleFilterCollapse: function (e) {
+        if (typeof e !== "undefined") {
+          var container = $(e.target).parents(".filter-contain.collapsable");
+        } else {
+          var container = this.$(".filter-contain.collapsable");
+        }
+
+        // If we can't find a container, then don't do anything
+        if (container.length < 1) return;
+
+        // Expand
+        if ($(container).is(".collapsed")) {
+          // Toggle the visibility of the collapse/expand icons
+          $(container).find(".expand").hide();
+          $(container).find(".collapse").show();
+
+          // Cache the height of this element so we can reset it on collapse
+          $(container).attr("data-height", $(container).css("height"));
+
+          // Increase the height of the container to expand it
+          $(container).css("max-height", "3000px");
+        }
+        // Collapse
+        else {
+          // Toggle the visibility of the collapse/expand icons
+          $(container).find(".collapse").hide();
+          $(container).find(".expand").show();
+
+          // Decrease the height of the container to collapse it
+          if ($(container).attr("data-height")) {
+            $(container).css("max-height", $(container).attr("data-height"));
+          } else {
+            $(container).css("max-height", "1.5em");
+          }
+        }
+
+        $(container).toggleClass("collapsed");
+      },
+
+      /*
+       * Either hides or shows the "clear all filters" button
+       */
+      toggleClearButton: function () {
+        if (this.searchModel.filterCount() > 0) {
+          this.showClearButton();
+        } else {
+          this.hideClearButton();
+        }
+      },
+
+      // Move the popover element up the page a bit if it runs off the bottom of the page
+      preventPopoverRunoff: function (e) {
+        // In map view only (because all elements are fixed and you can't scroll)
+        if (this.mode == "map") {
+          var viewportHeight = $("#map-container").outerHeight();
+        } else {
+          return false;
+        }
+
+        if ($(".popover").length > 0) {
+          var offset = $(".popover").offset();
+          var popoverHeight = $(".popover").outerHeight();
+          var topPosition = offset.top;
+
+          // If pixels are cut off the top of the page, readjust its vertical position
+          if (topPosition < 0) {
+            $(".popover").offset({
+              top: 10,
+            });
+          } else {
+            // Else, let's check if it is cut off at the bottom
+            var totalHeight = topPosition + popoverHeight;
+
+            var pixelsHidden = totalHeight - viewportHeight;
+
+            var newTopPosition = topPosition - pixelsHidden - 40;
+
+            // If pixels are cut off the bottom of the page, readjust its vertical position
+            if (pixelsHidden > 0) {
+              $(".popover").offset({
+                top: newTopPosition,
+              });
+            }
+          }
+        }
+      },
+
+      onClose: function () {
+        this.stopListening();
+
+        $(".DataCatalog").removeClass("DataCatalog");
+        $(".mapMode").removeClass("mapMode");
+
+        if (gmaps) {
+          // unset map mode
+          $("body").removeClass("mapMode");
+          $("#map-canvas").remove();
+        }
+
+        // remove everything so we don't get a flicker
+        this.$el.html("");
+      },
+    },
+  );
+  return DataCatalogView;
+});
 
diff --git a/docs/docs/src_js_views_DataCatalogViewWithFilters.js.html b/docs/docs/src_js_views_DataCatalogViewWithFilters.js.html index 53d317004..9626abc45 100644 --- a/docs/docs/src_js_views_DataCatalogViewWithFilters.js.html +++ b/docs/docs/src_js_views_DataCatalogViewWithFilters.js.html @@ -71,7 +71,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

DataCatalogView, FilterGroupsView, template, - nGeohash + nGeohash, ) { /** * @class DataCatalogViewWithFilters @@ -196,7 +196,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

isMySearch: _.indexOf( this.searchModel.get("username"), - MetacatUI.appUserModel.get("username") + MetacatUI.appUserModel.get("username"), ) > -1, loading: loadingHTML, searchModelRef: this.searchModel, @@ -205,7 +205,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

MetacatUI.theme == "dataone" ? "Member Node" : "Data source", }; compiledEl = this.template( - _.extend(this.searchModel.toJSON(), templateVars) + _.extend(this.searchModel.toJSON(), templateVars), ); this.$el.html(compiledEl); @@ -263,12 +263,12 @@

Source: src/js/views/DataCatalogViewWithFilters.js

// Listen to changes in the Search model Filters to trigger a search this.stopListening( this.searchModel.get("filters"), - "add remove update reset change" + "add remove update reset change", ); this.listenTo( this.searchModel.get("filters"), "add remove update reset change", - this.triggerSearch + this.triggerSearch, ); // Listen to the MetacatUI.appModel for the search trigger @@ -277,7 +277,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

this.listenTo( MetacatUI.appUserModel, "change:loggedIn", - this.triggerSearch + this.triggerSearch, ); // and go to a certain page if we have it @@ -324,7 +324,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

// Add the Filters to the array allFilters = _.union(allFilters, filterGroup.get("filters").models); }, - this + this, ); // Add the filters to the Search model @@ -405,7 +405,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

"northBoundCoord", "southBoundCoord", "eastBoundCoord", - "westBoundCoord" + "westBoundCoord", ); } // Set the field list on the SolrResults collection as a comma-separated @@ -461,7 +461,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

var spatialFilter = _.findWhere( this.searchModel.get("filters").models, - { type: "SpatialFilter" } + { type: "SpatialFilter" }, ); if (isOn) { @@ -485,7 +485,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

google.maps.event.trigger(this.mapModel.get("map"), "idle"); // Track this event - MetacatUI.analytics?.trackEvent("map", (isOn ? "on" : "off")); + MetacatUI.analytics?.trackEvent("map", isOn ? "on" : "off"); }, /** @@ -691,7 +691,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

west, north, east, - precision + precision, ); } @@ -723,7 +723,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

viewRef.$(".toggle-map-filter").prop("checked", false); viewRef.toggleMapFilter(); } - } + }, ); } } @@ -763,7 +763,7 @@

Source: src/js/views/DataCatalogViewWithFilters.js

viewRef.hasDragged = true; }); }, - } + }, ); return DataCatalogViewWithFilters; }); diff --git a/docs/docs/src_js_views_DataItemView.js.html b/docs/docs/src_js_views_DataItemView.js.html index 30c042205..eb26b4428 100644 --- a/docs/docs/src_js_views_DataItemView.js.html +++ b/docs/docs/src_js_views_DataItemView.js.html @@ -44,20 +44,28 @@

Source: src/js/views/DataItemView.js

-
/* global define */
-define([
-      'underscore',
-      'jquery',
-      'backbone',
-      'models/DataONEObject',
-      'models/metadata/eml211/EML211',
-      'models/metadata/eml211/EMLOtherEntity',
-      'views/DownloadButtonView',
-      'text!templates/dataItem.html',
-      'text!templates/dataItemHierarchy.html'],
-    function(_, $, Backbone, DataONEObject, EML, EMLOtherEntity, DownloadButtonView, DataItemTemplate, DataItemHierarchy){
-
-        /**
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/DataONEObject",
+  "models/metadata/eml211/EML211",
+  "models/metadata/eml211/EMLOtherEntity",
+  "views/DownloadButtonView",
+  "text!templates/dataItem.html",
+  "text!templates/dataItemHierarchy.html",
+], function (
+  _,
+  $,
+  Backbone,
+  DataONEObject,
+  EML,
+  EMLOtherEntity,
+  DownloadButtonView,
+  DataItemTemplate,
+  DataItemHierarchy,
+) {
+  /**
         * @class DataItemView
         * @classdesc    A DataItemView represents a single data item in a data package as a single row of
             a nested table.  An item may represent a metadata object (as a folder), or a data
@@ -68,1434 +76,1529 @@ 

Source: src/js/views/DataItemView.js

* @constructor * @screenshot views/DataItemView.png */ - var DataItemView = Backbone.View.extend( - /** @lends DataItemView.prototype */{ - - tagName: "tr", - - className: "data-package-item", - - id: null, - - /** The HTML template for a data item */ - template: _.template(DataItemTemplate), - - - /** The HTML template for a data item */ - dataItemHierarchyTemplate: _.template(DataItemHierarchy), - - //Templates - metricTemplate: _.template( "<span class='packageTable-resultItem badge '>" + - "<i class='catalog-metric-icon <%= metricIcon %>'>" + - "</i> <%= memberRowMetrics %> " + - "</span>"), - - /** - * The DataONEObject model to display in this view - * @type {DataONEObject} - */ - model: null, - - /** - * A reference to the parent EditorView that contains this DataItemView - * @type EditorView - * @since 2.15.0 - */ - parentEditorView: null, - - /** Events this view listens to */ - events: { - "focusout .name.canRename" : "updateName", - "click .name.canRename" : "emptyName", - "click .duplicate" : "duplicate", // Edit dropdown, duplicate scimeta/rdf - "click .addFolder" : "handleAddFolder", // Edit dropdown, add nested scimeta/rdf - "click .addFiles" : "handleAddFiles", // Edit dropdown, open file picker dialog - "change .file-upload" : "addFiles", // Adds the files into the collection - "change .file-replace" : "replaceFile", // Replace a file in the collection - "dragover" : "showDropzone", // Drag & drop, show the dropzone for this row - "dragend" : "hideDropzone", // Drag & drop, hide the dropzone for this row - "dragleave" : "hideDropzone", // Drag & drop, hide the dropzone for this row - "drop" : "addFiles", // Drag & drop, adds the files into the collection - "click .replaceFile" : "handleReplace", // Replace dropdown, data in collection - "click .removeFiles" : "handleRemove", // Edit dropdown, remove sci{data,meta} from collection - "click .cancel" : "handleCancel", // Cancel a file load - "change: percentLoaded": "updateLoadProgress", // Update the file read progress bar - "mouseover .remove" : "previewRemove", - "mouseout .remove" : "previewRemove", - "change .public" : "changeAccessPolicy", - "click .downloadAction": "downloadFile" - }, - - /** Initialize the object - post constructor */ - initialize: function(options) { - if(typeof options == "undefined") var options = {}; - - this.model = options.model || new DataONEObject(); - this.currentlyViewing = options.currentlyViewing || null; - this.mode = options.mode || "edit"; - this.itemName = options.itemName || null; - this.itemPath = options.itemPath || null; - this.itemType = options.itemType || "file"; - this.insertInfoIcon = options.insertInfoIcon || false; - this.id = this.model.get("id"); - this.canWrite = false; // Default. Updated in render() - this.canShare = false; // Default. Updated in render() - this.parentEditorView = options.parentEditorView || null; - this.dataPackageId = options.dataPackageId || null; - - if(!(typeof options.metricsModel == "undefined")){ - this.metricsModel = options.metricsModel; + var DataItemView = Backbone.View.extend( + /** @lends DataItemView.prototype */ { + tagName: "tr", + + className: "data-package-item", + + id: null, + + /** The HTML template for a data item */ + template: _.template(DataItemTemplate), + + /** The HTML template for a data item */ + dataItemHierarchyTemplate: _.template(DataItemHierarchy), + + //Templates + metricTemplate: _.template( + "<span class='packageTable-resultItem badge '>" + + "<i class='catalog-metric-icon <%= metricIcon %>'>" + + "</i> <%= memberRowMetrics %> " + + "</span>", + ), + + /** + * The DataONEObject model to display in this view + * @type {DataONEObject} + */ + model: null, + + /** + * A reference to the parent EditorView that contains this DataItemView + * @type EditorView + * @since 2.15.0 + */ + parentEditorView: null, + + /** Events this view listens to */ + events: { + "focusout .name.canRename": "updateName", + "click .name.canRename": "emptyName", + "click .duplicate": "duplicate", // Edit dropdown, duplicate scimeta/rdf + "click .addFolder": "handleAddFolder", // Edit dropdown, add nested scimeta/rdf + "click .addFiles": "handleAddFiles", // Edit dropdown, open file picker dialog + "change .file-upload": "addFiles", // Adds the files into the collection + "change .file-replace": "replaceFile", // Replace a file in the collection + dragover: "showDropzone", // Drag & drop, show the dropzone for this row + dragend: "hideDropzone", // Drag & drop, hide the dropzone for this row + dragleave: "hideDropzone", // Drag & drop, hide the dropzone for this row + drop: "addFiles", // Drag & drop, adds the files into the collection + "click .replaceFile": "handleReplace", // Replace dropdown, data in collection + "click .removeFiles": "handleRemove", // Edit dropdown, remove sci{data,meta} from collection + "click .cancel": "handleCancel", // Cancel a file load + "change: percentLoaded": "updateLoadProgress", // Update the file read progress bar + "mouseover .remove": "previewRemove", + "mouseout .remove": "previewRemove", + "change .public": "changeAccessPolicy", + "click .downloadAction": "downloadFile", + }, + + /** Initialize the object - post constructor */ + initialize: function (options) { + if (typeof options == "undefined") var options = {}; + + this.model = options.model || new DataONEObject(); + this.currentlyViewing = options.currentlyViewing || null; + this.mode = options.mode || "edit"; + this.itemName = options.itemName || null; + this.itemPath = options.itemPath || null; + this.itemType = options.itemType || "file"; + this.insertInfoIcon = options.insertInfoIcon || false; + this.id = this.model.get("id"); + this.canWrite = false; // Default. Updated in render() + this.canShare = false; // Default. Updated in render() + this.parentEditorView = options.parentEditorView || null; + this.dataPackageId = options.dataPackageId || null; + + if (!(typeof options.metricsModel == "undefined")) { + this.metricsModel = options.metricsModel; + } + }, + + /** Renders a DataItemView for the given DataONEObject + * @param {DataONEObject} model + */ + render: function (model) { + //Prevent duplicate listeners + this.stopListening(); + + if (this.itemType === "folder") { + // Set the data-id for identifying events to model ids + this.$el.attr( + "data-id", + (this.itemPath ? this.itemPath : "") + "/" + this.itemName, + ); + this.$el.attr("data-parent", this.itemPath ? this.itemPath : ""); + this.$el.attr("data-category", "entities-" + this.itemName); + + var attributes = new Object(); + attributes.fileType = undefined; + attributes.isFolder = true; + attributes.icon = "icon-folder-open"; + attributes.id = this.itemName; + attributes.size = undefined; + attributes.insertInfoIcon = false; + attributes.memberRowMetrics = undefined; + attributes.isMetadata = false; + attributes.downloadUrl = undefined; + attributes.moreInfoLink = undefined; + // attributes.isMetadata = false; + attributes.viewType = this.mode; + attributes.objectTitle = this.itemName; + + var itemPathParts = new Array(); + if (this.itemPath) { + itemPathParts = this.itemPath.split("/"); + attributes.nodeLevel = itemPathParts.length; + if (this.itemPath.startsWith("/")) { + attributes.nodeLevel -= 1; + } + if (this.itemPath.endsWith("/")) { + attributes.nodeLevel -= 1; + } + if (itemPathParts[-1] == attributes.objectTitle) { + attributes.nodeLevel -= 1; + } + } else { + attributes.nodeLevel = 0; + this.itemPath = "/"; + this.$el.attr("data-packageId", this.dataPackageId); + } + this.$el.html(this.dataItemHierarchyTemplate(attributes)); + } else { + // Set the data-id for identifying events to model ids + this.$el.attr("data-id", this.model.get("id")); + this.$el.attr("data-category", "entities-" + this.model.get("id")); + + //Destroy the old tooltip + this.$(".status .icon, .status .progress") + .tooltip("hide") + .tooltip("destroy"); + + var attributes = this.model.toJSON(); + + // check if this data item is a metadata object + attributes.isMetadata = false; + if ( + this.model.get("type") == "Metadata" || + this.model.get("formatType") == "METADATA" + ) { + attributes.isMetadata = true; + } + + //Format the title + if (Array.isArray(attributes.title)) { + attributes.title = attributes.title[0]; + } + + //Set some defaults + attributes.numAttributes = 0; + attributes.entityIsValid = true; + attributes.hasInvalidAttribute = false; + attributes.viewType = this.mode; + + if (this.mode === "edit") { + // Restrict item replacement and renaming depending on access policy + // + // Note: .canWrite is set here (at render) instead of at init + // because render will get called a few times during page load + // as the app updates what it knows about the object + let accessPolicy = this.model.get("accessPolicy"); + if (accessPolicy) { + attributes.canWrite = accessPolicy.isAuthorized("write"); + this.canWrite = attributes.canWrite; + attributes.canRename = accessPolicy.isAuthorizedUpdateSysMeta(); + } else { + attributes.canWrite = false; + this.canWrite = false; + attributes.canRename = false; + } + + // Restrict item sharing depending on access + this.canShare = this.canShareItem(); + attributes.canShare = this.canShare; + + //Get the number of attributes for this item + if (this.model.type != "EML") { + //Get the parent EML model + if (this.parentEML) { + var parentEML = this.parentEML; + } else { + var parentEML = MetacatUI.rootDataPackage.where({ + id: Array.isArray(this.model.get("isDocumentedBy")) + ? this.model.get("isDocumentedBy")[0] + : null, + }); } - }, - /** Renders a DataItemView for the given DataONEObject - * @param {DataONEObject} model - */ - render: function(model) { - - //Prevent duplicate listeners - this.stopListening(); - - if (this.itemType === "folder") { - - // Set the data-id for identifying events to model ids - this.$el.attr("data-id", (this.itemPath ? this.itemPath : "") + "/" + this.itemName); - this.$el.attr("data-parent", this.itemPath ? this.itemPath : ""); - this.$el.attr("data-category", "entities-" + this.itemName); - - var attributes = new Object(); - attributes.fileType = undefined; - attributes.isFolder = true; - attributes.icon = "icon-folder-open"; - attributes.id = this.itemName; - attributes.size = undefined; - attributes.insertInfoIcon = false; - attributes.memberRowMetrics = undefined; - attributes.isMetadata = false; - attributes.downloadUrl = undefined; - attributes.moreInfoLink = undefined; - // attributes.isMetadata = false; - attributes.viewType = this.mode; - attributes.objectTitle = this.itemName; - - var itemPathParts = new Array(); - if (this.itemPath) { - itemPathParts = this.itemPath.split("/"); - attributes.nodeLevel = itemPathParts.length; - if (this.itemPath.startsWith("/")) { - attributes.nodeLevel -= 1; - } - if (this.itemPath.endsWith("/")) { - attributes.nodeLevel -= 1; - } - if (itemPathParts[-1] == attributes.objectTitle) { - attributes.nodeLevel -= 1; - } - } - else { - attributes.nodeLevel = 0; - this.itemPath = "/"; - this.$el.attr("data-packageId", this.dataPackageId); - } - this.$el.html( this.dataItemHierarchyTemplate(attributes) ); - } - else { - // Set the data-id for identifying events to model ids - this.$el.attr("data-id", this.model.get("id")); - this.$el.attr("data-category", "entities-" + this.model.get("id")); - - //Destroy the old tooltip - this.$(".status .icon, .status .progress").tooltip("hide").tooltip("destroy"); - - var attributes = this.model.toJSON(); - - // check if this data item is a metadata object - attributes.isMetadata = false; - if (this.model.get("type") == "Metadata" || this.model.get("formatType") == "METADATA") { - attributes.isMetadata = true; - } + if (Array.isArray(parentEML)) parentEML = parentEML[0]; - //Format the title - if(Array.isArray(attributes.title)) { - attributes.title = attributes.title[0]; - } + //If we found a parent EML model + if (parentEML && parentEML.type == "EML") { + this.parentEML = parentEML; - //Set some defaults - attributes.numAttributes = 0; - attributes.entityIsValid = true; - attributes.hasInvalidAttribute = false; - attributes.viewType = this.mode; + //Find the EMLEntity model for this data item + var entity = + this.model.get("metadataEntity") || + parentEML.getEntity(this.model); - if (this.mode === "edit") { - // Restrict item replacement and renaming depending on access policy - // - // Note: .canWrite is set here (at render) instead of at init - // because render will get called a few times during page load - // as the app updates what it knows about the object - let accessPolicy = this.model.get("accessPolicy"); - if( accessPolicy ){ - attributes.canWrite = accessPolicy.isAuthorized("write"); - this.canWrite = attributes.canWrite; - attributes.canRename = accessPolicy.isAuthorizedUpdateSysMeta(); - } - else{ - attributes.canWrite = false; - this.canWrite = false; - attributes.canRename = false; - } - - // Restrict item sharing depending on access - this.canShare = this.canShareItem(); - attributes.canShare = this.canShare; - - - //Get the number of attributes for this item - if(this.model.type != "EML"){ - - //Get the parent EML model - if( this.parentEML ){ - var parentEML = this.parentEML; - } - else{ - var parentEML = MetacatUI.rootDataPackage.where({ - id: Array.isArray(this.model.get("isDocumentedBy")) ? - this.model.get("isDocumentedBy")[0] : null - }); - } - - if( Array.isArray(parentEML) ) - parentEML = parentEML[0]; - - //If we found a parent EML model - if(parentEML && parentEML.type == "EML"){ - - this.parentEML = parentEML; - - //Find the EMLEntity model for this data item - var entity = this.model.get("metadataEntity") || parentEML.getEntity(this.model); - - //If we found an EMLEntity model - if(entity){ - - this.entity = entity; - - //Get the file name from the metadata if it is not in the model - if( !this.model.get("fileName") ){ - - var fileName = ""; - - if( entity.get("physicalObjectName") ) - fileName = entity.get("physicalObjectName"); - else if( entity.get("entityName") ) - fileName = entity.get("entityName"); - - if( fileName ) - attributes.fileName = fileName; - this.model.set("fileName", fileName); - } - - //Get the number of attributes for this entity - attributes.numAttributes = entity.get("attributeList").length; - //Determine if the entity model is valid - attributes.entityIsValid = entity.isValid(); - - //Listen to changes to certain attributes of this EMLEntity model - // to re-render this view - this.stopListening(entity); - this.listenTo(entity, "change:entityType, change:entityName", this.render); - - //Check if there are any invalid attribute models - //Also listen to each attribute model - _.each( entity.get("attributeList"), function(attr){ - - var isValid = attr.isValid(); - - //Mark that this entity has at least one invalid attribute - if( !attributes.hasInvalidAttribute && !isValid ) - attributes.hasInvalidAttribute = true; - - this.stopListening(attr); - - //Listen to when the validation status changes and rerender - if(isValid) - this.listenTo( attr, "invalid", this.render); - else - this.listenTo( attr, "valid", this.render); - - - }, this); - - //If there are no attributes now, rerender when one is added - this.listenTo(entity, "change:attributeList", this.render); - - } - else{ - //Rerender when an entity is added - this.listenTo(this.model, "change:entities", this.render); - } - } - else{ - //When the package is complete, rerender - this.listenTo(MetacatUI.rootDataPackage, "add:EML", this.render); - } - } - - this.$el.html( this.template(attributes) ); - - //Initialize dropdowns - this.$el.find(".dropdown-toggle").dropdown(); - - //Render the Share button - this.renderShareControl(); - - if(this.model.get("type") == "Metadata"){ - //Add the title data-attribute attribute to the name cell - this.$el.find(".name").attr("data-attribute", "title"); - this.$el.addClass("folder"); - } - else{ - this.$el.addClass("data"); - } - - // Add tooltip to a disabled Replace link - $(this.$el).find(".replace.disabled").tooltip({ - title: "You don't have sufficient privileges to replace this item.", - placement: "left", - trigger: "hover", - delay: { show: 400 }, - container: "body" - }); - - //Check if the data package is in progress of being uploaded - this.toggleSaving(); - - //Create tooltips based on the upload status - var uploadStatus = this.model.get("uploadStatus"), - errorMessage = this.model.get("errorMessage"); - - // Use a friendlier message for 401 errors (the one returned is a little hard to understand) - if(this.model.get("sysMetaErrorCode") == 401){ - - // If the user at least has write permission, they cannot update the system metadata only, so show this message - /** @todo Do an object update when someone has write permission but not changePermission and is trying to change the system metadata (but not the access policy) */ - if(accessPolicy && accessPolicy.isAuthorized("write")){ - errorMessage = "The owner of this data file has not given you permission to rename it or change the " + MetacatUI.appModel.get("accessPolicyName") + "." - // Otherwise, assume they only have read access - } else { - errorMessage = "The owner of this data file has not given you permission to edit this data file or change the " + MetacatUI.appModel.get("accessPolicyName") + "."; - } - } - - // When there's an error or a warninig - if(uploadStatus == "e" && errorMessage){ - - var tooltipClass = uploadStatus == "e" ? "error" : ""; - - this.$(".status .icon").tooltip({ - placement: "top", - trigger: "hover", - html: true, - title: "<div class='status-tooltip " + tooltipClass + "'><h6>Issue saving:</h6><div>" + errorMessage + "</div></div>", - container: "body" - }); - - this.$el.removeClass("loading"); - } - else if (( !uploadStatus || uploadStatus == "c" || uploadStatus == "q") && attributes.numAttributes == 0){ - - this.$(".status .icon").tooltip({ - placement: "top", - trigger: "hover", - html: true, - title: "<div class='status-tooltip'>This file needs to be described - Click 'Describe'</div>", - container: "body" - }); - - this.$el.removeClass("loading"); + //If we found an EMLEntity model + if (entity) { + this.entity = entity; - } - else if( attributes.hasInvalidAttribute || !attributes.entityIsValid ){ - - this.$(".status .icon").tooltip({ - placement: "top", - trigger: "hover", - html: true, - title: "<div class='status-tooltip'>There is missing information about this file. Click 'Describe'</div>", - container: "body" - }); - - this.$el.removeClass("loading"); - - } - else if(uploadStatus == "c"){ - - this.$(".status .icon").tooltip({ - placement: "top", - trigger: "hover", - html: true, - title: "<div class='status-tooltip'>Complete</div>", - container: "body" - }); - - this.$el.removeClass("loading"); - } - else if(uploadStatus == "l"){ - this.$(".status .icon").tooltip({ - placement: "top", - trigger: "hover", - html: true, - title: "<div class='status-tooltip'>Reading file...</div>", - container: "body" - }); - - this.$el.addClass("loading"); - } - else if(uploadStatus == "p"){ - var model = this.model; - - this.$(".status .progress").tooltip({ - placement: "top", - trigger: "hover", - html: true, - title: function(){ - if(model.get("numSaveAttempts") > 0){ - return "<div class='status-tooltip'>Something went wrong during upload. <br/> Trying again... (attempt " + (model.get("numSaveAttempts") + 1) + " of 3)</div>"; - } - else if(model.get("uploadProgress")){ - var percentDone = model.get("uploadProgress").toString(); - if(percentDone.indexOf(".") > -1) - percentDone = percentDone.substring(0, percentDone.indexOf(".")); - } - else - var percentDone = "0"; - - return "<div class='status-tooltip'>Uploading: " + percentDone + "%</div>"; - }, - container: "body" - }); - - this.$el.addClass("loading"); - } - else{ - this.$el.removeClass("loading"); - } - - //Listen to changes to the upload progress of this object - this.listenTo(this.model, "change:uploadProgress", this.showUploadProgress); - - //Listen to changes to the upload status of the entire package - this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:uploadStatus", this.toggleSaving); - - //listen for changes to rerender the view - this.listenTo(this.model, "change:fileName change:title change:id change:formatType " + - "change:formatId change:type change:resourceMap change:documents change:isDocumentedBy " + - "change:size change:nodeLevel change:uploadStatus", this.render); // render changes to the item - - var view = this; - this.listenTo(this.model, "replace", function(newModel){ - view.model = newModel; - view.render(); - }); - } - else { - - this.isMetadata = false; - // format metadata object title - if (attributes.isMetadata || this.model.getFormat() == "metadata" || this.model.get("id") == this.currentlyViewing) { - attributes.title = "Metadata: " + this.model.get("fileName"); - attributes.icon = "icon-file-text"; - attributes.metricIcon = "icon-eye-open"; - this.isMetadata = true; - this.$el.attr("data-packageId", this.dataPackageId); - } - - var objectTitleTooltip = attributes.title || attributes.fileName || attributes.id; - attributes.objectTitle = (objectTitleTooltip.length > 150) ? objectTitleTooltip.slice(0,75) + "..." + objectTitleTooltip.slice(objectTitleTooltip.length - 75, objectTitleTooltip.length) : objectTitleTooltip; - - attributes.fileType = this.model.getFormat(); - attributes.isFolder = false; - //Determine the icon type based on format type - if(this.model.getFormat() == "program") - attributes.icon = "icon-code"; - else if(this.model.getFormat() == "data") - attributes.icon = "icon-table"; - else if (this.model.getFormat() == "image/jpeg") - attributes.icon = "icon-picture"; - else if (this.model.getFormat() == "PDF") - attributes.icon = "icon-file"; - else - attributes.icon = "icon-table"; - - attributes.id = this.model.get("id"); - attributes.memberRowMetrics = null; - var metricToolTip = null, - view = this; - - // Insert metrics for this item, - // if the model has already been fethced. - if (this.metricsModel.get("views") !== null) { - metricToolTip = this.getMemberRowMetrics(view.id); - attributes.memberRowMetrics = metricToolTip.split(" ")[0]; - } - else { - // Update the metrics later on - // If the fetch() is still in progress. - this.listenTo(this.metricsModel, "sync", function(){ - metricToolTip = this.getMemberRowMetrics(view.id); - let readsCell = this.$('.metrics-count.downloads[data-id="' + view.id + '"]'); - metricToolTip = view.getMemberRowMetrics(view.id); - if((typeof metricToolTip !== "undefined") && metricToolTip) - readsCell.html(this.metricTemplate({metricIcon: attributes.metricIcon, memberRowMetrics: metricToolTip.split(" ")[0]})); - }); - } - - // add nodeLevel for displaying indented filename - attributes.nodeLevel = 1; - if (!(attributes.isMetadata || this.model.getFormat() == "metadata" || this.model.get("id") == this.currentlyViewing)) { - attributes.metricIcon = "icon-cloud-download"; - - this.$el.addClass(); - if (this.itemPath && (typeof this.itemPath !== undefined) && this.itemPath != "/") { - itemPathParts = this.itemPath.split("/"); - - attributes.nodeLevel = itemPathParts.length; - - if (this.itemPath.startsWith("/")) { - attributes.nodeLevel -= 1; - } - if (this.itemPath.endsWith("/")) { - attributes.nodeLevel -= 1; - } - - // var parent = itemPathParts[itemPathParts.length - 2]; - var parentPath = (itemPathParts.slice(0, -1)).join("/"); - - if (parentPath !== undefined) { - this.$el.attr("data-parent", parentPath); - } - } - else { - attributes.nodeLevel = 1; - this.$el.attr("data-packageId", this.dataPackageId); - } - } - - if (attributes.nodeLevel == 1) { - this.$el.attr("data-packageId", this.dataPackageId); - } - - //Download button - attributes.downloadUrl = undefined; - if (this.model.get("dataUrl") !== undefined || - this.model.get("url") !== undefined || - this.model.url() !== undefined) { - if (this.model.get("dataUrl") !== undefined) { - attributes.downloadUrl = this.model.get("dataUrl"); - } - else if (this.model.get("url") !== undefined) { - attributes.downloadUrl = this.model.get("url"); - } - else if (this.model.url() !== undefined) { - var downloadUrl = this.model.url(); - attributes.downloadUrl = downloadUrl.replace("/meta/", "/object/"); - } - } - this.downloadButtonView = new DownloadButtonView({ model: this.model, view: "actionsView" }); - this.downloadButtonView.render(); - - let id = this.model.get("id"); - let infoLink = MetacatUI.root + "/view/" + encodeURIComponent(this.currentlyViewing) + "#" + encodeURIComponent(id) - attributes.moreInfoLink = infoLink; - - attributes.insertInfoIcon = this.insertInfoIcon; - - this.$el.html( this.dataItemHierarchyTemplate(attributes) ); - - this.$('.downloadAction').html(this.downloadButtonView.el); - - // add tooltip for metrics in package table - this.$('.packageTable-resultItem').tooltip({ - placement: "top", - trigger: "hover", - delay: 300, - title: metricToolTip - }); - - this.$(".fileTitle").addClass("tooltip-this") - .attr("data-placement", "top") - .attr("data-trigger", "hover") - .attr("data-delay", "300") - .attr("data-title", objectTitleTooltip); + //Get the file name from the metadata if it is not in the model + if (!this.model.get("fileName")) { + var fileName = ""; + if (entity.get("physicalObjectName")) + fileName = entity.get("physicalObjectName"); + else if (entity.get("entityName")) + fileName = entity.get("entityName"); + if (fileName) attributes.fileName = fileName; + this.model.set("fileName", fileName); } - } - - this.$el.data({ - view: this, - model: this.model - }); - - return this; - }, - - /** - * Renders a button that opens the AccessPolicyView for editing permissions on this data item - * @since 2.15.0 - */ - renderShareControl: function(){ - - //Get the Share button element - var shareButton = this.$(".sharing button"); - - if( this.parentEditorView && this.parentEditorView.isAccessPolicyEditEnabled() ){ - - //Start a title for the button tooltip - var sharebuttonTitle; - // If the user is not authorized to change the permissions of - // this object, then disable the share button - if (this.canShare) { - shareButton.removeClass("disabled"); - sharebuttonTitle = "Share this item with others"; + //Get the number of attributes for this entity + attributes.numAttributes = entity.get("attributeList").length; + //Determine if the entity model is valid + attributes.entityIsValid = entity.isValid(); + + //Listen to changes to certain attributes of this EMLEntity model + // to re-render this view + this.stopListening(entity); + this.listenTo( + entity, + "change:entityType, change:entityName", + this.render, + ); + + //Check if there are any invalid attribute models + //Also listen to each attribute model + _.each( + entity.get("attributeList"), + function (attr) { + var isValid = attr.isValid(); + + //Mark that this entity has at least one invalid attribute + if (!attributes.hasInvalidAttribute && !isValid) + attributes.hasInvalidAttribute = true; + + this.stopListening(attr); + + //Listen to when the validation status changes and rerender + if (isValid) this.listenTo(attr, "invalid", this.render); + else this.listenTo(attr, "valid", this.render); + }, + this, + ); + + //If there are no attributes now, rerender when one is added + this.listenTo(entity, "change:attributeList", this.render); } else { - shareButton.addClass("disabled"); - sharebuttonTitle = "You are not authorized to share this item." + //Rerender when an entity is added + this.listenTo(this.model, "change:entities", this.render); } - - // Set up tooltips for share button - shareButton.tooltip({ - title: sharebuttonTitle, - placement: "top", - container: this.el, - trigger: "hover", - delay: { show: 400 } - }); - + } else { + //When the package is complete, rerender + this.listenTo( + MetacatUI.rootDataPackage, + "add:EML", + this.render, + ); } - else{ - - shareButton.remove(); - + } + + this.$el.html(this.template(attributes)); + + //Initialize dropdowns + this.$el.find(".dropdown-toggle").dropdown(); + + //Render the Share button + this.renderShareControl(); + + if (this.model.get("type") == "Metadata") { + //Add the title data-attribute attribute to the name cell + this.$el.find(".name").attr("data-attribute", "title"); + this.$el.addClass("folder"); + } else { + this.$el.addClass("data"); + } + + // Add tooltip to a disabled Replace link + $(this.$el) + .find(".replace.disabled") + .tooltip({ + title: + "You don't have sufficient privileges to replace this item.", + placement: "left", + trigger: "hover", + delay: { show: 400 }, + container: "body", + }); + + //Check if the data package is in progress of being uploaded + this.toggleSaving(); + + //Create tooltips based on the upload status + var uploadStatus = this.model.get("uploadStatus"), + errorMessage = this.model.get("errorMessage"); + + // Use a friendlier message for 401 errors (the one returned is a little hard to understand) + if (this.model.get("sysMetaErrorCode") == 401) { + // If the user at least has write permission, they cannot update the system metadata only, so show this message + /** @todo Do an object update when someone has write permission but not changePermission and is trying to change the system metadata (but not the access policy) */ + if (accessPolicy && accessPolicy.isAuthorized("write")) { + errorMessage = + "The owner of this data file has not given you permission to rename it or change the " + + MetacatUI.appModel.get("accessPolicyName") + + "."; + // Otherwise, assume they only have read access + } else { + errorMessage = + "The owner of this data file has not given you permission to edit this data file or change the " + + MetacatUI.appModel.get("accessPolicyName") + + "."; } - }, + } + + // When there's an error or a warninig + if (uploadStatus == "e" && errorMessage) { + var tooltipClass = uploadStatus == "e" ? "error" : ""; + + this.$(".status .icon").tooltip({ + placement: "top", + trigger: "hover", + html: true, + title: + "<div class='status-tooltip " + + tooltipClass + + "'><h6>Issue saving:</h6><div>" + + errorMessage + + "</div></div>", + container: "body", + }); + + this.$el.removeClass("loading"); + } else if ( + (!uploadStatus || uploadStatus == "c" || uploadStatus == "q") && + attributes.numAttributes == 0 + ) { + this.$(".status .icon").tooltip({ + placement: "top", + trigger: "hover", + html: true, + title: + "<div class='status-tooltip'>This file needs to be described - Click 'Describe'</div>", + container: "body", + }); + + this.$el.removeClass("loading"); + } else if ( + attributes.hasInvalidAttribute || + !attributes.entityIsValid + ) { + this.$(".status .icon").tooltip({ + placement: "top", + trigger: "hover", + html: true, + title: + "<div class='status-tooltip'>There is missing information about this file. Click 'Describe'</div>", + container: "body", + }); + + this.$el.removeClass("loading"); + } else if (uploadStatus == "c") { + this.$(".status .icon").tooltip({ + placement: "top", + trigger: "hover", + html: true, + title: "<div class='status-tooltip'>Complete</div>", + container: "body", + }); + + this.$el.removeClass("loading"); + } else if (uploadStatus == "l") { + this.$(".status .icon").tooltip({ + placement: "top", + trigger: "hover", + html: true, + title: "<div class='status-tooltip'>Reading file...</div>", + container: "body", + }); + + this.$el.addClass("loading"); + } else if (uploadStatus == "p") { + var model = this.model; + + this.$(".status .progress").tooltip({ + placement: "top", + trigger: "hover", + html: true, + title: function () { + if (model.get("numSaveAttempts") > 0) { + return ( + "<div class='status-tooltip'>Something went wrong during upload. <br/> Trying again... (attempt " + + (model.get("numSaveAttempts") + 1) + + " of 3)</div>" + ); + } else if (model.get("uploadProgress")) { + var percentDone = model.get("uploadProgress").toString(); + if (percentDone.indexOf(".") > -1) + percentDone = percentDone.substring( + 0, + percentDone.indexOf("."), + ); + } else var percentDone = "0"; + + return ( + "<div class='status-tooltip'>Uploading: " + + percentDone + + "%</div>" + ); + }, + container: "body", + }); + + this.$el.addClass("loading"); + } else { + this.$el.removeClass("loading"); + } + + //Listen to changes to the upload progress of this object + this.listenTo( + this.model, + "change:uploadProgress", + this.showUploadProgress, + ); + + //Listen to changes to the upload status of the entire package + this.listenTo( + MetacatUI.rootDataPackage.packageModel, + "change:uploadStatus", + this.toggleSaving, + ); + + //listen for changes to rerender the view + this.listenTo( + this.model, + "change:fileName change:title change:id change:formatType " + + "change:formatId change:type change:resourceMap change:documents change:isDocumentedBy " + + "change:size change:nodeLevel change:uploadStatus", + this.render, + ); // render changes to the item + + var view = this; + this.listenTo(this.model, "replace", function (newModel) { + view.model = newModel; + view.render(); + }); + } else { + this.isMetadata = false; + // format metadata object title + if ( + attributes.isMetadata || + this.model.getFormat() == "metadata" || + this.model.get("id") == this.currentlyViewing + ) { + attributes.title = "Metadata: " + this.model.get("fileName"); + attributes.icon = "icon-file-text"; + attributes.metricIcon = "icon-eye-open"; + this.isMetadata = true; + this.$el.attr("data-packageId", this.dataPackageId); + } + + var objectTitleTooltip = + attributes.title || attributes.fileName || attributes.id; + attributes.objectTitle = + objectTitleTooltip.length > 150 + ? objectTitleTooltip.slice(0, 75) + + "..." + + objectTitleTooltip.slice( + objectTitleTooltip.length - 75, + objectTitleTooltip.length, + ) + : objectTitleTooltip; + + attributes.fileType = this.model.getFormat(); + attributes.isFolder = false; + //Determine the icon type based on format type + if (this.model.getFormat() == "program") + attributes.icon = "icon-code"; + else if (this.model.getFormat() == "data") + attributes.icon = "icon-table"; + else if (this.model.getFormat() == "image/jpeg") + attributes.icon = "icon-picture"; + else if (this.model.getFormat() == "PDF") + attributes.icon = "icon-file"; + else attributes.icon = "icon-table"; + + attributes.id = this.model.get("id"); + attributes.memberRowMetrics = null; + var metricToolTip = null, + view = this; + + // Insert metrics for this item, + // if the model has already been fethced. + if (this.metricsModel.get("views") !== null) { + metricToolTip = this.getMemberRowMetrics(view.id); + attributes.memberRowMetrics = metricToolTip.split(" ")[0]; + } else { + // Update the metrics later on + // If the fetch() is still in progress. + this.listenTo(this.metricsModel, "sync", function () { + metricToolTip = this.getMemberRowMetrics(view.id); + let readsCell = this.$( + '.metrics-count.downloads[data-id="' + view.id + '"]', + ); + metricToolTip = view.getMemberRowMetrics(view.id); + if (typeof metricToolTip !== "undefined" && metricToolTip) + readsCell.html( + this.metricTemplate({ + metricIcon: attributes.metricIcon, + memberRowMetrics: metricToolTip.split(" ")[0], + }), + ); + }); + } + + // add nodeLevel for displaying indented filename + attributes.nodeLevel = 1; + if ( + !( + attributes.isMetadata || + this.model.getFormat() == "metadata" || + this.model.get("id") == this.currentlyViewing + ) + ) { + attributes.metricIcon = "icon-cloud-download"; + + this.$el.addClass(); + if ( + this.itemPath && + typeof this.itemPath !== undefined && + this.itemPath != "/" + ) { + itemPathParts = this.itemPath.split("/"); + + attributes.nodeLevel = itemPathParts.length; + + if (this.itemPath.startsWith("/")) { + attributes.nodeLevel -= 1; + } + if (this.itemPath.endsWith("/")) { + attributes.nodeLevel -= 1; + } - /** Close the view and remove it from the DOM */ - onClose: function(){ - this.remove(); // remove for the DOM, stop listening - this.off(); // remove callbacks, prevent zombies + // var parent = itemPathParts[itemPathParts.length - 2]; + var parentPath = itemPathParts.slice(0, -1).join("/"); - }, + if (parentPath !== undefined) { + this.$el.attr("data-parent", parentPath); + } + } else { + attributes.nodeLevel = 1; + this.$el.attr("data-packageId", this.dataPackageId); + } + } + + if (attributes.nodeLevel == 1) { + this.$el.attr("data-packageId", this.dataPackageId); + } + + //Download button + attributes.downloadUrl = undefined; + if ( + this.model.get("dataUrl") !== undefined || + this.model.get("url") !== undefined || + this.model.url() !== undefined + ) { + if (this.model.get("dataUrl") !== undefined) { + attributes.downloadUrl = this.model.get("dataUrl"); + } else if (this.model.get("url") !== undefined) { + attributes.downloadUrl = this.model.get("url"); + } else if (this.model.url() !== undefined) { + var downloadUrl = this.model.url(); + attributes.downloadUrl = downloadUrl.replace( + "/meta/", + "/object/", + ); + } + } + this.downloadButtonView = new DownloadButtonView({ + model: this.model, + view: "actionsView", + }); + this.downloadButtonView.render(); + + let id = this.model.get("id"); + let infoLink = + MetacatUI.root + + "/view/" + + encodeURIComponent(this.currentlyViewing) + + "#" + + encodeURIComponent(id); + attributes.moreInfoLink = infoLink; + + attributes.insertInfoIcon = this.insertInfoIcon; + + this.$el.html(this.dataItemHierarchyTemplate(attributes)); + + this.$(".downloadAction").html(this.downloadButtonView.el); + + // add tooltip for metrics in package table + this.$(".packageTable-resultItem").tooltip({ + placement: "top", + trigger: "hover", + delay: 300, + title: metricToolTip, + }); + + this.$(".fileTitle") + .addClass("tooltip-this") + .attr("data-placement", "top") + .attr("data-trigger", "hover") + .attr("data-delay", "300") + .attr("data-title", objectTitleTooltip); + } + } + + this.$el.data({ + view: this, + model: this.model, + }); - /** + return this; + }, + + /** + * Renders a button that opens the AccessPolicyView for editing permissions on this data item + * @since 2.15.0 + */ + renderShareControl: function () { + //Get the Share button element + var shareButton = this.$(".sharing button"); + + if ( + this.parentEditorView && + this.parentEditorView.isAccessPolicyEditEnabled() + ) { + //Start a title for the button tooltip + var sharebuttonTitle; + + // If the user is not authorized to change the permissions of + // this object, then disable the share button + if (this.canShare) { + shareButton.removeClass("disabled"); + sharebuttonTitle = "Share this item with others"; + } else { + shareButton.addClass("disabled"); + sharebuttonTitle = "You are not authorized to share this item."; + } + + // Set up tooltips for share button + shareButton.tooltip({ + title: sharebuttonTitle, + placement: "top", + container: this.el, + trigger: "hover", + delay: { show: 400 }, + }); + } else { + shareButton.remove(); + } + }, + + /** Close the view and remove it from the DOM */ + onClose: function () { + this.remove(); // remove for the DOM, stop listening + this.off(); // remove callbacks, prevent zombies + }, + + /** Generate a unique id for each data item in the table TODO: This could be replaced with the DataONE identifier */ - generateId: function() { - var idStr = ''; // the id to return - var length = 30; // the length of the generated string - var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'.split(''); - - for (var i = 0; i < length; i++) { - idStr += chars[Math.floor(Math.random() * chars.length)]; - } - - return idStr; - }, - - /** - * Update the folder name based on the scimeta title - * - * @param e The event triggering this method - */ - updateName: function(e) { - - - - var enteredText = this.cleanInput($(e.target).text().trim()); - - // Set the title if this item is metadata or set the file name - // if its not - if(this.model.get("type") == "Metadata") { - var title = this.model.get("title"); - - // Get the current title which is either an array of titles - // or a single string. When it's an array of strings, we - // use the first as the canonical title - var currentTitle = Array.isArray(title) ? title[0] : title; - - // Don't set the title if it hasn't changed or is empty - if (enteredText !== "" && - currentTitle !== enteredText && - enteredText !== "Untitled dataset") { - // Set the new title, upgrading any title attributes - // that aren't Arrays into Arrays - if ((Array.isArray(title) && title.length < 2) || typeof title == "string") { - this.model.set("title", [ enteredText ]); - } else { - title[0] = enteredText; - } - } - } else { - this.model.set("fileName", enteredText); - - // Reset sysMetaUploadStatus only if this item doesn't - // have content changes. This is here because replaceFile - // sets sysMetaUploadStatus to "c" to prevent the editor - // from updating sysmeta after the update call - if (!this.model.get("hasContentChanges")) { - this.model.set("sysMetaUploadStatus", null); - } - } - }, - - /** + generateId: function () { + var idStr = ""; // the id to return + var length = 30; // the length of the generated string + var chars = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".split( + "", + ); + + for (var i = 0; i < length; i++) { + idStr += chars[Math.floor(Math.random() * chars.length)]; + } + + return idStr; + }, + + /** + * Update the folder name based on the scimeta title + * + * @param e The event triggering this method + */ + updateName: function (e) { + var enteredText = this.cleanInput($(e.target).text().trim()); + + // Set the title if this item is metadata or set the file name + // if its not + if (this.model.get("type") == "Metadata") { + var title = this.model.get("title"); + + // Get the current title which is either an array of titles + // or a single string. When it's an array of strings, we + // use the first as the canonical title + var currentTitle = Array.isArray(title) ? title[0] : title; + + // Don't set the title if it hasn't changed or is empty + if ( + enteredText !== "" && + currentTitle !== enteredText && + enteredText !== "Untitled dataset" + ) { + // Set the new title, upgrading any title attributes + // that aren't Arrays into Arrays + if ( + (Array.isArray(title) && title.length < 2) || + typeof title == "string" + ) { + this.model.set("title", [enteredText]); + } else { + title[0] = enteredText; + } + } + } else { + this.model.set("fileName", enteredText); + + // Reset sysMetaUploadStatus only if this item doesn't + // have content changes. This is here because replaceFile + // sets sysMetaUploadStatus to "c" to prevent the editor + // from updating sysmeta after the update call + if (!this.model.get("hasContentChanges")) { + this.model.set("sysMetaUploadStatus", null); + } + } + }, + + /** Handle the add file event, showing the file picker dialog Multiple files are allowed using the shift and or option/alt key @param {Event} event */ - handleAddFiles: function(event) { - - event.stopPropagation(); - var fileUploadElement = this.$(".file-upload"); + handleAddFiles: function (event) { + event.stopPropagation(); + var fileUploadElement = this.$(".file-upload"); - fileUploadElement.val(""); + fileUploadElement.val(""); - if ( fileUploadElement ) { - fileUploadElement.click(); - } - event.preventDefault(); + if (fileUploadElement) { + fileUploadElement.click(); + } + event.preventDefault(); + }, - }, - - /** + /** With a file list from the file picker or drag and drop, add the files to the collection @param {Event} event */ - addFiles: function(event) { - - var fileList, // The list of chosen files - parentDataPackage, // The id of the first resource of this row's scimeta - self = this; // A reference to this view - - event.stopPropagation(); - event.preventDefault(); - // handle drag and drop files - if ( typeof event.originalEvent.dataTransfer !== "undefined" ) { - fileList = event.originalEvent.dataTransfer.files; - - // handle file picker files - } else { - if ( event.target ) { - fileList = event.target.files; - } - - } - this.$el.removeClass("droppable"); - - // Find the correct collection to add to. Use JQuery's delegateTarget - // attribute corresponding to the element where the event handler was attached - if ( typeof event.delegateTarget.dataset.id !== "undefined" ) { - this.parentSciMeta = this.getParentScienceMetadata(event); - this.collection = this.getParentDataPackage(event); - - // Read each file, and make a DataONEObject - _.each(fileList, function(file) { - - var uploadStatus = "l", - errorMessage = ""; - - if( file.size == 0 ){ - uploadStatus = "e"; - errorMessage = "This is an empty file. It won't be included in the dataset."; - } - - var dataONEObject = new DataONEObject({ - synced: true, - type: "Data", - fileName: file.name, - size: file.size, - mediaType: file.type, - uploadFile: file, - uploadStatus: uploadStatus, - errorMessage: errorMessage, - isDocumentedBy: [this.parentSciMeta.id], - isDocumentedByModels: [this.parentSciMeta], - resourceMap: [this.collection.packageModel.id] - }); - - // Add it to the parent collection - this.collection.add(dataONEObject); - - // Asychronously calculate the checksum - if ( dataONEObject.get("uploadFile") && ! dataONEObject.get("checksum") ) { - dataONEObject.stopListening(dataONEObject, "checksumCalculated"); - dataONEObject.listenToOnce(dataONEObject, "checksumCalculated", dataONEObject.save); - try { - dataONEObject.calculateChecksum(); - } catch (exception) { - // TODO: Fail gracefully here for the user - } - } - - - }, this); - - } - - }, - - /** Show the drop zone for this row in the table */ - showDropzone: function() { - if ( this.model.get("type") !== "Metadata" ) return; - this.$el.addClass("droppable"); - - }, - - /** Hide the drop zone for this row in the table */ - hideDropzone: function(event) { - if ( this.model.get("type") !== "Metadata" ) return; - this.$el.removeClass("droppable"); - - }, - - /** - * Handle the user's click of the Replace item in the DataItemView - * dropdown. Triggers replaceFile after some basic validation. - * - * Called indirectly via the "click" event on elements with the - * class .replaceFile. See this View's events map. - * - * @param {MouseEvent} event Browser Click event - */ - handleReplace: function(event) { - event.stopPropagation(); - - // Stop immediately if we know the user doesn't have privs - if (!this.canWrite) { - event.preventDefault(); - return; - } - - var fileReplaceElement = $(event.target) - .parents(".dropdown-menu") - .children(".file-replace") - - if (!fileReplaceElement) { - console.log("Unable to find Replace file picker."); - - return; - } - - fileReplaceElement.val(""); - fileReplaceElement.trigger("click"); - - event.preventDefault(); - }, - - /** - * Replace a file (DataONEObject) in the collection with another one - * from a file picker. Maintains attributes on the original - * DataONEObject and maintains the entity information in the parent - * collection's metadata record (i.e., keeps your attributes, etc.). - * - * Called indirectly via the "change" event on elements with the - * class .file-upload. See this View's events map. - * - * The bulk of the work is done in a try-catch block to catch - * mistakes that would cause the editor to get into a broken state. - * On error, we attempt to return the editor back to its pre-replace - * state. - * - * @param {Event} event - */ - replaceFile: function(event) { - event.stopPropagation(); - event.preventDefault(); - - if (!this.canWrite) { - return; - } - - var fileList = event.target.files; - - // Pre-check fileList value to make sure we can work with it - if (fileList.length != 1) { - // TODO: Show error, find out how to do this - return; - } - - if (typeof event.delegateTarget.dataset.id === "undefined") { - // TODO: Show error, find out how to do this - return; - } - - // Save uploadStatus for reverting if need to - var oldUploadStatus = this.model.get("uploadStatus"); - - var file = fileList[0], - uploadStatus = "q", - errorMessage = ""; - - if (file.size == 0 ) { - uploadStatus = "e"; - errorMessage = "This is an empty file. It won't be included in the dataset."; - } - - if (!this.model) { - console.log("Couldn't find model we're supposed to be replacing. Stopping."); - return; - } - - // Copy model attributes aside for reverting on error - var newAttributes = { - synced: false, - fileName: file.name, - size: file.size, - mediaType: file.type, - uploadFile: file, - hasContentChanges: true, - checksum: null, - uploadStatus: uploadStatus, - sysMetaUploadStatus: "c", // I set this so DataPackage::save - // wouldn't try to update the sysmeta after the update - errorMessage: errorMessage - }; - - // Save a copy of the attributes we're changing so we can revert - // later if we encounter an exception - var oldAttributes = {}; - _.each(Object.keys(newAttributes), function(k) { - oldAttributes[k] = _.clone(this.model.get(k)); - }, this); - - oldAttributes["uploadStatus"] = oldUploadStatus; + addFiles: function (event) { + var fileList, // The list of chosen files + parentDataPackage, // The id of the first resource of this row's scimeta + self = this; // A reference to this view + + event.stopPropagation(); + event.preventDefault(); + // handle drag and drop files + if (typeof event.originalEvent.dataTransfer !== "undefined") { + fileList = event.originalEvent.dataTransfer.files; + + // handle file picker files + } else { + if (event.target) { + fileList = event.target.files; + } + } + this.$el.removeClass("droppable"); + + // Find the correct collection to add to. Use JQuery's delegateTarget + // attribute corresponding to the element where the event handler was attached + if (typeof event.delegateTarget.dataset.id !== "undefined") { + this.parentSciMeta = this.getParentScienceMetadata(event); + this.collection = this.getParentDataPackage(event); + + // Read each file, and make a DataONEObject + _.each( + fileList, + function (file) { + var uploadStatus = "l", + errorMessage = ""; + + if (file.size == 0) { + uploadStatus = "e"; + errorMessage = + "This is an empty file. It won't be included in the dataset."; + } + var dataONEObject = new DataONEObject({ + synced: true, + type: "Data", + fileName: file.name, + size: file.size, + mediaType: file.type, + uploadFile: file, + uploadStatus: uploadStatus, + errorMessage: errorMessage, + isDocumentedBy: [this.parentSciMeta.id], + isDocumentedByModels: [this.parentSciMeta], + resourceMap: [this.collection.packageModel.id], + }); + + // Add it to the parent collection + this.collection.add(dataONEObject); + + // Asychronously calculate the checksum + if ( + dataONEObject.get("uploadFile") && + !dataONEObject.get("checksum") + ) { + dataONEObject.stopListening( + dataONEObject, + "checksumCalculated", + ); + dataONEObject.listenToOnce( + dataONEObject, + "checksumCalculated", + dataONEObject.save, + ); try { - - this.model.set(newAttributes); - - // Attempt the formatId. Defaults to app/octet-stream - this.model.set("formatId", this.model.getFormatId()); - - // Grab a reference to the entity in the EML for the object - // we're replacing - this.parentSciMeta = this.getParentScienceMetadata(event); - var entity = null; - - if (this.parentSciMeta) { - entity = this.parentSciMeta.getEntity(this.model); - } - - // Eagerly update the PID for this object so we can update - // the matching EML entity - this.model.updateID(); - - // Update the EML entity with the new id - if (entity) { - entity.set("xmlID", this.model.getXMLSafeID()); - } - - this.render(); - - if (this.model.get("uploadFile") && !this.model.get("checksum")) { - - try { - this.model.calculateChecksum(); - } catch (exception) { - // TODO: Fail gracefully here for the user - } - } - - MetacatUI.rootDataPackage.packageModel.set("changed", true); - - // Last, provided a visual indication the replace was completed - var describeButton = this.$el - .children(".controls") - .children(".btn-group") - .children("button.edit") - .first(); - - if (describeButton.length != 1) { - return; - } - - var oldText = describeButton.html(); - - describeButton.html('<i class="icon icon-ok success" /> Replaced'); - - var previousBtnClasses = describeButton.attr("class"); - describeButton.removeClass("warning error").addClass("message"); - - window.setTimeout(function() { - describeButton.html(oldText); - describeButton.addClass(previousBtnClasses).removeClass("message"); - }, 3000); - } catch (error) { - console.log("Error replacing: ", error); - - // Revert changes to the attributes - this.model.set(oldAttributes); - this.model.set("formatId", this.model.getFormatId()); - this.model.set("sysMetaUploadStatus", "c"); // Prevents a sysmeta update - this.model.resetID(); - - this.render(); - } - - return; - }, - - /** - Handle remove events for this row in the data package table - @param {Event} event - */ - handleRemove: function(event) { - var eventId, // The id of the row of this event - removalIds = [], // The list of target ids to remove - dataONEObject, // The model represented by this row - documents; // The list of ids documented by this row (if meta) - - event.stopPropagation(); - event.preventDefault(); - - // Get the row id, add it to the remove list - if ( typeof event.delegateTarget.dataset.id !== "undefined" ) { - eventId = event.delegateTarget.dataset.id; - removalIds.push(eventId); - } - - this.parentSciMeta = this.getParentScienceMetadata(event); - - if(!this.parentSciMeta){ - this.$(".status .icon, .status .progress").tooltip("hide").tooltip("destroy"); - - // Remove the row - this.remove(); - return; - } - - this.collection = this.getParentDataPackage(event); - - // Get the corresponding model - if ( typeof eventId !== "undefined" ) { - dataONEObject = this.collection.get(eventId); + dataONEObject.calculateChecksum(); + } catch (exception) { + // TODO: Fail gracefully here for the user } - - // Is it nested science metadata? - if ( dataONEObject && dataONEObject.get("type") == "Metadata" ) { - - // We also remove the data documented by these metadata - documents = dataONEObject.get("documents"); - - if ( documents.length > 0 ) { - _.each(documents, removalIds.push()); - } - } - //Data objects may need to be removed from the EML model entities list - else if(dataONEObject && this.parentSciMeta.type == "EML"){ - - var matchingEntity = this.parentSciMeta.getEntity(dataONEObject); - - if(matchingEntity) - this.parentSciMeta.removeEntity(matchingEntity); - - } - - // Remove the id from the documents array in the science metadata - _.each(removalIds, function(id) { - var documents = this.parentSciMeta.get("documents"); - var index = documents.indexOf(id); - if ( index > -1 ) { - this.parentSciMeta.get("documents").splice(index, 1); - - } - }, this); - - // Remove each object from the collection - this.collection.remove(removalIds); - - this.$(".status .icon, .status .progress").tooltip("hide").tooltip("destroy"); - - // Remove the row - this.remove(); - - MetacatUI.rootDataPackage.packageModel.set("changed", true); - - }, - - /** - * Return the parent science metadata model associated with the - * data or metadata row of the UI event - * @param {Event} event - */ - getParentScienceMetadata: function(event) { - var parentMetadata, // The parent metadata array in the collection - eventModels, // The models associated with the event's table row - eventModel, // The model associated with the event's table row - parentSciMeta; // The parent science metadata for the event model - - if ( typeof event.delegateTarget.dataset.id !== "undefined" ) { - eventModels = MetacatUI.rootDataPackage.where({ - id: event.delegateTarget.dataset.id - }); - - if ( eventModels.length > 0 ) { - eventModel = eventModels[0]; - - } else { - return; - } - - // Is this a Data or Metadata model? - if ( eventModel.get && eventModel.get("type") === "Metadata" ) { - return eventModel; - - } else { - // It's data, get the parent scimeta - parentMetadata = MetacatUI.rootDataPackage.where({ - id: Array.isArray(eventModel.get("isDocumentedBy"))? eventModel.get("isDocumentedBy")[0] : null - }); - - if ( parentMetadata.length > 0 ) { - parentSciMeta = parentMetadata[0]; - return parentSciMeta; - - } else { - //If there is only one metadata model in the root data package, then use that metadata model - var metadataModels = MetacatUI.rootDataPackage.where({ - type: "Metadata" - }); - - if(metadataModels.length == 1) - return metadataModels[0]; - - } - } - } - }, - - /** - * Return the parent data package collection associated with the - * data or metadata row of the UI event - * @param {Event} event - */ - getParentDataPackage: function(event) { - var parentSciMeta, - parenResourceMaps, - parentResourceMapId; - - if ( typeof event.delegateTarget.dataset.id !== "undefined" ) { - - parentSciMeta = this.getParentScienceMetadata(event); - - if ( parentSciMeta.get && parentSciMeta.get("resourceMap").length > 0 ) { - parentResourceMaps = parentSciMeta.get("resourceMap"); - - if ( ! MetacatUI.rootDataPackage.packageModel.get("latestVersion") ) { - // Decide how to handle this by calling model.findLatestVersion() - // and listen for the result, setting getParentDataPackage() as the callback? - - } else { - parentResourceMapId = MetacatUI.rootDataPackage.packageModel.get("latestVersion"); - - } - - } else { - console.log("There is no resource map associated with the science metadata."); - - } - - // Is this the root package or a nested package? - if ( MetacatUI.rootDataPackage.packageModel.id === parentResourceMapId ) { - return MetacatUI.rootDataPackage; - - // A nested package - } else { - return MetacatUI.rootDataPackage.where({id: parentResourceMapId})[0]; - - } - } - }, - - /** - * Removes invalid characters and formatting from the given input string - * @param {string} input The string to clean - * @return {string} - */ - cleanInput: function(input){ - // 1. remove line breaks / Mso classes - var stringStripper = /(\n|\r| class=(")?Mso[a-zA-Z]+(")?)/g; - var output = input.replace(stringStripper, ' '); - - // 2. strip Word generated HTML comments - var commentSripper = new RegExp('<!--(.*?)-->','g'); - output = output.replace(commentSripper, ''); - var tagStripper = new RegExp('<(/)*(meta|link|span|\\?xml:|st1:|o:|font)(.*?)>','gi'); - - // 3. remove tags leave content if any - output = output.replace(tagStripper, ''); - - // 4. Remove everything in between and including tags '<style(.)style(.)>' - var badTags = ['style', 'script','applet','embed','noframes','noscript']; - - for (var i=0; i< badTags.length; i++) { - tagStripper = new RegExp('<'+badTags[i]+'.*?'+badTags[i]+'(.*?)>', 'gi'); - output = output.replace(tagStripper, ''); - } - - // 5. remove attributes ' style="..."' - var badAttributes = ['style', 'start']; - for (var i=0; i< badAttributes.length; i++) { - var attributeStripper = new RegExp(' ' + badAttributes[i] + '="(.*?)"','gi'); - output = output.replace(attributeStripper, ''); - } - - output = EML.prototype.cleanXMLText(output); - - return output; - }, - - /** - * Style this table row to indicate it will be removed - */ - previewRemove: function(){ - this.$el.toggleClass("remove-preview"); - }, - - /** - * Clears the text in the cell if the text was the default. We add - * an 'empty' class, and remove it when the user focuses back out. - * @param {Event} e - */ - emptyName: function(e){ - - var editableCell = this.$(".canRename [contenteditable]"); - - editableCell.tooltip('hide'); - - if(editableCell.text().indexOf("Untitled") > -1){ - editableCell.attr("data-original-text", editableCell.text().trim()) - .text("") - .addClass("empty") - .on("focusout", function(){ - if(!editableCell.text()) - editableCell.text(editableCell.attr("data-original-text")).removeClass("empty"); - }); } }, + this, + ); + } + }, + + /** Show the drop zone for this row in the table */ + showDropzone: function () { + if (this.model.get("type") !== "Metadata") return; + this.$el.addClass("droppable"); + }, + + /** Hide the drop zone for this row in the table */ + hideDropzone: function (event) { + if (this.model.get("type") !== "Metadata") return; + this.$el.removeClass("droppable"); + }, + + /** + * Handle the user's click of the Replace item in the DataItemView + * dropdown. Triggers replaceFile after some basic validation. + * + * Called indirectly via the "click" event on elements with the + * class .replaceFile. See this View's events map. + * + * @param {MouseEvent} event Browser Click event + */ + handleReplace: function (event) { + event.stopPropagation(); + + // Stop immediately if we know the user doesn't have privs + if (!this.canWrite) { + event.preventDefault(); + return; + } + + var fileReplaceElement = $(event.target) + .parents(".dropdown-menu") + .children(".file-replace"); + + if (!fileReplaceElement) { + console.log("Unable to find Replace file picker."); + + return; + } + + fileReplaceElement.val(""); + fileReplaceElement.trigger("click"); + + event.preventDefault(); + }, + + /** + * Replace a file (DataONEObject) in the collection with another one + * from a file picker. Maintains attributes on the original + * DataONEObject and maintains the entity information in the parent + * collection's metadata record (i.e., keeps your attributes, etc.). + * + * Called indirectly via the "change" event on elements with the + * class .file-upload. See this View's events map. + * + * The bulk of the work is done in a try-catch block to catch + * mistakes that would cause the editor to get into a broken state. + * On error, we attempt to return the editor back to its pre-replace + * state. + * + * @param {Event} event + */ + replaceFile: function (event) { + event.stopPropagation(); + event.preventDefault(); + + if (!this.canWrite) { + return; + } + + var fileList = event.target.files; + + // Pre-check fileList value to make sure we can work with it + if (fileList.length != 1) { + // TODO: Show error, find out how to do this + return; + } + + if (typeof event.delegateTarget.dataset.id === "undefined") { + // TODO: Show error, find out how to do this + return; + } + + // Save uploadStatus for reverting if need to + var oldUploadStatus = this.model.get("uploadStatus"); + + var file = fileList[0], + uploadStatus = "q", + errorMessage = ""; + + if (file.size == 0) { + uploadStatus = "e"; + errorMessage = + "This is an empty file. It won't be included in the dataset."; + } + + if (!this.model) { + console.log( + "Couldn't find model we're supposed to be replacing. Stopping.", + ); + return; + } + + // Copy model attributes aside for reverting on error + var newAttributes = { + synced: false, + fileName: file.name, + size: file.size, + mediaType: file.type, + uploadFile: file, + hasContentChanges: true, + checksum: null, + uploadStatus: uploadStatus, + sysMetaUploadStatus: "c", // I set this so DataPackage::save + // wouldn't try to update the sysmeta after the update + errorMessage: errorMessage, + }; + + // Save a copy of the attributes we're changing so we can revert + // later if we encounter an exception + var oldAttributes = {}; + _.each( + Object.keys(newAttributes), + function (k) { + oldAttributes[k] = _.clone(this.model.get(k)); + }, + this, + ); - /** - * Changes the access policy of a data object based on user input. - * - * @param {Event} e - The event that triggered this function as a callback - */ - changeAccessPolicy: function(e){ + oldAttributes["uploadStatus"] = oldUploadStatus; - if( typeof e === "undefined" || !e ) - return; + try { + this.model.set(newAttributes); - var accessPolicy = this.model.get("accessPolicy"); + // Attempt the formatId. Defaults to app/octet-stream + this.model.set("formatId", this.model.getFormatId()); - var makePublic = $(e.target).prop("checked"); + // Grab a reference to the entity in the EML for the object + // we're replacing + this.parentSciMeta = this.getParentScienceMetadata(event); + var entity = null; - //If the user has chosen to make this object private - if(!makePublic){ - if( accessPolicy ){ - //Make the access policy private - accessPolicy.makePrivate(); - } - else{ - //Create an access policy from the default settings - this.model.createAccessPolicy(); - //Make the access policy private - this.model.get("accessPolicy").makePrivate(); - } + if (this.parentSciMeta) { + entity = this.parentSciMeta.getEntity(this.model); + } - } - else{ - if( accessPolicy ){ - //Make the access policy public - accessPolicy.makePublic(); - } - else{ - //Create an access policy from the default settings - this.model.createAccessPolicy(); - //Make the access policy public - this.model.get("accessPolicy").makePublic(); - } - } - }, + // Eagerly update the PID for this object so we can update + // the matching EML entity + this.model.updateID(); - /** - * Shows form validation for this data item - * @param {string} attr The modal attribute that has been validated - * @param {string} errorMsg The validation error message to display - */ - showValidation: function(attr, errorMsg){ + // Update the EML entity with the new id + if (entity) { + entity.set("xmlID", this.model.getXMLSafeID()); + } - //Find the element that is required - var requiredEl = this.$("[data-category='" + attr + "']").addClass("error"); + this.render(); - //When it is updated, remove the error styling - this.listenToOnce(this.model, "change:" + attr, this.hideRequired); - }, + if (this.model.get("uploadFile") && !this.model.get("checksum")) { + try { + this.model.calculateChecksum(); + } catch (exception) { + // TODO: Fail gracefully here for the user + } + } - /** - * Hides the 'required' styling from this view - */ - hideRequired: function(){ + MetacatUI.rootDataPackage.packageModel.set("changed", true); - //Remove the error styling - this.$("[contenteditable].error").removeClass("error"); - }, + // Last, provided a visual indication the replace was completed + var describeButton = this.$el + .children(".controls") + .children(".btn-group") + .children("button.edit") + .first(); - /** - * Show the data item as saving - */ - showSaving: function(){ - this.$(".controls button").prop("disabled", true); + if (describeButton.length != 1) { + return; + } - if(this.model.get("type") != "Metadata") - this.$(".controls").prepend($(document.createElement("div")).addClass("disable-layer")); + var oldText = describeButton.html(); - this.$(".canRename > div").prop("contenteditable", false); - }, + describeButton.html('<i class="icon icon-ok success" /> Replaced'); - /** - * Hides the styles applied in {@link DataItemView#showSaving} - */ - hideSaving: function(){ - this.$(".controls button").prop("disabled", false); - this.$(".disable-layer").remove(); + var previousBtnClasses = describeButton.attr("class"); + describeButton.removeClass("warning error").addClass("message"); - //Make the name cell editable again - this.$(".canRename > div").prop("contenteditable", true); + window.setTimeout(function () { + describeButton.html(oldText); + describeButton.addClass(previousBtnClasses).removeClass("message"); + }, 3000); + } catch (error) { + console.log("Error replacing: ", error); - this.$el.removeClass("error-saving"); - }, + // Revert changes to the attributes + this.model.set(oldAttributes); + this.model.set("formatId", this.model.getFormatId()); + this.model.set("sysMetaUploadStatus", "c"); // Prevents a sysmeta update + this.model.resetID(); - toggleSaving: function(){ - if(this.model.get("uploadStatus") == "p" || - this.model.get("uploadStatus") == "l" || - ( this.model.get("uploadStatus") == "e" && this.model.get("type") != "Metadata") || - MetacatUI.rootDataPackage.packageModel.get("uploadStatus") == "p") - this.showSaving(); - else - this.hideSaving(); - - if(this.model.get("uploadStatus") == "e") - this.$el.addClass("error-saving"); - }, + this.render(); + } - /** - * Shows the current progress of the file upload - */ - showUploadProgress: function(){ + return; + }, - if(this.model.get("numSaveAttempts") > 0){ - this.$(".progress .bar").css("width", "100%"); - } - else{ - this.$(".progress .bar").css("width", this.model.get("uploadProgress") + "%"); - } - }, - - /** - * Determine whether this item can be shared - * - * Used to control whether the Share button in the template - * is enabled or not. - * - * Has special behavior depending on whether the item is metadata or - * not. If metadata (ie type is EML), also checks that the resource - * map can be shared. Otherwise, only checks if the data item can be - * shared. - * - * @return {boolean} Whether the item can be shared - * @since 2.15.0 + /** + Handle remove events for this row in the data package table + @param {Event} event */ - canShareItem: function() { - - if( this.parentEditorView ){ - if( this.parentEditorView.isAccessPolicyEditEnabled() ){ - if (this.model.type === "EML") { - // Check whether we can share the resource map - var pkgModel = MetacatUI.rootDataPackage.packageModel, - pkgAccessPolicy = pkgModel.get("accessPolicy"); - - var canShareResourceMap = pkgModel.isNew() || (pkgAccessPolicy && pkgAccessPolicy.isAuthorized("changePermission")); - - // Check whether we can share the metadata - var modelAccessPolicy = this.model.get("accessPolicy"); - var canShareMetadata = this.model.isNew() || (modelAccessPolicy && modelAccessPolicy.isAuthorized("changePermission")); - - // Only return true if we can share both - return canShareMetadata && canShareResourceMap; - } else { - return this.model.get("accessPolicy") && this.model.get("accessPolicy").isAuthorized("changePermission"); - } - } - } - }, - - downloadFile: function(e) { - this.downloadButtonView.download(e); - }, - - // Member row metrics for the package table - // Retrieving information from the Metrics Model's result details - getMemberRowMetrics: function(id) { - - if(typeof this.metricsModel !== "undefined"){ - var metricsResultDetails = this.metricsModel.get("resultDetails"); - - if( typeof metricsResultDetails !== "undefined" && metricsResultDetails ){ - var metricsPackageDetails = metricsResultDetails["metrics_package_counts"]; - - var objectLevelMetrics = metricsPackageDetails[id]; - if(typeof objectLevelMetrics !== "undefined") { - if(this.isMetadata) { - var reads = objectLevelMetrics["viewCount"]; - } - else { - var reads = objectLevelMetrics["downloadCount"]; - } - } - else{ - var reads = 0; - } - } - else{ - var reads = 0; - } - - } - - if((typeof reads !== "undefined") && reads){ - // giving labels - if(this.isMetadata && reads == 1) - reads += " view"; - else if(this.isMetadata) - reads += " views"; - else if(reads == 1) - reads += " download"; - else - reads += " downloads"; - } - else { - // returning an empty string if the metrics are 0 - reads = ""; - } - - return reads; + handleRemove: function (event) { + var eventId, // The id of the row of this event + removalIds = [], // The list of target ids to remove + dataONEObject, // The model represented by this row + documents; // The list of ids documented by this row (if meta) + + event.stopPropagation(); + event.preventDefault(); + + // Get the row id, add it to the remove list + if (typeof event.delegateTarget.dataset.id !== "undefined") { + eventId = event.delegateTarget.dataset.id; + removalIds.push(eventId); + } + + this.parentSciMeta = this.getParentScienceMetadata(event); + + if (!this.parentSciMeta) { + this.$(".status .icon, .status .progress") + .tooltip("hide") + .tooltip("destroy"); + + // Remove the row + this.remove(); + return; + } + + this.collection = this.getParentDataPackage(event); + + // Get the corresponding model + if (typeof eventId !== "undefined") { + dataONEObject = this.collection.get(eventId); + } + + // Is it nested science metadata? + if (dataONEObject && dataONEObject.get("type") == "Metadata") { + // We also remove the data documented by these metadata + documents = dataONEObject.get("documents"); + + if (documents.length > 0) { + _.each(documents, removalIds.push()); + } + } + //Data objects may need to be removed from the EML model entities list + else if (dataONEObject && this.parentSciMeta.type == "EML") { + var matchingEntity = this.parentSciMeta.getEntity(dataONEObject); + + if (matchingEntity) this.parentSciMeta.removeEntity(matchingEntity); + } + + // Remove the id from the documents array in the science metadata + _.each( + removalIds, + function (id) { + var documents = this.parentSciMeta.get("documents"); + var index = documents.indexOf(id); + if (index > -1) { + this.parentSciMeta.get("documents").splice(index, 1); + } }, - - }); - - return DataItemView; - }); + this, + ); + + // Remove each object from the collection + this.collection.remove(removalIds); + + this.$(".status .icon, .status .progress") + .tooltip("hide") + .tooltip("destroy"); + + // Remove the row + this.remove(); + + MetacatUI.rootDataPackage.packageModel.set("changed", true); + }, + + /** + * Return the parent science metadata model associated with the + * data or metadata row of the UI event + * @param {Event} event + */ + getParentScienceMetadata: function (event) { + var parentMetadata, // The parent metadata array in the collection + eventModels, // The models associated with the event's table row + eventModel, // The model associated with the event's table row + parentSciMeta; // The parent science metadata for the event model + + if (typeof event.delegateTarget.dataset.id !== "undefined") { + eventModels = MetacatUI.rootDataPackage.where({ + id: event.delegateTarget.dataset.id, + }); + + if (eventModels.length > 0) { + eventModel = eventModels[0]; + } else { + return; + } + + // Is this a Data or Metadata model? + if (eventModel.get && eventModel.get("type") === "Metadata") { + return eventModel; + } else { + // It's data, get the parent scimeta + parentMetadata = MetacatUI.rootDataPackage.where({ + id: Array.isArray(eventModel.get("isDocumentedBy")) + ? eventModel.get("isDocumentedBy")[0] + : null, + }); + + if (parentMetadata.length > 0) { + parentSciMeta = parentMetadata[0]; + return parentSciMeta; + } else { + //If there is only one metadata model in the root data package, then use that metadata model + var metadataModels = MetacatUI.rootDataPackage.where({ + type: "Metadata", + }); + + if (metadataModels.length == 1) return metadataModels[0]; + } + } + } + }, + + /** + * Return the parent data package collection associated with the + * data or metadata row of the UI event + * @param {Event} event + */ + getParentDataPackage: function (event) { + var parentSciMeta, parenResourceMaps, parentResourceMapId; + + if (typeof event.delegateTarget.dataset.id !== "undefined") { + parentSciMeta = this.getParentScienceMetadata(event); + + if ( + parentSciMeta.get && + parentSciMeta.get("resourceMap").length > 0 + ) { + parentResourceMaps = parentSciMeta.get("resourceMap"); + + if (!MetacatUI.rootDataPackage.packageModel.get("latestVersion")) { + // Decide how to handle this by calling model.findLatestVersion() + // and listen for the result, setting getParentDataPackage() as the callback? + } else { + parentResourceMapId = + MetacatUI.rootDataPackage.packageModel.get("latestVersion"); + } + } else { + console.log( + "There is no resource map associated with the science metadata.", + ); + } + + // Is this the root package or a nested package? + if ( + MetacatUI.rootDataPackage.packageModel.id === parentResourceMapId + ) { + return MetacatUI.rootDataPackage; + + // A nested package + } else { + return MetacatUI.rootDataPackage.where({ + id: parentResourceMapId, + })[0]; + } + } + }, + + /** + * Removes invalid characters and formatting from the given input string + * @param {string} input The string to clean + * @return {string} + */ + cleanInput: function (input) { + // 1. remove line breaks / Mso classes + var stringStripper = /(\n|\r| class=(")?Mso[a-zA-Z]+(")?)/g; + var output = input.replace(stringStripper, " "); + + // 2. strip Word generated HTML comments + var commentSripper = new RegExp("<!--(.*?)-->", "g"); + output = output.replace(commentSripper, ""); + var tagStripper = new RegExp( + "<(/)*(meta|link|span|\\?xml:|st1:|o:|font)(.*?)>", + "gi", + ); + + // 3. remove tags leave content if any + output = output.replace(tagStripper, ""); + + // 4. Remove everything in between and including tags '<style(.)style(.)>' + var badTags = [ + "style", + "script", + "applet", + "embed", + "noframes", + "noscript", + ]; + + for (var i = 0; i < badTags.length; i++) { + tagStripper = new RegExp( + "<" + badTags[i] + ".*?" + badTags[i] + "(.*?)>", + "gi", + ); + output = output.replace(tagStripper, ""); + } + + // 5. remove attributes ' style="..."' + var badAttributes = ["style", "start"]; + for (var i = 0; i < badAttributes.length; i++) { + var attributeStripper = new RegExp( + " " + badAttributes[i] + '="(.*?)"', + "gi", + ); + output = output.replace(attributeStripper, ""); + } + + output = EML.prototype.cleanXMLText(output); + + return output; + }, + + /** + * Style this table row to indicate it will be removed + */ + previewRemove: function () { + this.$el.toggleClass("remove-preview"); + }, + + /** + * Clears the text in the cell if the text was the default. We add + * an 'empty' class, and remove it when the user focuses back out. + * @param {Event} e + */ + emptyName: function (e) { + var editableCell = this.$(".canRename [contenteditable]"); + + editableCell.tooltip("hide"); + + if (editableCell.text().indexOf("Untitled") > -1) { + editableCell + .attr("data-original-text", editableCell.text().trim()) + .text("") + .addClass("empty") + .on("focusout", function () { + if (!editableCell.text()) + editableCell + .text(editableCell.attr("data-original-text")) + .removeClass("empty"); + }); + } + }, + + /** + * Changes the access policy of a data object based on user input. + * + * @param {Event} e - The event that triggered this function as a callback + */ + changeAccessPolicy: function (e) { + if (typeof e === "undefined" || !e) return; + + var accessPolicy = this.model.get("accessPolicy"); + + var makePublic = $(e.target).prop("checked"); + + //If the user has chosen to make this object private + if (!makePublic) { + if (accessPolicy) { + //Make the access policy private + accessPolicy.makePrivate(); + } else { + //Create an access policy from the default settings + this.model.createAccessPolicy(); + //Make the access policy private + this.model.get("accessPolicy").makePrivate(); + } + } else { + if (accessPolicy) { + //Make the access policy public + accessPolicy.makePublic(); + } else { + //Create an access policy from the default settings + this.model.createAccessPolicy(); + //Make the access policy public + this.model.get("accessPolicy").makePublic(); + } + } + }, + + /** + * Shows form validation for this data item + * @param {string} attr The modal attribute that has been validated + * @param {string} errorMsg The validation error message to display + */ + showValidation: function (attr, errorMsg) { + //Find the element that is required + var requiredEl = this.$("[data-category='" + attr + "']").addClass( + "error", + ); + + //When it is updated, remove the error styling + this.listenToOnce(this.model, "change:" + attr, this.hideRequired); + }, + + /** + * Hides the 'required' styling from this view + */ + hideRequired: function () { + //Remove the error styling + this.$("[contenteditable].error").removeClass("error"); + }, + + /** + * Show the data item as saving + */ + showSaving: function () { + this.$(".controls button").prop("disabled", true); + + if (this.model.get("type") != "Metadata") + this.$(".controls").prepend( + $(document.createElement("div")).addClass("disable-layer"), + ); + + this.$(".canRename > div").prop("contenteditable", false); + }, + + /** + * Hides the styles applied in {@link DataItemView#showSaving} + */ + hideSaving: function () { + this.$(".controls button").prop("disabled", false); + this.$(".disable-layer").remove(); + + //Make the name cell editable again + this.$(".canRename > div").prop("contenteditable", true); + + this.$el.removeClass("error-saving"); + }, + + toggleSaving: function () { + if ( + this.model.get("uploadStatus") == "p" || + this.model.get("uploadStatus") == "l" || + (this.model.get("uploadStatus") == "e" && + this.model.get("type") != "Metadata") || + MetacatUI.rootDataPackage.packageModel.get("uploadStatus") == "p" + ) + this.showSaving(); + else this.hideSaving(); + + if (this.model.get("uploadStatus") == "e") + this.$el.addClass("error-saving"); + }, + + /** + * Shows the current progress of the file upload + */ + showUploadProgress: function () { + if (this.model.get("numSaveAttempts") > 0) { + this.$(".progress .bar").css("width", "100%"); + } else { + this.$(".progress .bar").css( + "width", + this.model.get("uploadProgress") + "%", + ); + } + }, + + /** + * Determine whether this item can be shared + * + * Used to control whether the Share button in the template + * is enabled or not. + * + * Has special behavior depending on whether the item is metadata or + * not. If metadata (ie type is EML), also checks that the resource + * map can be shared. Otherwise, only checks if the data item can be + * shared. + * + * @return {boolean} Whether the item can be shared + * @since 2.15.0 + */ + canShareItem: function () { + if (this.parentEditorView) { + if (this.parentEditorView.isAccessPolicyEditEnabled()) { + if (this.model.type === "EML") { + // Check whether we can share the resource map + var pkgModel = MetacatUI.rootDataPackage.packageModel, + pkgAccessPolicy = pkgModel.get("accessPolicy"); + + var canShareResourceMap = + pkgModel.isNew() || + (pkgAccessPolicy && + pkgAccessPolicy.isAuthorized("changePermission")); + + // Check whether we can share the metadata + var modelAccessPolicy = this.model.get("accessPolicy"); + var canShareMetadata = + this.model.isNew() || + (modelAccessPolicy && + modelAccessPolicy.isAuthorized("changePermission")); + + // Only return true if we can share both + return canShareMetadata && canShareResourceMap; + } else { + return ( + this.model.get("accessPolicy") && + this.model.get("accessPolicy").isAuthorized("changePermission") + ); + } + } + } + }, + + downloadFile: function (e) { + this.downloadButtonView.download(e); + }, + + // Member row metrics for the package table + // Retrieving information from the Metrics Model's result details + getMemberRowMetrics: function (id) { + if (typeof this.metricsModel !== "undefined") { + var metricsResultDetails = this.metricsModel.get("resultDetails"); + + if ( + typeof metricsResultDetails !== "undefined" && + metricsResultDetails + ) { + var metricsPackageDetails = + metricsResultDetails["metrics_package_counts"]; + + var objectLevelMetrics = metricsPackageDetails[id]; + if (typeof objectLevelMetrics !== "undefined") { + if (this.isMetadata) { + var reads = objectLevelMetrics["viewCount"]; + } else { + var reads = objectLevelMetrics["downloadCount"]; + } + } else { + var reads = 0; + } + } else { + var reads = 0; + } + } + + if (typeof reads !== "undefined" && reads) { + // giving labels + if (this.isMetadata && reads == 1) reads += " view"; + else if (this.isMetadata) reads += " views"; + else if (reads == 1) reads += " download"; + else reads += " downloads"; + } else { + // returning an empty string if the metrics are 0 + reads = ""; + } + + return reads; + }, + }, + ); + + return DataItemView; +});
diff --git a/docs/docs/src_js_views_DataPackageView.js.html b/docs/docs/src_js_views_DataPackageView.js.html index 148116d5e..54e13a56b 100644 --- a/docs/docs/src_js_views_DataPackageView.js.html +++ b/docs/docs/src_js_views_DataPackageView.js.html @@ -44,935 +44,1041 @@

Source: src/js/views/DataPackageView.js

-
/* global define */
-define([
-    'jquery',
-    'underscore',
-    'backbone',
-    'localforage',
-    'collections/DataPackage',
-    'models/DataONEObject',
-    'models/PackageModel',
-    'models/metadata/ScienceMetadata',
-    'models/metadata/eml211/EML211',
-    'models/PackageModel',
-    'views/DataItemView',
-    'views/DownloadButtonView',
-    'text!templates/dataPackage.html',
-    'text!templates/dataPackageStart.html',
-    'text!templates/dataPackageHeader.html'],
-    function($, _, Backbone, LocalForage, DataPackage, DataONEObject, PackageModel, ScienceMetadata, EML211, Package, DataItemView,
-            DownloadButtonView, DataPackageTemplate, DataPackageStartTemplate, DataPackageHeaderTemplate) {
-        'use strict';
-
-        /**
-         * @class DataPackageView
-         * @classdesc The main view of a Data Package in MetacatUI.  The view is
-         *  a file/folder browser
-         * @classcategory Views
-         * @screenshot views/DataPackageView.png
-         * @extends Backbone.View
-         */
-        var DataPackageView = Backbone.View.extend(
-          /** @lends DataPackageView.prototype */{
-
-            type: "DataPackage",
-
-            tagName: "table",
-
-            className: "table table-striped table-hover",
-
-            id: "data-package-table",
-
-            events: {
-                "click .toggle-rows" 		   : "toggleRows", // Show/hide rows associated with event's metadata row
-                "click .message-row .addFiles" : "handleAddFiles",
-                "click .expand-control"   : "expand",
-			    "click .collapse-control" : "collapse",
-                "click .d1package-expand"   : "expandAll",
-			    "click .d1package-collapse" : "collapseAll"
-            },
-
-            subviews: {},
-
-            /**
-            * A reference to the parent EditorView that contains this view
-            * @type EditorView
-            * @since 2.15.0
-            */
-            parentEditorView: null,
-
-            template: _.template(DataPackageTemplate),
-            startMessageTemplate: _.template(DataPackageStartTemplate),
-            dataPackageHeaderTemplate: _.template(DataPackageHeaderTemplate),
-
-            // Models waiting for their parent folder to be rendered, hashed by parent id:
-            // {'parentid': [model1, model2, ...]}
-            delayedModels: {},
-
-            /* Flag indicating the open or closed state of the package rows */
-            isOpen: true,
-
-            initialize: function(options) {
-                if((options === undefined) || (!options)) var options = {};
-
-                if (!options.edit) {
-                    //The edit option will allow the user to edit the table
-                    this.edit = options.edit || false;
-                    this.mode = "view";
-                    this.packageId  = options.packageId	 || null;
-                    this.memberId	= options.memberId	 || null;
-                    this.attributes = options.attributes || null;
-                    this.dataPackage = options.dataPackage || new DataPackage();
-                    this.dataEntities = options.dataEntities || new Array();
-                    this.disablePackageDownloads = options.disablePackageDownloads || false;
-                    this.currentlyViewing = options.currentlyViewing || null;
-                    this.parentEditorView = options.parentView || null;
-                    this.title = options.title || "";
-                    this.packageTitle = options.packageTitle || "";
-                    this.nested = (typeof options.nested === "undefined")? false : options.nested;
-                    this.metricsModel = options.metricsModel;
-
-                    // set the package model
-                    this.packageModel = this.dataPackage.packageModel;
-
-                    this.listenTo(this.packageModel, "changeAll", this.render);
-                }
-                else {
-                    //Get the options sent to this view
-                    if(typeof options == "object"){
-                        //The edit option will allow the user to edit the table
-                        this.edit = options.edit || false;
-                        this.mode = "edit";
-
-                        //The data package to render
-                        this.dataPackage = options.dataPackage || new DataPackage();
-
-                        this.parentEditorView = options.parentEditorView || null;
-                    }
-                    //Create a new DataPackage collection if one wasn't sent
-                    else if(!this.dataPackage){
-                        this.dataPackage = new DataPackage();
-                    }
-
-                    return this;
-                }
-            },
-
-            /**
-             *  Render the DataPackage HTML
-             */
-            render: function() {
-                this.$el.addClass("download-contents table-condensed");
-                this.$el.append(this.template({
-                    edit: this.edit,
-                    dataPackageFiltering: MetacatUI.appModel.get("dataPackageFiltering") || false,
-                    dataPackageSorting: MetacatUI.appModel.get("dataPackageSorting") || false,
-                	loading: MetacatUI.appView.loadingTemplate({msg: "Loading files table... "}),
-                	id: this.dataPackage.get("id"),
-                    title   : this.title || "Files in this dataset",
-                    classes: "download-contents table-striped table-condensed table",
-                }));
-
-                if (this.edit) {
-                    // Listen for  add events because models are being merged
-                    this.listenTo(this.dataPackage, 'add', this.addOne);
-                    this.listenTo(this.dataPackage, "fileAdded", this.addOne);
-                }
-
-                // Render the current set of models in the DataPackage
-                this.addAll();
-
-                if (this.edit) {
-                    //If this is a new data package, then display a message and button
-                    if((this.dataPackage.length == 1 && this.dataPackage.models[0].isNew()) || !this.dataPackage.length){
-
-                        var messageRow = this.startMessageTemplate();
-
-                        this.$("tbody").append(messageRow);
-
-                        this.listenTo(this.dataPackage, "add", function(){
-                            this.$(".message-row").remove();
-                        });
-                    }
-
-                    //Render the Share control(s)
-                    this.renderShareControl();
-                }
-                else {
-                    // check for nessted datasets
-
-                    if (this.nested) {
-                        this.getNestedPackages();
-                    }
-                }
-
-                return this;
-            },
-
-            /**
-             * Add a single DataItemView row to the DataPackageView
-             */
-            addOne: function(item, dataPackage) {
-            	if(!item) return false;
-
-                //Don't add duplicate rows
-                if(this.$(".data-package-item[data-id='" + item.id + "']").length)
-                	return;
-
-                // Don't add data package
-                if ((item.get("formatType") == "RESOURCE") || (item.get("type") == "DataPackage")) {
-                    return;
-                }
-
-                var dataItemView, scimetaParent, parentRow, delayed_models;
-
-                if ( _.contains(Object.keys(this.subviews), item.id) ) {
-                    return false; // Don't double render
-
-                }
-
-                var itemPath = null,
-                    view = this;
-                if (!(_.isEmpty(this.atLocationObj))) {
-                    itemPath = this.atLocationObj[item.get("id")];
-                    if (itemPath[0] != "/") {
-                        itemPath = "/" + itemPath;
-                    }
-                }
-
-                // get the data package id
-                if(typeof dataPackage !== 'undefined') {
-                    var dataPackageId = dataPackage.id;
-                }
-                if (typeof dataPackageId === 'undefined')
-                    dataPackageId = this.dataPackage.id;
-
-                var insertInfoIcon = (this.edit) ? false : view.dataEntities.includes(item.id);
-
-                dataItemView = new DataItemView({
-                    model: item,
-                    metricsModel: this.metricsModel,
-                    itemPath: itemPath,
-                    insertInfoIcon: insertInfoIcon,
-                    currentlyViewing: this.currentlyViewing,
-                    mode: this.mode,
-                    parentEditorView: this.parentEditorView,
-                    dataPackageId: dataPackageId
-                });
-                this.subviews[item.id] = dataItemView; // keep track of all views
-
-                if (this.edit) {
-                    //Get the science metadata that documents this item
-                    scimetaParent = item.get("isDocumentedBy");
-
-                    //If this item is not documented by a science metadata object,
-                    // and there is only one science metadata doc in the package, then assume it is
-                    // documented by that science metadata doc
-                    if( typeof scimetaParent == "undefined" || !scimetaParent ){
-
-                        //Get the science metadata models
-                        var metadataIds = this.dataPackage.sciMetaPids;
-
-                        //If there is only one science metadata model in the package, then use it
-                        if( metadataIds.length == 1 )
-                            scimetaParent = metadataIds[0];
-                    }
-                    //Otherwise, get the first science metadata doc that documents this object
-                    else{
-                        scimetaParent = scimetaParent[0];
-                    }
-
-                    if((scimetaParent == item.get("id")) || (!scimetaParent && item.get("type") == "Metadata")) {
-                        // This is a metadata folder row, append it to the table
-                        this.$el.append(dataItemView.render().el);
-
-                        // Render any delayed models if this is the parent
-                        if ( _.contains(Object.keys(this.delayedModels), dataItemView.id) ) {
-
-                            delayed_models = this.delayedModels[dataItemView.id];
-                            _.each(delayed_models, this.addOne, this);
-
-                        }
-                    }
-                    else{
-                        // Find the parent row by it's id, stored in a custom attribute
-                        if(scimetaParent)
-                            parentRow = this.$("[data-id='" + scimetaParent + "']");
-
-                        if ( typeof parentRow !== "undefined" && parentRow.length ) {
-                            // This is a data row, insert below it's metadata parent folder
-                            parentRow.after(dataItemView.render().el);
-
-                            // Remove it from the delayedModels list if necessary
-                            if ( _.contains(Object.keys(this.delayedModels), scimetaParent) ) {
-                                delayed_models = this.delayedModels[scimetaParent];
-                                var index = _.indexOf(delayed_models, item);
-                                delayed_models = delayed_models.splice(index, 1);
-
-                                // Put the shortened array back if delayed models remains
-                                if ( delayed_models.length > 0 ) {
-                                    this.delayedModels[scimetaParent] = delayed_models;
-
-                                } else {
-                                    this.delayedModels[scimetaParent] = undefined;
-
-                                }
-                            }
-
-                            this.trigger("addOne");
-
-                        } else {
-                            console.warn("Couldn't render " + item.id + ". Delayed until parent is rendered.");
-                            // Postpone the data row until the parent is rendered
-                            delayed_models = this.delayedModels[scimetaParent];
-
-                            // Delay the model rendering if it isn't already delayed
-                            if ( typeof delayed_models !== "undefined" ) {
-
-                                if ( ! _.contains(delayed_models, item) ) {
-                                    delayed_models.push(item);
-                                    this.delayedModels[scimetaParent] = delayed_models;
-
-                                }
-
-                            } else {
-                                delayed_models = [];
-                                delayed_models.push(item);
-                                this.delayedModels[scimetaParent] = delayed_models;
-                            }
-                        }
-
-                    }
-                }
-                else {
-
-                    // This is a metadata folder row, append it to the table
-                    this.$el.append(dataItemView.render().el);
-
-                    this.trigger("addOne");
-                }
-
-            },
-
-            /**
-             * Render the Data Package View and insert it into this view
-             */
-            renderDataPackage: function () {
-
-                var view = this;
-
-                if(MetacatUI.rootDataPackage.packageModel.isNew()){
-                view.renderMember(this.model);
-                };
-
-                // As the root collection is updated with models, render the UI
-                this.listenTo(MetacatUI.rootDataPackage, "add", function (model) {
-
-                if (!model.get("synced") && model.get('id'))
-                    this.listenTo(model, "sync", view.renderMember);
-                else if (model.get("synced"))
-                    view.renderMember(model);
-
-                //Listen for changes on this member
-                model.on("change:fileName", model.addToUploadQueue);
-                });
-
-                //Render the Data Package view
-                this.dataPackageView = new DataPackageView({
-                edit: true,
-                dataPackage: MetacatUI.rootDataPackage,
-                parentEditorView: this
-                });
-
-                //Render the view
-                var $packageTableContainer = this.$("#data-package-container");
-                $packageTableContainer.html(this.dataPackageView.render().el);
-
-                //Make the view resizable on the bottom
-                var handle = $(document.createElement("div"))
-                .addClass("ui-resizable-handle ui-resizable-s")
-                .attr("title", "Drag to resize")
-                .append($(document.createElement("i")).addClass("icon icon-caret-down"));
-                $packageTableContainer.after(handle);
-                $packageTableContainer.resizable({
-                handles: { "s": handle },
-                minHeight: 100,
-                maxHeight: 900,
-                resize: function () {
-                    view.emlView.resizeTOC();
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "localforage",
+  "collections/DataPackage",
+  "models/DataONEObject",
+  "models/PackageModel",
+  "models/metadata/ScienceMetadata",
+  "models/metadata/eml211/EML211",
+  "models/PackageModel",
+  "views/DataItemView",
+  "views/DownloadButtonView",
+  "text!templates/dataPackage.html",
+  "text!templates/dataPackageStart.html",
+  "text!templates/dataPackageHeader.html",
+], function (
+  $,
+  _,
+  Backbone,
+  LocalForage,
+  DataPackage,
+  DataONEObject,
+  PackageModel,
+  ScienceMetadata,
+  EML211,
+  Package,
+  DataItemView,
+  DownloadButtonView,
+  DataPackageTemplate,
+  DataPackageStartTemplate,
+  DataPackageHeaderTemplate,
+) {
+  "use strict";
+
+  /**
+   * @class DataPackageView
+   * @classdesc The main view of a Data Package in MetacatUI.  The view is
+   *  a file/folder browser
+   * @classcategory Views
+   * @screenshot views/DataPackageView.png
+   * @extends Backbone.View
+   */
+  var DataPackageView = Backbone.View.extend(
+    /** @lends DataPackageView.prototype */ {
+      type: "DataPackage",
+
+      tagName: "table",
+
+      className: "table table-striped table-hover",
+
+      id: "data-package-table",
+
+      events: {
+        "click .toggle-rows": "toggleRows", // Show/hide rows associated with event's metadata row
+        "click .message-row .addFiles": "handleAddFiles",
+        "click .expand-control": "expand",
+        "click .collapse-control": "collapse",
+        "click .d1package-expand": "expandAll",
+        "click .d1package-collapse": "collapseAll",
+      },
+
+      subviews: {},
+
+      /**
+       * A reference to the parent EditorView that contains this view
+       * @type EditorView
+       * @since 2.15.0
+       */
+      parentEditorView: null,
+
+      template: _.template(DataPackageTemplate),
+      startMessageTemplate: _.template(DataPackageStartTemplate),
+      dataPackageHeaderTemplate: _.template(DataPackageHeaderTemplate),
+
+      // Models waiting for their parent folder to be rendered, hashed by parent id:
+      // {'parentid': [model1, model2, ...]}
+      delayedModels: {},
+
+      /* Flag indicating the open or closed state of the package rows */
+      isOpen: true,
+
+      initialize: function (options) {
+        if (options === undefined || !options) var options = {};
+
+        if (!options.edit) {
+          //The edit option will allow the user to edit the table
+          this.edit = options.edit || false;
+          this.mode = "view";
+          this.packageId = options.packageId || null;
+          this.memberId = options.memberId || null;
+          this.attributes = options.attributes || null;
+          this.dataPackage = options.dataPackage || new DataPackage();
+          this.dataEntities = options.dataEntities || new Array();
+          this.disablePackageDownloads =
+            options.disablePackageDownloads || false;
+          this.currentlyViewing = options.currentlyViewing || null;
+          this.parentEditorView = options.parentView || null;
+          this.title = options.title || "";
+          this.packageTitle = options.packageTitle || "";
+          this.nested =
+            typeof options.nested === "undefined" ? false : options.nested;
+          this.metricsModel = options.metricsModel;
+
+          // set the package model
+          this.packageModel = this.dataPackage.packageModel;
+
+          this.listenTo(this.packageModel, "changeAll", this.render);
+        } else {
+          //Get the options sent to this view
+          if (typeof options == "object") {
+            //The edit option will allow the user to edit the table
+            this.edit = options.edit || false;
+            this.mode = "edit";
+
+            //The data package to render
+            this.dataPackage = options.dataPackage || new DataPackage();
+
+            this.parentEditorView = options.parentEditorView || null;
+          }
+          //Create a new DataPackage collection if one wasn't sent
+          else if (!this.dataPackage) {
+            this.dataPackage = new DataPackage();
+          }
+
+          return this;
+        }
+      },
+
+      /**
+       *  Render the DataPackage HTML
+       */
+      render: function () {
+        this.$el.addClass("download-contents table-condensed");
+        this.$el.append(
+          this.template({
+            edit: this.edit,
+            dataPackageFiltering:
+              MetacatUI.appModel.get("dataPackageFiltering") || false,
+            dataPackageSorting:
+              MetacatUI.appModel.get("dataPackageSorting") || false,
+            loading: MetacatUI.appView.loadingTemplate({
+              msg: "Loading files table... ",
+            }),
+            id: this.dataPackage.get("id"),
+            title: this.title || "Files in this dataset",
+            classes: "download-contents table-striped table-condensed table",
+          }),
+        );
+
+        if (this.edit) {
+          // Listen for  add events because models are being merged
+          this.listenTo(this.dataPackage, "add", this.addOne);
+          this.listenTo(this.dataPackage, "fileAdded", this.addOne);
+        }
+
+        // Render the current set of models in the DataPackage
+        this.addAll();
+
+        if (this.edit) {
+          //If this is a new data package, then display a message and button
+          if (
+            (this.dataPackage.length == 1 &&
+              this.dataPackage.models[0].isNew()) ||
+            !this.dataPackage.length
+          ) {
+            var messageRow = this.startMessageTemplate();
+
+            this.$("tbody").append(messageRow);
+
+            this.listenTo(this.dataPackage, "add", function () {
+              this.$(".message-row").remove();
+            });
+          }
+
+          //Render the Share control(s)
+          this.renderShareControl();
+        } else {
+          // check for nessted datasets
+
+          if (this.nested) {
+            this.getNestedPackages();
+          }
+        }
+
+        return this;
+      },
+
+      /**
+       * Add a single DataItemView row to the DataPackageView
+       */
+      addOne: function (item, dataPackage) {
+        if (!item) return false;
+
+        //Don't add duplicate rows
+        if (this.$(".data-package-item[data-id='" + item.id + "']").length)
+          return;
+
+        // Don't add data package
+        if (
+          item.get("formatType") == "RESOURCE" ||
+          item.get("type") == "DataPackage"
+        ) {
+          return;
+        }
+
+        var dataItemView, scimetaParent, parentRow, delayed_models;
+
+        if (_.contains(Object.keys(this.subviews), item.id)) {
+          return false; // Don't double render
+        }
+
+        var itemPath = null,
+          view = this;
+        if (!_.isEmpty(this.atLocationObj)) {
+          itemPath = this.atLocationObj[item.get("id")];
+          if (itemPath[0] != "/") {
+            itemPath = "/" + itemPath;
+          }
+        }
+
+        // get the data package id
+        if (typeof dataPackage !== "undefined") {
+          var dataPackageId = dataPackage.id;
+        }
+        if (typeof dataPackageId === "undefined")
+          dataPackageId = this.dataPackage.id;
+
+        var insertInfoIcon = this.edit
+          ? false
+          : view.dataEntities.includes(item.id);
+
+        dataItemView = new DataItemView({
+          model: item,
+          metricsModel: this.metricsModel,
+          itemPath: itemPath,
+          insertInfoIcon: insertInfoIcon,
+          currentlyViewing: this.currentlyViewing,
+          mode: this.mode,
+          parentEditorView: this.parentEditorView,
+          dataPackageId: dataPackageId,
+        });
+        this.subviews[item.id] = dataItemView; // keep track of all views
+
+        if (this.edit) {
+          //Get the science metadata that documents this item
+          scimetaParent = item.get("isDocumentedBy");
+
+          //If this item is not documented by a science metadata object,
+          // and there is only one science metadata doc in the package, then assume it is
+          // documented by that science metadata doc
+          if (typeof scimetaParent == "undefined" || !scimetaParent) {
+            //Get the science metadata models
+            var metadataIds = this.dataPackage.sciMetaPids;
+
+            //If there is only one science metadata model in the package, then use it
+            if (metadataIds.length == 1) scimetaParent = metadataIds[0];
+          }
+          //Otherwise, get the first science metadata doc that documents this object
+          else {
+            scimetaParent = scimetaParent[0];
+          }
+
+          if (
+            scimetaParent == item.get("id") ||
+            (!scimetaParent && item.get("type") == "Metadata")
+          ) {
+            // This is a metadata folder row, append it to the table
+            this.$el.append(dataItemView.render().el);
+
+            // Render any delayed models if this is the parent
+            if (_.contains(Object.keys(this.delayedModels), dataItemView.id)) {
+              delayed_models = this.delayedModels[dataItemView.id];
+              _.each(delayed_models, this.addOne, this);
+            }
+          } else {
+            // Find the parent row by it's id, stored in a custom attribute
+            if (scimetaParent)
+              parentRow = this.$("[data-id='" + scimetaParent + "']");
+
+            if (typeof parentRow !== "undefined" && parentRow.length) {
+              // This is a data row, insert below it's metadata parent folder
+              parentRow.after(dataItemView.render().el);
+
+              // Remove it from the delayedModels list if necessary
+              if (_.contains(Object.keys(this.delayedModels), scimetaParent)) {
+                delayed_models = this.delayedModels[scimetaParent];
+                var index = _.indexOf(delayed_models, item);
+                delayed_models = delayed_models.splice(index, 1);
+
+                // Put the shortened array back if delayed models remains
+                if (delayed_models.length > 0) {
+                  this.delayedModels[scimetaParent] = delayed_models;
+                } else {
+                  this.delayedModels[scimetaParent] = undefined;
                 }
-                });
-
-                var tableHeight = ($(window).height() - $("#Navbar").height()) * .40;
-                $packageTableContainer.css("height", tableHeight + "px");
-
-                var table = this.dataPackageView.$el;
-                this.listenTo(this.dataPackageView, "addOne", function () {
-                if (table.outerHeight() > $packageTableContainer.outerHeight() && table.outerHeight() < 220) {
-                    $packageTableContainer.css("height", table.outerHeight() + handle.outerHeight());
-                    if (this.emlView)
-                    this.emlView.resizeTOC();
+              }
+
+              this.trigger("addOne");
+            } else {
+              console.warn(
+                "Couldn't render " +
+                  item.id +
+                  ". Delayed until parent is rendered.",
+              );
+              // Postpone the data row until the parent is rendered
+              delayed_models = this.delayedModels[scimetaParent];
+
+              // Delay the model rendering if it isn't already delayed
+              if (typeof delayed_models !== "undefined") {
+                if (!_.contains(delayed_models, item)) {
+                  delayed_models.push(item);
+                  this.delayedModels[scimetaParent] = delayed_models;
                 }
-                });
-
-                if (this.emlView)
-                this.emlView.resizeTOC();
-
-                //Save the view as a subview
-                this.subviews.push(this.dataPackageView);
+              } else {
+                delayed_models = [];
+                delayed_models.push(item);
+                this.delayedModels[scimetaParent] = delayed_models;
+              }
+            }
+          }
+        } else {
+          // This is a metadata folder row, append it to the table
+          this.$el.append(dataItemView.render().el);
+
+          this.trigger("addOne");
+        }
+      },
+
+      /**
+       * Render the Data Package View and insert it into this view
+       */
+      renderDataPackage: function () {
+        var view = this;
+
+        if (MetacatUI.rootDataPackage.packageModel.isNew()) {
+          view.renderMember(this.model);
+        }
+
+        // As the root collection is updated with models, render the UI
+        this.listenTo(MetacatUI.rootDataPackage, "add", function (model) {
+          if (!model.get("synced") && model.get("id"))
+            this.listenTo(model, "sync", view.renderMember);
+          else if (model.get("synced")) view.renderMember(model);
+
+          //Listen for changes on this member
+          model.on("change:fileName", model.addToUploadQueue);
+        });
 
-                this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:childPackages", this.renderChildren);
-            },
+        //Render the Data Package view
+        this.dataPackageView = new DataPackageView({
+          edit: true,
+          dataPackage: MetacatUI.rootDataPackage,
+          parentEditorView: this,
+        });
 
-            /**
-             * Add all rows to the DataPackageView
-             */
-            addAll: function() {
-
-                this.$el.find('#data-package-table-body').html(''); // clear the table first
-                this.dataPackage.sort();
-
-                if (!this.edit) {
-                    var atLocationObj = this.dataPackage.getAtLocation();
-                    this.atLocationObj = atLocationObj;
-
-                    // form path to D1 object dictionary
-                    if (this.atLocationObj !== undefined && !(_.isEmpty(this.atLocationObj))) {
-                        var filePathObj = new Object();
-
-                        this.dataPackage.each (function(item) {
-                            if (!(Object.keys(this.atLocationObj).includes(item.id))) {
-                                this.atLocationObj[item.id] = "/";
-                            }
-                        }, this);
-                        
-                        for (let key of Object.keys(this.atLocationObj)) {
-                            var path = this.atLocationObj[key];
-                            var pathArray = path.split('/');
-                            pathArray.pop();
-                            var parentPath = pathArray.join("/");
-                            if (filePathObj.hasOwnProperty(parentPath)) {
-                                filePathObj[parentPath].push(key);
-                            }
-                            else {
-                                filePathObj[parentPath] = new Array();
-                                filePathObj[parentPath].push(key);
-                            }
-                        }
-                    }
-
-                    // add top level data package row to the package table
-                    var tableRow = null, 
-                        view = this,
-                        title = this.packageTitle,
-                        packageUrl = null;
-
-                    if (title === ""){
-                        let metadataObj  = _.filter(this.dataPackage.models, function(m){ return(m.get("id") == view.currentlyViewing) });
-                        
-                        if (metadataObj.length > 0){
-                            title = metadataObj[0].get("title");
-                            let metaId = metadataObj[0].get("id");
-                            this.metaId = metaId;
-                        }
-                        else{
-                            title = this.dataPackage.get("id" );
-                        }
-                    }
-
-                    let titleTooltip = title;
-                    title = (title.length > 150) ? title.slice(0,75) + "..." + title.slice(title.length - 75, title.length) : title;
-                    
-                    // set the package URL
-                    if(MetacatUI.appModel.get("packageServiceUrl"))
-                        packageUrl = MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(view.dataPackage.id);
-
-                    var disablePackageDownloads = this.disablePackageDownloads;
-                    tableRow = this.dataPackageHeaderTemplate({id:view.dataPackage.id, title: title, titleTooltip: titleTooltip, downloadUrl: packageUrl, disablePackageDownloads: disablePackageDownloads});
-
-                    this.$el.append(tableRow);
-
-                    if (this.atLocationObj !== undefined && filePathObj !== undefined) {
-                        // sort the filePath by length
-                        var sortedFilePathObj = Object.keys(filePathObj).sort().reduce(
-                            (obj, key) => { 
-                            obj[key] = filePathObj[key]; 
-                            return obj;
-                            }, 
-                            {}
-                        );
-                        this.sortedFilePathObj = sortedFilePathObj;
-
-                        this.addFilesAndFolders(sortedFilePathObj);
-                    }
-                    else {
-                        this.dataPackage.each(this.addOne, this, this.dataPackage);
-                    }
-                }
-                else {
-                    this.dataPackage.each(this.addOne, this);
-                }
-            },
+        //Render the view
+        var $packageTableContainer = this.$("#data-package-container");
+        $packageTableContainer.html(this.dataPackageView.render().el);
+
+        //Make the view resizable on the bottom
+        var handle = $(document.createElement("div"))
+          .addClass("ui-resizable-handle ui-resizable-s")
+          .attr("title", "Drag to resize")
+          .append(
+            $(document.createElement("i")).addClass("icon icon-caret-down"),
+          );
+        $packageTableContainer.after(handle);
+        $packageTableContainer.resizable({
+          handles: { s: handle },
+          minHeight: 100,
+          maxHeight: 900,
+          resize: function () {
+            view.emlView.resizeTOC();
+          },
+        });
 
-            /**
-             * Add all the files and folders
-             */
-            addFilesAndFolders: function(sortedFilePathObj) {
-            	if(!sortedFilePathObj) return false;
-                var insertedPath = new Array();
-                let pathMap = new Object();
-                pathMap[""] = "";
-
-                for (let key of Object.keys(sortedFilePathObj)) {
-                    // add folder
-                    var pathArray = key.split("/");
-                    //skip the first empty value
-                    for (let i = 0; i < pathArray.length; i++) {
-
-                        if (pathArray[i].length < 1) 
-                            continue;
-
-                        if (!(pathArray[i] in pathMap)) {
-                            // insert path
-                            var dataItemView,
-                                itemPath;
-
-                            // root
-                            if (i == 0) {
-                                itemPath = "";
-                            }
-                            else {
-                                itemPath = pathMap[pathArray[i - 1]];
-                            }
-                            
-                            dataItemView = new DataItemView({
-                                mode: this.mode,
-                                itemName: pathArray[i],
-                                itemPath: itemPath,
-                                itemType: "folder",
-                                parentEditorView: this.parentEditorView,
-                                dataPackageId: this.dataPackage.id
-                            });
-
-                            this.subviews[pathArray[i]] = dataItemView; // keep track of all views
-
-                            this.$el.append(dataItemView.render().el);
-
-                            this.trigger("addOne");
-
-                            pathMap[pathArray[i]] = itemPath + "/" + pathArray[i];
-                        }
-                    }
-
-                    // add files in the folder
-                    var itemArray = sortedFilePathObj[key];
-
-                    // Add metadata object at the top of the file table
-                    if (key == "" && this.metaId !== "undefined" && itemArray.includes(this.metaId)) {
-                        let item = this.metaId;
-                        this.addOne(this.dataPackage.get(item));
-                    }
-
-                    for (let i = 0; i < itemArray.length; i++) {
-                        let item = itemArray[i];
-                        this.addOne(this.dataPackage.get(item));
-                    }
-                }
-            },
+        var tableHeight = ($(window).height() - $("#Navbar").height()) * 0.4;
+        $packageTableContainer.css("height", tableHeight + "px");
+
+        var table = this.dataPackageView.$el;
+        this.listenTo(this.dataPackageView, "addOne", function () {
+          if (
+            table.outerHeight() > $packageTableContainer.outerHeight() &&
+            table.outerHeight() < 220
+          ) {
+            $packageTableContainer.css(
+              "height",
+              table.outerHeight() + handle.outerHeight(),
+            );
+            if (this.emlView) this.emlView.resizeTOC();
+          }
+        });
 
-            /**
+        if (this.emlView) this.emlView.resizeTOC();
+
+        //Save the view as a subview
+        this.subviews.push(this.dataPackageView);
+
+        this.listenTo(
+          MetacatUI.rootDataPackage.packageModel,
+          "change:childPackages",
+          this.renderChildren,
+        );
+      },
+
+      /**
+       * Add all rows to the DataPackageView
+       */
+      addAll: function () {
+        this.$el.find("#data-package-table-body").html(""); // clear the table first
+        this.dataPackage.sort();
+
+        if (!this.edit) {
+          var atLocationObj = this.dataPackage.getAtLocation();
+          this.atLocationObj = atLocationObj;
+
+          // form path to D1 object dictionary
+          if (
+            this.atLocationObj !== undefined &&
+            !_.isEmpty(this.atLocationObj)
+          ) {
+            var filePathObj = new Object();
+
+            this.dataPackage.each(function (item) {
+              if (!Object.keys(this.atLocationObj).includes(item.id)) {
+                this.atLocationObj[item.id] = "/";
+              }
+            }, this);
+
+            for (let key of Object.keys(this.atLocationObj)) {
+              var path = this.atLocationObj[key];
+              var pathArray = path.split("/");
+              pathArray.pop();
+              var parentPath = pathArray.join("/");
+              if (filePathObj.hasOwnProperty(parentPath)) {
+                filePathObj[parentPath].push(key);
+              } else {
+                filePathObj[parentPath] = new Array();
+                filePathObj[parentPath].push(key);
+              }
+            }
+          }
+
+          // add top level data package row to the package table
+          var tableRow = null,
+            view = this,
+            title = this.packageTitle,
+            packageUrl = null;
+
+          if (title === "") {
+            let metadataObj = _.filter(this.dataPackage.models, function (m) {
+              return m.get("id") == view.currentlyViewing;
+            });
+
+            if (metadataObj.length > 0) {
+              title = metadataObj[0].get("title");
+              let metaId = metadataObj[0].get("id");
+              this.metaId = metaId;
+            } else {
+              title = this.dataPackage.get("id");
+            }
+          }
+
+          let titleTooltip = title;
+          title =
+            title.length > 150
+              ? title.slice(0, 75) +
+                "..." +
+                title.slice(title.length - 75, title.length)
+              : title;
+
+          // set the package URL
+          if (MetacatUI.appModel.get("packageServiceUrl"))
+            packageUrl =
+              MetacatUI.appModel.get("packageServiceUrl") +
+              encodeURIComponent(view.dataPackage.id);
+
+          var disablePackageDownloads = this.disablePackageDownloads;
+          tableRow = this.dataPackageHeaderTemplate({
+            id: view.dataPackage.id,
+            title: title,
+            titleTooltip: titleTooltip,
+            downloadUrl: packageUrl,
+            disablePackageDownloads: disablePackageDownloads,
+          });
+
+          this.$el.append(tableRow);
+
+          if (this.atLocationObj !== undefined && filePathObj !== undefined) {
+            // sort the filePath by length
+            var sortedFilePathObj = Object.keys(filePathObj)
+              .sort()
+              .reduce((obj, key) => {
+                obj[key] = filePathObj[key];
+                return obj;
+              }, {});
+            this.sortedFilePathObj = sortedFilePathObj;
+
+            this.addFilesAndFolders(sortedFilePathObj);
+          } else {
+            this.dataPackage.each(this.addOne, this, this.dataPackage);
+          }
+        } else {
+          this.dataPackage.each(this.addOne, this);
+        }
+      },
+
+      /**
+       * Add all the files and folders
+       */
+      addFilesAndFolders: function (sortedFilePathObj) {
+        if (!sortedFilePathObj) return false;
+        var insertedPath = new Array();
+        let pathMap = new Object();
+        pathMap[""] = "";
+
+        for (let key of Object.keys(sortedFilePathObj)) {
+          // add folder
+          var pathArray = key.split("/");
+          //skip the first empty value
+          for (let i = 0; i < pathArray.length; i++) {
+            if (pathArray[i].length < 1) continue;
+
+            if (!(pathArray[i] in pathMap)) {
+              // insert path
+              var dataItemView, itemPath;
+
+              // root
+              if (i == 0) {
+                itemPath = "";
+              } else {
+                itemPath = pathMap[pathArray[i - 1]];
+              }
+
+              dataItemView = new DataItemView({
+                mode: this.mode,
+                itemName: pathArray[i],
+                itemPath: itemPath,
+                itemType: "folder",
+                parentEditorView: this.parentEditorView,
+                dataPackageId: this.dataPackage.id,
+              });
+
+              this.subviews[pathArray[i]] = dataItemView; // keep track of all views
+
+              this.$el.append(dataItemView.render().el);
+
+              this.trigger("addOne");
+
+              pathMap[pathArray[i]] = itemPath + "/" + pathArray[i];
+            }
+          }
+
+          // add files in the folder
+          var itemArray = sortedFilePathObj[key];
+
+          // Add metadata object at the top of the file table
+          if (
+            key == "" &&
+            this.metaId !== "undefined" &&
+            itemArray.includes(this.metaId)
+          ) {
+            let item = this.metaId;
+            this.addOne(this.dataPackage.get(item));
+          }
+
+          for (let i = 0; i < itemArray.length; i++) {
+            let item = itemArray[i];
+            this.addOne(this.dataPackage.get(item));
+          }
+        }
+      },
+
+      /**
                 Remove the subview represented by the given model item.
 
                 @param item The model representing the sub view to be removed
             */
-            removeOne: function(item) {
-                if (_.contains(Object.keys(this.subviews), item.id)) {
-                    // Remove the view and the its reference in the subviews list
-                    this.subviews[item.id].remove();
-                    delete this.subviews[item.id];
-
-                }
-            },
-
-            handleAddFiles: function(e){
-            	//Pass this on to the DataItemView for the root data package
-            	this.$(".data-package-item.folder").first().data("view").handleAddFiles(e);
-            },
-
-            /**
-            * Renders a control that opens the AccessPolicyView for editing permissions on this package
-            * @since 2.15.0
-            */
-            renderShareControl: function(){
-
-                if( this.parentEditorView && !this.parentEditorView.isAccessPolicyEditEnabled() ){
-                    this.$("#data-package-table-share").remove();
-                }
-
-            },
-
-            /**
-             * Close subviews as needed
-             */
-            onClose: function() {
-                // Close each subview
-                _.each(Object.keys(this.subviews), function(id) {
-    				var subview = this.subviews[id];
-                    subview.onClose();
-
-                }, this);
-
-                //Reset the subviews from the view completely (by removing it from the prototype)
-                this.__proto__.subviews = {};
-            },
-
-            /**
+      removeOne: function (item) {
+        if (_.contains(Object.keys(this.subviews), item.id)) {
+          // Remove the view and the its reference in the subviews list
+          this.subviews[item.id].remove();
+          delete this.subviews[item.id];
+        }
+      },
+
+      handleAddFiles: function (e) {
+        //Pass this on to the DataItemView for the root data package
+        this.$(".data-package-item.folder")
+          .first()
+          .data("view")
+          .handleAddFiles(e);
+      },
+
+      /**
+       * Renders a control that opens the AccessPolicyView for editing permissions on this package
+       * @since 2.15.0
+       */
+      renderShareControl: function () {
+        if (
+          this.parentEditorView &&
+          !this.parentEditorView.isAccessPolicyEditEnabled()
+        ) {
+          this.$("#data-package-table-share").remove();
+        }
+      },
+
+      /**
+       * Close subviews as needed
+       */
+      onClose: function () {
+        // Close each subview
+        _.each(
+          Object.keys(this.subviews),
+          function (id) {
+            var subview = this.subviews[id];
+            subview.onClose();
+          },
+          this,
+        );
+
+        //Reset the subviews from the view completely (by removing it from the prototype)
+        this.__proto__.subviews = {};
+      },
+
+      /**
              Show or hide the data rows associated with the event row science metadata
              */
-            toggleRows: function(event) {
-
-                if ( this.isOpen ) {
-
-                    // Get the DataItemView associated with each id
-                    _.each(Object.keys(this.subviews), function(id) {
-
-                        var subview = this.subviews[id];
-
-                        if ( subview.model.get("type") === "Data" && subview.remove ) {
-                            // Remove the view from the DOM
-                            subview.remove();
-                            // And from the subviews list
-                            delete this.subviews[id];
-
-                        }
-
-                    }, this);
-
-                    // And then close the folder
-                    this.$el.find(".open")
-                        .removeClass("open")
-                        .addClass("closed")
-                        .removeClass("icon-chevron-down")
-                        .addClass("icon-chevron-right");
-
-                    this.$el.find(".icon-folder-open")
-                        .removeClass("icon-folder-open")
-                        .addClass("icon-folder-close");
-
-                    this.isOpen = false;
-
-                } else {
-
-                    // Add sub rows to the view
-                    var dataModels =  this.dataPackage.where({type: "Data"});
-                    _.each(dataModels, function(model) {
-                            this.addOne(model);
-                    }, this);
-
-                    // And then open the folder
-                    this.$el.find(".closed")
-                        .removeClass("closed")
-                        .addClass("open")
-                        .removeClass("icon-folder-close")
-                        .addClass("icon-chevron-down");
-
-                    this.$el.find(".icon-folder-close")
-                        .removeClass("icon-folder-close")
-                        .addClass("icon-folder-open");
-
-                    this.isOpen = true;
-
-                }
-
-                event.stopPropagation();
-                event.preventDefault();
-            },
-
-            /**
-             * Expand function to show hidden rows when a user clicks on an expand control.
-             * @param {Event} e - The event object.
-             * @since 2.28.0
-             */
-            expand: function(e) {
-                // Don't do anything...
-                e.preventDefault();
-
-                var view = this;
-                var eventEl = $(e.target).parents("td");
-                var rowEl = $(e.target).parents("tr");
-
-                var parentId = rowEl.data("id");
-                var children = "tr[data-parent='" + parentId + "']";
-
-                this.$(children).fadeIn();
-
-                this.$(eventEl).children().children(".expand-control").fadeOut(function() {
-                    view.$(eventEl).children().children(".collapse-control").fadeIn("fast");
-                    view.$(".tooltip-this").tooltip();
-                });
-
-                this.$(children).children().children().children(".collapse-control").fadeOut(function() {
-                    view.$(children).children().children().children(".expand-control").fadeIn("fast");
-                });
+      toggleRows: function (event) {
+        if (this.isOpen) {
+          // Get the DataItemView associated with each id
+          _.each(
+            Object.keys(this.subviews),
+            function (id) {
+              var subview = this.subviews[id];
+
+              if (subview.model.get("type") === "Data" && subview.remove) {
+                // Remove the view from the DOM
+                subview.remove();
+                // And from the subviews list
+                delete this.subviews[id];
+              }
             },
-
-            /**
-             * Collapse function to hide rows when a user clicks on a collapse control.
-             * @param {Event} e - The event object.
-             * 
-             * @since 2.28.0
-             */
-            collapse: function(e) {
-                // Don't do anything...
-                e.preventDefault();
-
-                var view = this;
-                var eventEl = $(e.target).parents("td");
-                var rowEl = $(e.target).parents("tr");
-
-                var parentId = rowEl.data("id");
-                var children = "tr[data-parent^='" + parentId + "']";
-                this.$(children).fadeOut();
-
-                this.$(eventEl).children().children(".collapse-control").fadeOut(function() {
-                    view.$(eventEl).children().children(".expand-control").fadeIn();
-                    view.$(".tooltip-this").tooltip();
-                });
+            this,
+          );
+
+          // And then close the folder
+          this.$el
+            .find(".open")
+            .removeClass("open")
+            .addClass("closed")
+            .removeClass("icon-chevron-down")
+            .addClass("icon-chevron-right");
+
+          this.$el
+            .find(".icon-folder-open")
+            .removeClass("icon-folder-open")
+            .addClass("icon-folder-close");
+
+          this.isOpen = false;
+        } else {
+          // Add sub rows to the view
+          var dataModels = this.dataPackage.where({ type: "Data" });
+          _.each(
+            dataModels,
+            function (model) {
+              this.addOne(model);
             },
+            this,
+          );
+
+          // And then open the folder
+          this.$el
+            .find(".closed")
+            .removeClass("closed")
+            .addClass("open")
+            .removeClass("icon-folder-close")
+            .addClass("icon-chevron-down");
+
+          this.$el
+            .find(".icon-folder-close")
+            .removeClass("icon-folder-close")
+            .addClass("icon-folder-open");
+
+          this.isOpen = true;
+        }
+
+        event.stopPropagation();
+        event.preventDefault();
+      },
+
+      /**
+       * Expand function to show hidden rows when a user clicks on an expand control.
+       * @param {Event} e - The event object.
+       * @since 2.28.0
+       */
+      expand: function (e) {
+        // Don't do anything...
+        e.preventDefault();
+
+        var view = this;
+        var eventEl = $(e.target).parents("td");
+        var rowEl = $(e.target).parents("tr");
+
+        var parentId = rowEl.data("id");
+        var children = "tr[data-parent='" + parentId + "']";
+
+        this.$(children).fadeIn();
+
+        this.$(eventEl)
+          .children()
+          .children(".expand-control")
+          .fadeOut(function () {
+            view
+              .$(eventEl)
+              .children()
+              .children(".collapse-control")
+              .fadeIn("fast");
+            view.$(".tooltip-this").tooltip();
+          });
+
+        this.$(children)
+          .children()
+          .children()
+          .children(".collapse-control")
+          .fadeOut(function () {
+            view
+              .$(children)
+              .children()
+              .children()
+              .children(".expand-control")
+              .fadeIn("fast");
+          });
+      },
+
+      /**
+       * Collapse function to hide rows when a user clicks on a collapse control.
+       * @param {Event} e - The event object.
+       *
+       * @since 2.28.0
+       */
+      collapse: function (e) {
+        // Don't do anything...
+        e.preventDefault();
+
+        var view = this;
+        var eventEl = $(e.target).parents("td");
+        var rowEl = $(e.target).parents("tr");
+
+        var parentId = rowEl.data("id");
+        var children = "tr[data-parent^='" + parentId + "']";
+        this.$(children).fadeOut();
+
+        this.$(eventEl)
+          .children()
+          .children(".collapse-control")
+          .fadeOut(function () {
+            view.$(eventEl).children().children(".expand-control").fadeIn();
+            view.$(".tooltip-this").tooltip();
+          });
+      },
+
+      /**
+       * Expand all function to show all child rows when a user clicks on an expand-all control.
+       * @param {Event} e - The event object.
+       *
+       * @since 2.28.0
+       */
+      expandAll: function (e) {
+        // Don't do anything...
+        e.preventDefault();
+
+        var view = this;
+        var eventEl = $(e.target).parents("td");
+        var rowEl = $(e.target).parents("tr");
+
+        var parentId = rowEl.data("id");
+        var children = "tr[data-packageid='" + parentId + "']";
+
+        this.$(children).fadeIn();
+
+        this.$(eventEl)
+          .children(".d1package-expand")
+          .fadeOut(function () {
+            view.$(eventEl).children(".d1package-collapse").fadeIn("fast");
+            view.$(".tooltip-this").tooltip();
+          });
+
+        this.$(children)
+          .children()
+          .children()
+          .children(".collapse-control")
+          .fadeOut(function () {
+            view
+              .$(children)
+              .children()
+              .children()
+              .children(".expand-control")
+              .fadeIn("fast");
+          });
+      },
+
+      /**
+       * Collapse all function to hide all child rows when a user clicks on a collapse-all control.
+       * @param {Event} e - The event object.
+       *
+       * @since 2.28.0
+       */
+      collapseAll: function (e) {
+        // Don't do anything...
+        e.preventDefault();
+
+        var view = this;
+        var eventEl = $(e.target).parents("td");
+        var rowEl = $(e.target).parents("tr");
+
+        var parentId = rowEl.data("id");
+        var children = "tr[data-packageid='" + parentId + "']";
+
+        this.$(children).each(function () {
+          $(this).fadeOut();
+          let childId = $(this).data("id");
+          let grandchildren = "tr[data-parent^='" + childId + "']";
+
+          $(grandchildren).fadeOut();
+        });
 
-            /**
-             * Expand all function to show all child rows when a user clicks on an expand-all control.
-             * @param {Event} e - The event object.
-             * 
-             * @since 2.28.0
-             */
-            expandAll: function(e) {
-                // Don't do anything...
-                e.preventDefault();
-
-                var view = this;
-                var eventEl = $(e.target).parents("td");
-                var rowEl = $(e.target).parents("tr");
-
-                var parentId = rowEl.data("id");
-                var children = "tr[data-packageid='" + parentId + "']";
-
-                this.$(children).fadeIn();
-
-                this.$(eventEl).children(".d1package-expand").fadeOut(function() {
-                    view.$(eventEl).children(".d1package-collapse").fadeIn("fast");
-                    view.$(".tooltip-this").tooltip();
-                });
-
-                this.$(children).children().children().children(".collapse-control").fadeOut(function() {
-                    view.$(children).children().children().children(".expand-control").fadeIn("fast");
-                });
-            },
+        this.$(eventEl)
+          .children(".d1package-collapse")
+          .fadeOut(function () {
+            view.$(eventEl).children(".d1package-expand").fadeIn();
+            view.$(".tooltip-this").tooltip();
+          });
+      },
+
+      /**
+       * Check for private members and disable download buttons if necessary.
+       *
+       * @since 2.28.0
+       */
+      checkForPrivateMembers: function () {
+        try {
+          var packageModel = this.model,
+            packageCollection = this.dataPackage;
+
+          if (!packageModel || !packageCollection) {
+            return;
+          }
+
+          var numMembersFromSolr = packageModel.get("members").length,
+            numMembersFromRDF = packageCollection.length;
+
+          if (numMembersFromRDF > numMembersFromSolr) {
+            var downloadButtons = this.$(".btn.download");
+
+            for (var i = 0; i < downloadButtons.length; i++) {
+              var btn = downloadButtons[i];
+              var downloadURL = $(btn).attr("href");
+
+              if (
+                downloadURL.indexOf(packageModel.get("id")) > -1 ||
+                downloadURL.indexOf(
+                  encodeURIComponent(packageModel.get("id")),
+                ) > -1
+              ) {
+                $(btn)
+                  .attr("disabled", "disabled")
+                  .addClass("disabled")
+                  .attr("href", "")
+                  .tooltip({
+                    trigger: "hover",
+                    placement: "top",
+                    delay: 500,
+                    title:
+                      "This dataset may contain private data, so each data file should be downloaded individually.",
+                  });
 
-            /**
-             * Collapse all function to hide all child rows when a user clicks on a collapse-all control.
-             * @param {Event} e - The event object.
-             * 
-             * @since 2.28.0
-             */
-            collapseAll: function(e) {
-                // Don't do anything...
-                e.preventDefault();
+                i = downloadButtons.length;
+              }
+            }
+          }
+        } catch (e) {
+          console.error(e);
+        }
+      },
+
+      /**
+       * Retrieves and processes nested packages for the current package.
+       *
+       * @since 2.28.0
+       */
+      getNestedPackages: function () {
+        var nestedPackages = new Array();
+        var nestedPackageIds = new Array();
+        this.nestedPackages = nestedPackages;
+
+        // get all the child packages for this resource map
+        var childPackages = this.dataPackage.filter(function (m) {
+          return m.get("formatType") === "RESOURCE";
+        });
 
-                var view = this;
-                var eventEl = $(e.target).parents("td");
-                var rowEl = $(e.target).parents("tr");
+        // iterate over the list of child packages and add their members
+        for (var ite in childPackages) {
+          var childPkg = childPackages[ite];
+          if (!nestedPackageIds.includes(childPkg.get("id"))) {
+            var nestedPackage = new PackageModel();
+            nestedPackage.set("id", childPkg.get("id"));
+            nestedPackage.setURL();
+            nestedPackage.getMembers();
+            nestedPackages.push(nestedPackage);
+            nestedPackageIds.push(childPkg.get("id"));
+
+            this.listenToOnce(
+              nestedPackage,
+              "change:members",
+              this.addNestedPackages,
+              nestedPackage,
+            );
+          }
+        }
+      },
+
+      /**
+       * Adds a nested data package to the package table.
+       *
+       * @param {Object} dataPackage - The data package to be added.
+       * @since 2.28.0
+       */
+      addNestedPackages: function (dataPackage) {
+        /**
+         * Generates the table row for the data package header.
+         *
+         * @type {null|Element}
+         */
+        var tableRow = null,
+          /**
+           * Reference to the current view.
+           *
+           * @type {Object}
+           */
+          view = this,
+          /**
+           * The title of the data package.
+           *
+           * @type {null|string}
+           */
+          title = null,
+          /**
+           * The URL of the data package.
+           *
+           * @type {null|string}
+           */
+          packageUrl = null;
 
-                var parentId = rowEl.data("id");
-                var children = "tr[data-packageid='" + parentId + "']";
+        /**
+         * The members of the data package.
+         *
+         * @type {Array}
+         */
+        var members = dataPackage.get("members");
+        /**
+         * Filters out metadata objects from the members.
+         *
+         * @type {Array}
+         */
+        let metadataObj = _.filter(members, function (m) {
+          return m.get("type") == "Metadata" || m.get("type") == "metadata";
+        });
 
-                this.$(children).each(function() {
-                    $(this).fadeOut();
-                    let childId = $(this).data("id");
-                    let grandchildren = "tr[data-parent^='" + childId + "']";
+        title = metadataObj[0].get("title");
 
-                    $(grandchildren).fadeOut();
-                });
+        /**
+         * The tooltip for the title (used for long titles).
+         *
+         * @type {string}
+         */
+        let titleTooltip = title;
+        title =
+          title.length > 150
+            ? title.slice(0, 75) +
+              "..." +
+              title.slice(title.length - 75, title.length)
+            : title;
+
+        // Set the package URL
+        if (MetacatUI.appModel.get("packageServiceUrl"))
+          packageUrl =
+            MetacatUI.appModel.get("packageServiceUrl") +
+            encodeURIComponent(dataPackage.id);
 
-                this.$(eventEl).children(".d1package-collapse").fadeOut(function() {
-                    view.$(eventEl).children(".d1package-expand").fadeIn();
-                    view.$(".tooltip-this").tooltip();
-                });
-            },
+        /**
+         * The HTML content for the data package header.
+         *
+         * @type {string}
+         */
+        tableRow = this.dataPackageHeaderTemplate({
+          id: dataPackage.id,
+          title: title,
+          titleTooltip: titleTooltip,
+          disablePackageDownloads: false,
+          downloadUrl: packageUrl,
+        });
+        this.$el.append(tableRow);
 
-            /**
-             * Check for private members and disable download buttons if necessary.
-             * 
-             * @since 2.28.0
-             */
-            checkForPrivateMembers: function() {
-                try {
-                    var packageModel = this.model,
-                        packageCollection = this.dataPackage;
-
-                    if (!packageModel || !packageCollection) {
-                        return;
-                    }
-
-                    var numMembersFromSolr = packageModel.get("members").length,
-                        numMembersFromRDF = packageCollection.length;
-
-                    if (numMembersFromRDF > numMembersFromSolr) {
-                        var downloadButtons = this.$(".btn.download");
-
-                        for (var i = 0; i < downloadButtons.length; i++) {
-                            var btn = downloadButtons[i];
-                            var downloadURL = $(btn).attr("href");
-
-                            if (
-                                downloadURL.indexOf(packageModel.get("id")) > -1 ||
-                                downloadURL.indexOf(encodeURIComponent(packageModel.get("id"))) > -1
-                            ) {
-                                $(btn)
-                                    .attr("disabled", "disabled")
-                                    .addClass("disabled")
-                                    .attr("href", "")
-                                    .tooltip({
-                                        trigger: "hover",
-                                        placement: "top",
-                                        delay: 500,
-                                        title: "This dataset may contain private data, so each data file should be downloaded individually."
-                                    });
-
-                                i = downloadButtons.length;
-                            }
-                        }
-                    }
-                } catch (e) {
-                    console.error(e);
-                }
-            },
+        // Create an instance of DownloadButtonView to handle package downloads
+        this.downloadButtonView = new DownloadButtonView({
+          model: dataPackage,
+          view: "actionsView",
+          nested: true,
+        });
 
+        // Render
+        this.downloadButtonView.render();
 
-            /**
-             * Retrieves and processes nested packages for the current package.
-             *
-             * @since 2.28.0
-             */
-            getNestedPackages: function() {
-                var nestedPackages = new Array();
-                var nestedPackageIds = new Array();
-                this.nestedPackages = nestedPackages;
-
-                // get all the child packages for this resource map
-                var childPackages = this.dataPackage.filter(function(m){
-                    return (m.get("formatType") === "RESOURCE");
-                  });
+        // Add the downloadButtonView el to the span
+        this.$el.find(".downloadAction").html(this.downloadButtonView.el);
 
-                // iterate over the list of child packages and add their members
-                for (var ite in childPackages) {
-                    var childPkg = childPackages[ite];
-                    if (!nestedPackageIds.includes(childPkg.get("id"))) {
-                        var nestedPackage = new PackageModel();
-                        nestedPackage.set("id", childPkg.get("id"));
-                        nestedPackage.setURL();
-                        nestedPackage.getMembers();
-                        nestedPackages.push(nestedPackage);
-                        nestedPackageIds.push(childPkg.get("id"));
-
-                        this.listenToOnce(nestedPackage, 'change:members', this.addNestedPackages, nestedPackage);
-                    }
-                }
-            },
+        // Filter out the packages from the member list
+        members = _.filter(members, function (m) {
+          return m.type != "Package";
+        });
 
-            /**
-             * Adds a nested data package to the package table.
-             *
-             * @param {Object} dataPackage - The data package to be added.
-             * @since 2.28.0
-             */
-            addNestedPackages: function(dataPackage) {
-                /**
-                * Generates the table row for the data package header.
-                *
-                * @type {null|Element}
-                */
-                var tableRow = null,
-                    /**
-                    * Reference to the current view.
-                    *
-                    * @type {Object}
-                    */
-                    view = this,
-                    /**
-                    * The title of the data package.
-                    *
-                    * @type {null|string}
-                    */
-                    title = null,
-                    /**
-                    * The URL of the data package.
-                    *
-                    * @type {null|string}
-                    */
-                    packageUrl = null;
-
-                /**
-                * The members of the data package.
-                *
-                * @type {Array}
-                */
-                var members = dataPackage.get("members");
-                /**
-                * Filters out metadata objects from the members.
-                *
-                * @type {Array}
-                */
-                let metadataObj = _.filter(members, function(m) { return (m.get("type") == "Metadata" || m.get("type") == "metadata") });
-
-                title = metadataObj[0].get("title");
-
-                /**
-                * The tooltip for the title (used for long titles).
-                *
-                * @type {string}
-                */
-                let titleTooltip = title;
-                title = (title.length > 150) ? title.slice(0, 75) + "..." + title.slice(title.length - 75, title.length) : title;
-
-                // Set the package URL
-                if (MetacatUI.appModel.get("packageServiceUrl"))
-                    packageUrl = MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(dataPackage.id);
-
-                /**
-                * The HTML content for the data package header.
-                *
-                * @type {string}
-                */
-                tableRow = this.dataPackageHeaderTemplate({ id: dataPackage.id, title: title, titleTooltip: titleTooltip, disablePackageDownloads: false, downloadUrl: packageUrl });
-                this.$el.append(tableRow);
-
-                // Create an instance of DownloadButtonView to handle package downloads
-                this.downloadButtonView = new DownloadButtonView({ model: dataPackage, view: "actionsView", nested: true });
-
-                // Render
-                this.downloadButtonView.render();
-
-                // Add the downloadButtonView el to the span
-                this.$el.find('.downloadAction').html(this.downloadButtonView.el);
-
-                // Filter out the packages from the member list
-                members = _.filter(members, function(m) { return (m.type != "Package") });
-
-                // Add each member to the package table view
-                var view = this;
-                _.each(members, function(m) {
-                    // Update the size to bytes format
-                    m.set({ size: m.bytesToSize(m.get("size")) });
-
-                    // Add each item of this nested package to the package table view
-                    view.addOne(m, dataPackage);
-                });
-            },
+        // Add each member to the package table view
+        var view = this;
+        _.each(members, function (m) {
+          // Update the size to bytes format
+          m.set({ size: m.bytesToSize(m.get("size")) });
 
+          // Add each item of this nested package to the package table view
+          view.addOne(m, dataPackage);
+        });
+      },
 
-            /*showDownloadProgress: function(e){
+      /*showDownloadProgress: function(e){
                 e.preventDefault();
 
                 var button = $(e.target);
@@ -982,9 +1088,9 @@ 

Source: src/js/views/DataPackageView.js

return true; }*/ - - }); - return DataPackageView; + }, + ); + return DataPackageView; });
diff --git a/docs/docs/src_js_views_DraftsView.js.html b/docs/docs/src_js_views_DraftsView.js.html index 724be7961..a68a2f571 100644 --- a/docs/docs/src_js_views_DraftsView.js.html +++ b/docs/docs/src_js_views_DraftsView.js.html @@ -44,71 +44,82 @@

Source: src/js/views/DraftsView.js

-
define(["jquery", "underscore", "backbone", "localforage", "clipboard", "text!templates/draftsTemplate.html"],
-  function($, _, Backbone, LocalForage, Clipboard, draftsTemplate){
-    /**
-    * @class DraftsView
-    * @classdesc A view that lists the local submission drafts for this user
-    * @classcategory Views
-    * @extends Backbone.View
-    */
-    var view = Backbone.View.extend(
-      /** @lends DraftsView.prototype */{
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "localforage",
+  "clipboard",
+  "text!templates/draftsTemplate.html",
+], function ($, _, Backbone, LocalForage, Clipboard, draftsTemplate) {
+  /**
+   * @class DraftsView
+   * @classdesc A view that lists the local submission drafts for this user
+   * @classcategory Views
+   * @extends Backbone.View
+   */
+  var view = Backbone.View.extend(
+    /** @lends DraftsView.prototype */ {
       type: "DraftsView",
       el: "#Content",
       className: "div",
       template: _.template(draftsTemplate),
 
-      initialize: function() {
+      initialize: function () {
         return this;
       },
 
-      render: function() {
+      render: function () {
         var view = this;
         var drafts = [];
 
-        LocalForage.iterate(function(value, key, iterationNumber) {
+        LocalForage.iterate(function (value, key, iterationNumber) {
           // Extract each draft
           drafts.push({
             key: key,
             value: value,
-            fileName: (typeof value.title === "string") ?
-              value.title.substr(0, 50).replace(/[^a-zA-Z0-9_]/, "_") : "draft",
-            friendlyTimeDiff: view.friendlyTimeDiff(value.datetime)
+            fileName:
+              typeof value.title === "string"
+                ? value.title.substr(0, 50).replace(/[^a-zA-Z0-9_]/, "_")
+                : "draft",
+            friendlyTimeDiff: view.friendlyTimeDiff(value.datetime),
+          });
+        })
+          .then(function () {
+            // Sort by datetime
+            drafts = _.sortBy(drafts, function (draft) {
+              return draft.value.datetime.toString();
+            }).reverse();
+          })
+          .then(function () {
+            // Render
+            view.$el.html(
+              view.template({
+                drafts: drafts,
+              }),
+            );
+
+            // Insert downloadables
+            view.insertDownloadables();
+            // Insert copiables
+            view.insertCopiables();
+          })
+          .catch(function (err) {
+            console.log(err);
+            view.$el.html("<div>There was an error listing drafts.</div>");
           });
-        }).then(function(){
-          // Sort by datetime
-          drafts = _.sortBy(drafts, function(draft) {
-            return draft.value.datetime.toString();
-          }).reverse();
-        }).then(function() {
-          // Render
-          view.$el.html(
-            view.template({
-              drafts: drafts
-            })
-          );
-
-          // Insert downloadables
-          view.insertDownloadables();
-          // Insert copiables
-          view.insertCopiables();
-        }).catch(function(err) {
-          console.log(err);
-          view.$el.html("<div>There was an error listing drafts.</div>");
-        });
 
         return this;
       },
 
       /** Attach a click handler for download buttons that triggers a draft
-      * or all drafts to be downloaded
-      */
-      insertDownloadables: function() {
+       * or all drafts to be downloaded
+       */
+      insertDownloadables: function () {
         var view = this;
 
         // Build handlers for single downloaders
-        _.each(this.$el.find(".draft-download"), function(el) {
+        _.each(this.$el.find(".draft-download"), function (el) {
           var a = $(el).find("a.download");
 
           var text = $(el).find("textarea")[0].value;
@@ -122,12 +133,12 @@ 

Source: src/js/views/DraftsView.js

}, /** Creates a function for use as an event handler in insertDownloadables - * that creates a closure around the content (text) and filename and - * causes the browser to download the draft when clicked - */ - createDownloader: function(text, fileName) { - return function() { - var blob = new Blob([text], { type: "application/xml" }) + * that creates a closure around the content (text) and filename and + * causes the browser to download the draft when clicked + */ + createDownloader: function (text, fileName) { + return function () { + var blob = new Blob([text], { type: "application/xml" }); var url = window.URL.createObjectURL(blob); var a = document.createElement("a"); @@ -136,26 +147,25 @@

Source: src/js/views/DraftsView.js

a.download = fileName; a.click(); a.remove(); - } + }; }, - createDownloadAll: function() { + createDownloadAll: function () { var drafts = []; - _.each(this.$el.find("textarea"), function(textarea) { + _.each(this.$el.find("textarea"), function (textarea) { drafts.push(textarea.value); }); - var doc = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<drafts>\n" + - _.map(drafts, function(draft) { - return "\t<draft>\n\t\t" + - draft + - "\n\t</draft>\n" + var doc = + '<?xml version="1.0" encoding="utf-8"?>\n<drafts>\n' + + _.map(drafts, function (draft) { + return "\t<draft>\n\t\t" + draft + "\n\t</draft>\n"; }).join("") + - "</drafts>"; + "</drafts>"; - return function() { - var blob = new Blob([doc], { type: "application/xml" }) + return function () { + var blob = new Blob([doc], { type: "application/xml" }); var url = window.URL.createObjectURL(blob); var a = document.createElement("a"); @@ -164,31 +174,34 @@

Source: src/js/views/DraftsView.js

a.download = "drafts.xml"; a.click(); a.remove(); - } + }; }, - insertCopiables: function() { + insertCopiables: function () { var copiables = $(".copy-to-clipboard"); - _.each(copiables, function(copiable, i) { - var clipboard = new Clipboard(copiable, - { - text: function(trigger) { - return $("#draft-" + i).text() - } - }); + _.each(copiables, function (copiable, i) { + var clipboard = new Clipboard(copiable, { + text: function (trigger) { + return $("#draft-" + i).text(); + }, + }); - clipboard.on("success", function(e) { + clipboard.on("success", function (e) { var el = $(e.trigger); - $(el).html( $(document.createElement("span")).addClass("icon icon-ok success") ); + $(el).html( + $(document.createElement("span")).addClass( + "icon icon-ok success", + ), + ); // Use setTimeout instead of jQuery's built-in Events system because // it didn't look flexible enough to allow me update innerHTML in // a chain - setTimeout(function() { + setTimeout(function () { $(el).html('<i class="icon icon-copy"></i> Copy to Clipboard'); - }, 500) + }, 500); }); }); }, @@ -198,44 +211,44 @@

Source: src/js/views/DraftsView.js

* @param {string} datetime: A datetime as a string which needs to be * parsed before working with */ - friendlyTimeDiff: function(datetime) { + friendlyTimeDiff: function (datetime) { var friendly, - now = new Date(), - then = new Date(datetime), - diff = now - then; + now = new Date(), + then = new Date(datetime), + diff = now - then; // Fall through from largest to smallest, finding the largest unit // that describes the difference with a unit value of one or greater if (diff > 2678400000) { friendly = { - value: Math.round(diff / 2678400000) , - unit: "month" - } + value: Math.round(diff / 2678400000), + unit: "month", + }; } else if (diff > 604800000) { friendly = { value: Math.round(diff / 604800000), - unit: "week" - } + unit: "week", + }; } else if (diff > 86400000) { friendly = { value: Math.round(diff / 86400000), - unit: "day" - } + unit: "day", + }; } else if (diff > 3600000) { friendly = { value: Math.round(diff / 3600000), - unit: "hour" - } + unit: "hour", + }; } else if (diff > 60000) { friendly = { value: Math.round(diff / 60000), - unit: "minute" - } + unit: "minute", + }; } else if (diff > 1000) { friendly = { value: Math.round(diff / 1000), - unit: "second" - } + unit: "second", + }; } else { // Shortcircuit if really small and return... return "just now"; @@ -243,16 +256,16 @@

Source: src/js/views/DraftsView.js

// Pluralize if (friendly.value !== 1) { - friendly.unit = friendly.unit + "s" + friendly.unit = friendly.unit + "s"; } return friendly.value + " " + friendly.unit + " ago"; - } - }) - - return view; + }, + }, + ); - }); + return view; +});
diff --git a/docs/docs/src_js_views_EditCollectionView.js.html b/docs/docs/src_js_views_EditCollectionView.js.html index 59d27b532..e09dea34e 100644 --- a/docs/docs/src_js_views_EditCollectionView.js.html +++ b/docs/docs/src_js_views_EditCollectionView.js.html @@ -44,323 +44,371 @@

Source: src/js/views/EditCollectionView.js

-
define(['underscore',
-        'jquery',
-        'backbone',
-        "models/Map",
-        "models/CollectionModel",
-        "models/Search",
-        "views/DataCatalogViewWithFilters",
-        "views/queryBuilder/QueryBuilderView",
-        "text!templates/editCollection.html"],
-function(_, $, Backbone, Map, CollectionModel, Search, DataCatalogViewWithFilters,
-          QueryBuilder, Template){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/Map",
+  "models/CollectionModel",
+  "models/Search",
+  "views/DataCatalogViewWithFilters",
+  "views/queryBuilder/QueryBuilderView",
+  "text!templates/editCollection.html",
+], function (
+  _,
+  $,
+  Backbone,
+  Map,
+  CollectionModel,
+  Search,
+  DataCatalogViewWithFilters,
+  QueryBuilder,
+  Template,
+) {
   /**
-  * @class EditCollectionView
-  * @classdesc A view that allows the user to edit the search filters that define their dataset collection
-  * @classcategory Views
-  * @extends Backbone.View
-  * @constructor
-  */
+   * @class EditCollectionView
+   * @classdesc A view that allows the user to edit the search filters that define their dataset collection
+   * @classcategory Views
+   * @extends Backbone.View
+   * @constructor
+   */
   var EditCollectionView = Backbone.View.extend(
-    /** @lends EditCollectionView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "EditCollection",
-
-    /**
-     * A reference to the parent editor view, if there is one
-     * @type {PortalEditorView}
-     */
-    editorView: undefined,
-
-    /**
-    * The HTML tag name to use for this view's element
-    * @type {string}
-    */
-    tagName: "div",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "edit-collection",
-
-    /**
-    * The Collection model that is being edited
-    * @type {CollectionModel}
-    */
-    model: undefined,
-
-    /**
-    * The template for this view. An HTML file is converted to an Underscore.js template
-    */
-    template: _.template(Template),
-
-    /**
-    * A jQuery selector for the element that the DataCatalogViewWithFilters should be inserted into
-    * @type {string}
-    */
-    dataCatalogViewContainer: ".data-catalog-view-container",
-    /**
-    * A jQuery selector for the element that the Save and Cancel buttons should be inserted into
-    * @type {string}
-    */
-    collectionControlsContainer: ".applied-filters-container",
-    /**
-    * A jQuery selector for the element that the QueryBuilder should be inserted into
-    * @type {string}
-    */
-    queryBuilderViewContainer: ".query-builder-view-container",
-    /**
-    * A jQuery selector for the element that contains the filter help text
-    * @type {string}
-    */
-    helpTextContainer: "#filter-help-text",
-
-    /**
-     * An array of hex color codes used to help distinguish between different rules
-     * @type {string[]}
-     */
-     ruleColorPalette: ["#44AA99", "#137733", "#c9a538", "#CC6677", "#882355", "#AA4499","#332288"],
-
-    /**
-     * Query fields to exclude in the metadata field selector of each Query Rule. This
-     * is a list of field names that exist in the query service index (i.e. Solr), but
-     * which should be hidden in the Query Builder
-     * @type {string[]}
-     */
-    queryBuilderExcludeFields: MetacatUI.appModel.get("collectionQueryExcludeFields"),
-
-    /**
-     * Query fields to exclude in the metadata field selector for any Query Rules that are
-     * in nested Query Builders (i.e. in nested Filter Groups). This is a list of field
-     * names that exist in the query service index (i.e. Solr), but which should be hidden
-     * in nested Query Builders
-     * @type {string[]}
-     */
-    queryBuilderNestedExcludeFields: _.union(
-      MetacatUI.appModel.get("collectionQueryExcludeFields"),
-      MetacatUI.appModel.get("queryIdentifierFields")
-    ),
-
-    /**
-     * Query fields that do not exist in the query service index, but which we would
-     * like to show as options in the Query Builder field input.
-     * @type {SpecialField[]}
-     * @since 2.15.0
-     */
-    queryBuilderSpecialFields: MetacatUI.appModel.get("collectionQuerySpecialFields"),
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-    },
-
-    /**
-    * Is exexcuted when a new EditCollectionView is created
-    * @param {Object} options - A literal object with options to pass to the view
-    * @property {CollectionModel} options.model - The collection whose search results will be displayed and edited in this view
-    */
-    initialize: function(options){
-
-      if( typeof options == "object" ){
-        this.model = options.model || undefined;
-      }
-
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-      var title = "Change the data in your collection"
-      if(this.model.isNew()){
-        title = "Add data to your collection"
-      }
-
-      var helpText = "",
+    /** @lends EditCollectionView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "EditCollection",
+
+      /**
+       * A reference to the parent editor view, if there is one
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * The HTML tag name to use for this view's element
+       * @type {string}
+       */
+      tagName: "div",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "edit-collection",
+
+      /**
+       * The Collection model that is being edited
+       * @type {CollectionModel}
+       */
+      model: undefined,
+
+      /**
+       * The template for this view. An HTML file is converted to an Underscore.js template
+       */
+      template: _.template(Template),
+
+      /**
+       * A jQuery selector for the element that the DataCatalogViewWithFilters should be inserted into
+       * @type {string}
+       */
+      dataCatalogViewContainer: ".data-catalog-view-container",
+      /**
+       * A jQuery selector for the element that the Save and Cancel buttons should be inserted into
+       * @type {string}
+       */
+      collectionControlsContainer: ".applied-filters-container",
+      /**
+       * A jQuery selector for the element that the QueryBuilder should be inserted into
+       * @type {string}
+       */
+      queryBuilderViewContainer: ".query-builder-view-container",
+      /**
+       * A jQuery selector for the element that contains the filter help text
+       * @type {string}
+       */
+      helpTextContainer: "#filter-help-text",
+
+      /**
+       * An array of hex color codes used to help distinguish between different rules
+       * @type {string[]}
+       */
+      ruleColorPalette: [
+        "#44AA99",
+        "#137733",
+        "#c9a538",
+        "#CC6677",
+        "#882355",
+        "#AA4499",
+        "#332288",
+      ],
+
+      /**
+       * Query fields to exclude in the metadata field selector of each Query Rule. This
+       * is a list of field names that exist in the query service index (i.e. Solr), but
+       * which should be hidden in the Query Builder
+       * @type {string[]}
+       */
+      queryBuilderExcludeFields: MetacatUI.appModel.get(
+        "collectionQueryExcludeFields",
+      ),
+
+      /**
+       * Query fields to exclude in the metadata field selector for any Query Rules that are
+       * in nested Query Builders (i.e. in nested Filter Groups). This is a list of field
+       * names that exist in the query service index (i.e. Solr), but which should be hidden
+       * in nested Query Builders
+       * @type {string[]}
+       */
+      queryBuilderNestedExcludeFields: _.union(
+        MetacatUI.appModel.get("collectionQueryExcludeFields"),
+        MetacatUI.appModel.get("queryIdentifierFields"),
+      ),
+
+      /**
+       * Query fields that do not exist in the query service index, but which we would
+       * like to show as options in the Query Builder field input.
+       * @type {SpecialField[]}
+       * @since 2.15.0
+       */
+      queryBuilderSpecialFields: MetacatUI.appModel.get(
+        "collectionQuerySpecialFields",
+      ),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {},
+
+      /**
+       * Is exexcuted when a new EditCollectionView is created
+       * @param {Object} options - A literal object with options to pass to the view
+       * @property {CollectionModel} options.model - The collection whose search results will be displayed and edited in this view
+       */
+      initialize: function (options) {
+        if (typeof options == "object") {
+          this.model = options.model || undefined;
+        }
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        var title = "Change the data in your collection";
+        if (this.model.isNew()) {
+          title = "Add data to your collection";
+        }
+
+        var helpText = "",
           email = MetacatUI.appModel.get("emailContact");
 
-      if (email) {
-        helpText = 'Need help building your data collection? <a href="maito:' + email + '">Get in touch.</a>'
-      }
-
-      this.$el.html(this.template({
-        title: title,
-        description: "Your collection can include any of the datasets that are available on the network. " +
-          "Build rules based on metadata to define which datasets should be included in your collection. " +
-          "Data added to the network in the future that match these rules will also be added to your collection. " +
-          "Click the save button when you're happy with the results.",
-        helpText: helpText
-      }));
-
-      // Remove this when the Query Builder is no longer new:
-      this.$el
-        .find(".edit-collection-title")
-        .append($('<span class="new-icon" style="margin-left:10px; font-size:1rem; line-height: 25px;"><i class="icon icon-star icon-on-right"></i> NEW </span>'));
+        if (email) {
+          helpText =
+            'Need help building your data collection? <a href="maito:' +
+            email +
+            '">Get in touch.</a>';
+        }
+
+        this.$el.html(
+          this.template({
+            title: title,
+            description:
+              "Your collection can include any of the datasets that are available on the network. " +
+              "Build rules based on metadata to define which datasets should be included in your collection. " +
+              "Data added to the network in the future that match these rules will also be added to your collection. " +
+              "Click the save button when you're happy with the results.",
+            helpText: helpText,
+          }),
+        );
+
+        // Remove this when the Query Builder is no longer new:
+        this.$el
+          .find(".edit-collection-title")
+          .append(
+            $(
+              '<span class="new-icon" style="margin-left:10px; font-size:1rem; line-height: 25px;"><i class="icon icon-star icon-on-right"></i> NEW </span>',
+            ),
+          );
         // .append($('<span class="badge badge-info d1_pill d1_pill--primary" style="margin-left:10px">NEW!</span>'));
 
-      // Make sure that we have a series ID before we render the Data Catalog
-      // View With Filters. For new portals, we generate and reserve a series ID
-      // and use it to add an isPartOf filter to the portal model. This takes time,
-      // and influences the search results shown in the data catalog.
-      if( this.model.get("seriesId") || this.model.get("latestVersion") ){
-        //Render the DataCatalog
-        this.renderDataCatalog();
-      } else {
-        //When the seriesId or latest pid version is found, render the DataCatalog
-        this.listenToOnce(this.model, "change:seriesId",    this.renderDataCatalog);
-        this.listenToOnce(this.model, "latestVersionFound", this.renderDataCatalog);
-      }
-
-      //this.renderCollectionControls();
-
-    },
-
-    /**
-     * renderQueryBuilder - Render the QueryBuilder and insert it into this view
-     */
-    renderQueryBuilder: function(){
-
-      // If the isPartOf filter is hidden, then don't allow users to build
-      // a Query Rule using the isPartOf field. If they do, that rule will
-      // be hidden the next time they open the portal in the editor. Also,
-      // the filter they create will overwrite the isPartOf filter created by
-      // default.
-      if(MetacatUI.appModel.get("hideIsPartOfFilter") === true ? true : false){
-        this.queryBuilderExcludeFields.push("isPartOf")
-      }
-
-      var queryBuilder = new QueryBuilder({
-        filterGroup: this.model.get("definition"),
-        ruleColorPalette: this.ruleColorPalette,
-        excludeFields: this.queryBuilderExcludeFields,
-        nestedExcludeFields: this.queryBuilderNestedExcludeFields,
-        specialFields: this.queryBuilderSpecialFields,
-      });
-
-      // Render the Query Builder and insert it into this view
-      this.$(this.queryBuilderViewContainer).html(queryBuilder.el);
-      queryBuilder.render();
-    },
-
-    /**
-     * Render the DataCatalogViewWithFilters
-     */
-    renderDataCatalog: function(){
-
-      this.renderQueryBuilder();
-
-      var searchModel = this.model.get("searchModel");
-
-      searchModel.set("useGeohash", false);
-
-      // Create a DataCatalog view
-      var dataCatalogView = new DataCatalogViewWithFilters({
-        searchModel: searchModel,
-        searchResults: this.model.get("searchResults"),
-        mapModel: this.model.get("mapModel") || new Map(),
-        isSubView: true,
-        mode: "map",
-        filters: false,
-        solrError500Message: "There may be a problem with one of the rules you created." +
-          " Try undoing the last change you made.",
-        solrErrorTitle: "Something went wrong searching for datasets that match your query",
-        // Override the function that creates filter groups on the left of the
-        // data catalog view. With the Query Builder view, they are not needed.
-        // Otherwise, the defaultFilterGroups will be added to the Query Builder
-        createFilterGroups: function(){ return },
-        addAnnotationFilter: function(){ return },
-        editorView: this.editorView
-      });
-
-      //Render the view and insert it into the page
-      this.$(this.dataCatalogViewContainer).html(dataCatalogView.el);
-      dataCatalogView.render();
-
-      this.listenTo(this.model.get("searchResults"), "reset", this.toggleHelpText);
-
-    },
-
-    /**
-    * Renders the edit collection controls - e.g. a Save and Cancel buttton
-    */
-    renderCollectionControls: function(){
-
-      //Create a Save button
-      var saveButton   = $(document.createElement("a"))
-                        .addClass("save btn btn-primary")
-                        .text("Save"),
-      //Create a Cancel button
+        // Make sure that we have a series ID before we render the Data Catalog
+        // View With Filters. For new portals, we generate and reserve a series ID
+        // and use it to add an isPartOf filter to the portal model. This takes time,
+        // and influences the search results shown in the data catalog.
+        if (this.model.get("seriesId") || this.model.get("latestVersion")) {
+          //Render the DataCatalog
+          this.renderDataCatalog();
+        } else {
+          //When the seriesId or latest pid version is found, render the DataCatalog
+          this.listenToOnce(
+            this.model,
+            "change:seriesId",
+            this.renderDataCatalog,
+          );
+          this.listenToOnce(
+            this.model,
+            "latestVersionFound",
+            this.renderDataCatalog,
+          );
+        }
+
+        //this.renderCollectionControls();
+      },
+
+      /**
+       * renderQueryBuilder - Render the QueryBuilder and insert it into this view
+       */
+      renderQueryBuilder: function () {
+        // If the isPartOf filter is hidden, then don't allow users to build
+        // a Query Rule using the isPartOf field. If they do, that rule will
+        // be hidden the next time they open the portal in the editor. Also,
+        // the filter they create will overwrite the isPartOf filter created by
+        // default.
+        if (
+          MetacatUI.appModel.get("hideIsPartOfFilter") === true ? true : false
+        ) {
+          this.queryBuilderExcludeFields.push("isPartOf");
+        }
+
+        var queryBuilder = new QueryBuilder({
+          filterGroup: this.model.get("definition"),
+          ruleColorPalette: this.ruleColorPalette,
+          excludeFields: this.queryBuilderExcludeFields,
+          nestedExcludeFields: this.queryBuilderNestedExcludeFields,
+          specialFields: this.queryBuilderSpecialFields,
+        });
+
+        // Render the Query Builder and insert it into this view
+        this.$(this.queryBuilderViewContainer).html(queryBuilder.el);
+        queryBuilder.render();
+      },
+
+      /**
+       * Render the DataCatalogViewWithFilters
+       */
+      renderDataCatalog: function () {
+        this.renderQueryBuilder();
+
+        var searchModel = this.model.get("searchModel");
+
+        searchModel.set("useGeohash", false);
+
+        // Create a DataCatalog view
+        var dataCatalogView = new DataCatalogViewWithFilters({
+          searchModel: searchModel,
+          searchResults: this.model.get("searchResults"),
+          mapModel: this.model.get("mapModel") || new Map(),
+          isSubView: true,
+          mode: "map",
+          filters: false,
+          solrError500Message:
+            "There may be a problem with one of the rules you created." +
+            " Try undoing the last change you made.",
+          solrErrorTitle:
+            "Something went wrong searching for datasets that match your query",
+          // Override the function that creates filter groups on the left of the
+          // data catalog view. With the Query Builder view, they are not needed.
+          // Otherwise, the defaultFilterGroups will be added to the Query Builder
+          createFilterGroups: function () {
+            return;
+          },
+          addAnnotationFilter: function () {
+            return;
+          },
+          editorView: this.editorView,
+        });
+
+        //Render the view and insert it into the page
+        this.$(this.dataCatalogViewContainer).html(dataCatalogView.el);
+        dataCatalogView.render();
+
+        this.listenTo(
+          this.model.get("searchResults"),
+          "reset",
+          this.toggleHelpText,
+        );
+      },
+
+      /**
+       * Renders the edit collection controls - e.g. a Save and Cancel buttton
+       */
+      renderCollectionControls: function () {
+        //Create a Save button
+        var saveButton = $(document.createElement("a"))
+            .addClass("save btn btn-primary")
+            .text("Save"),
+          //Create a Cancel button
           cancelButton = $(document.createElement("a"))
-                        .addClass("cancel btn")
-                        .text("Cancel"),
-      //Create a container for the buttons
-          buttons      = $(document.createElement("div"))
-                        .addClass("collection-controls")
-                        .append(saveButton, cancelButton);
-
-      //Add the buttons to the view
-      this.$(this.collectionControlsContainer).append(buttons);
-
-    },
-
-    /**
-     * Either hides or shows the help message that lets the user know
-     * they can add filters when the collection is empty.
-     */
-    toggleHelpText: function() {
-
-      //Get the list of filters currently applied to the collection definition
-      var currentFilters = this.model.get("definitionFilters"),
+            .addClass("cancel btn")
+            .text("Cancel"),
+          //Create a container for the buttons
+          buttons = $(document.createElement("div"))
+            .addClass("collection-controls")
+            .append(saveButton, cancelButton);
+
+        //Add the buttons to the view
+        this.$(this.collectionControlsContainer).append(buttons);
+      },
+
+      /**
+       * Either hides or shows the help message that lets the user know
+       * they can add filters when the collection is empty.
+       */
+      toggleHelpText: function () {
+        //Get the list of filters currently applied to the collection definition
+        var currentFilters = this.model.get("definitionFilters"),
           msg = "";
 
-      // If there are no filters set at all, the entire repository catalog will be listed as
-      // search results, so display a helpful message
-      if ( currentFilters.length == 0 && this.model.get("searchResults").length ) {
-        msg = "<h5>Your dataset collection hasn't been created yet.</h5>" +
-              "<p>The datasets listed here are totally unfiltered. To specify which datasets belong to your collection, " +
-              "add rules in the Query Builder above.</p>";
-      }
-      //If there is only an isPartOf filter, but no datasets have been marked as part of this collection
-      else if( currentFilters.length == 1 &&
-               currentFilters.models[0].get("fields")[0] == "isPartOf" &&
-               !this.model.get("searchResults").length){
-
-         msg = "<h5>Your dataset collection is empty.</h5> " +
-               "<p>To add datasets to your collection, " +
-               "add rules in query builder above.</p>";
-
-        //TODO: When the ability to add datasets to collection via the "isPartOf" relationship is added to MetacatUI
-        // then update this message with details on how to add datasets to the collection
-      }
-
-      //If a message string was created, display it
-      if( msg ){
-        //Show the message
-        MetacatUI.appView.showAlert(msg, "alert-warning", this.$(this.helpTextContainer));
-      }
-      else{
-        //Remove the message
-        this.$(this.helpTextContainer).empty();
-        //Remove validation messaging, too
-        this.$(".notification.error[data-category='definition']").removeClass("error").empty();
-      }
-
-    }
-
-  });
+        // If there are no filters set at all, the entire repository catalog will be listed as
+        // search results, so display a helpful message
+        if (
+          currentFilters.length == 0 &&
+          this.model.get("searchResults").length
+        ) {
+          msg =
+            "<h5>Your dataset collection hasn't been created yet.</h5>" +
+            "<p>The datasets listed here are totally unfiltered. To specify which datasets belong to your collection, " +
+            "add rules in the Query Builder above.</p>";
+        }
+        //If there is only an isPartOf filter, but no datasets have been marked as part of this collection
+        else if (
+          currentFilters.length == 1 &&
+          currentFilters.models[0].get("fields")[0] == "isPartOf" &&
+          !this.model.get("searchResults").length
+        ) {
+          msg =
+            "<h5>Your dataset collection is empty.</h5> " +
+            "<p>To add datasets to your collection, " +
+            "add rules in query builder above.</p>";
+
+          //TODO: When the ability to add datasets to collection via the "isPartOf" relationship is added to MetacatUI
+          // then update this message with details on how to add datasets to the collection
+        }
+
+        //If a message string was created, display it
+        if (msg) {
+          //Show the message
+          MetacatUI.appView.showAlert(
+            msg,
+            "alert-warning",
+            this.$(this.helpTextContainer),
+          );
+        } else {
+          //Remove the message
+          this.$(this.helpTextContainer).empty();
+          //Remove validation messaging, too
+          this.$(".notification.error[data-category='definition']")
+            .removeClass("error")
+            .empty();
+        }
+      },
+    },
+  );
 
   return EditCollectionView;
-
 });
 
diff --git a/docs/docs/src_js_views_EditorView.js.html b/docs/docs/src_js_views_EditorView.js.html index 5615ef074..28fbc5077 100644 --- a/docs/docs/src_js_views_EditorView.js.html +++ b/docs/docs/src_js_views_EditorView.js.html @@ -44,282 +44,293 @@

Source: src/js/views/EditorView.js

-
define(['underscore',
-        'jquery',
-        'backbone',
-        "views/SignInView",
-        "text!templates/editorSubmitMessage.html"],
-function(_, $, Backbone, SignInView, EditorSubmitMessageTemplate){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "views/SignInView",
+  "text!templates/editorSubmitMessage.html",
+], function (_, $, Backbone, SignInView, EditorSubmitMessageTemplate) {
   /**
-  * @class EditorView
-  * @classdesc A basic shell of a view, primarily meant to be extended for views that allow editing capabilities.
-  * @classcategory Views
-  * @name EditorView
-  * @extends Backbone.View
-  * @constructs
-  */
+   * @class EditorView
+   * @classdesc A basic shell of a view, primarily meant to be extended for views that allow editing capabilities.
+   * @classcategory Views
+   * @name EditorView
+   * @extends Backbone.View
+   * @constructs
+   */
   var EditorView = Backbone.View.extend(
-    /** @lends EditorView.prototype */{
-
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    */
-    editorSubmitMessageTemplate: _.template(EditorSubmitMessageTemplate),
-
-    /**
-    * The element this view is contained in. A jQuery selector or the element itself.
-    * @type {string|DOMElement}
-    */
-    el: "#Content",
-
-    /**
-    * The text to use in the editor submit button
-    * @type {string}
-    */
-    submitButtonText: "Save",
-
-    /**
-    * The text to use in the editor submit button
-    * @type {string}
-    */
-    accessPolicyModalID: "editor-access-policy-modal",
-
-    /**
-    * The selector for the HTML element that will contain a button/link/control for
-    * opening the AccessPolicyView modal window. If this element doesn't exist on the page,
-    * then the AccessPolicyView will be inserted into the `accessPolicyViewContainer` directly, rather than a modal window.
-    * @type {string}
-    */
-    accessPolicyControlContainer: ".access-policy-control-container",
-
-    /**
-    * The selector for the HTML element that will contain the AccessPolicyView.
-    * If this element doesn't exist on the page, then the AccessPolicyView will not be inserted into the page.
-    * If a `accessPolicyControlContainer` element is on the page, then this element will
-    * contain the modal window element.
-    * @type {string}
-    */
-    accessPolicyViewContainer: ".access-policy-view-container",
-    /**
-    * The events this view will listen to and the associated function to call
-    * @type {Object}
-    */
-    events: {
-      "click #save-editor" : "save",
-      "click .access-policy-control" : "showAccessPolicyModal",
-      "keypress input:not(.ignore-changes)" : "showControls",
-      "keypress textarea:not(.ignore-changes)" : "showControls",
-      "keypress [contenteditable]:not(.ignore-changes)" : "showControls",
-      "click .image-uploader" : "showControls",
-      "change .access-policy-view" : "showControls",
-      "click .access-policy-view .remove" : "showControls"
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-      //Style the body as an Editor
-      $("body").addClass("Editor rendering");
-
-      this.delegateEvents();
-
-      //If there is no active alternate repository, set one
-      if( !MetacatUI.appModel.getActiveAltRepo() && MetacatUI.appModel.get("alternateRepositories").length ){
-        MetacatUI.appModel.setActiveAltRepo();
-      }
-    },
-
-    /**
-     * Set listeners on the view's model.
-     * This function centralizes all the listeners so that when/if the view's
-     * model is replaced, the listeners can be reset.
-     */
-    setListeners: function() {
-
-      //Stop listening first
-      this.stopListening(this.model, "errorSaving", this.saveError);
-      this.stopListening(this.model, "successSaving", this.saveSuccess);
-      this.stopListening(this.model, "invalid", this.showValidation);
-
-      //Set listeners
-      this.listenTo(this.model, "errorSaving", this.saveError);
-      this.listenTo(this.model, "successSaving", this.saveSuccess);
-      this.listenTo(this.model, "invalid", this.showValidation);
-
-      // //Set a beforeunload event only if there isn't one already
-      // if( !this.beforeunloadCallback ){
-      //   var view = this;
-      //   //When the Window is about to be closed, show a confirmation message
-      //   this.beforeunloadCallback = function(e){
-      //     if( !view.canClose() ){
-      //       //Browsers don't support custom confirmation messages anymore,
-      //       // so preventDefault() needs to be called or the return value has to be set
-      //       e.preventDefault();
-      //       e.returnValue = "";
-      //     }
-      //     return;
-      //   }
-      //   window.addEventListener("beforeunload", this.beforeunloadCallback);
-      // }
-    },
-
-    /**
-    * Show Sign In buttons
-    */
-    showSignIn: function(){
-      var container = $(document.createElement("div")).addClass("container center");
-      this.$el.html(container);
-      var signInButtons = new SignInView().render().el;
-      $(container).append('<h1>Sign in to submit data</h1>', signInButtons);
-    },
-
-    /**
-    * Saves the model
-    */
-    save: function(){
-      this.showSaving();
-      this.model.save();
-    },
-
-    /**
-     * Cancel all edits in the editor by simply re-rendering the view
-     */
-    cancel: function(){
-      this.render();
-    },
-
-    /**
-    * Trigger a save error with a message that the save was cancelled
-    */
-    handleSaveCancel: function(){
-      if(this.model.get("uploadStatus") == "e"){
-        this.saveError("Your submission was cancelled due to an error.");
-      }
-    },
-
-    /**
-    * Adds top-level control elements to this editor.
-    */
-    renderEditorControls: function(){
-      //If the AccessPolicy editor is enabled, add a button for opening it
-      if( MetacatUI.appModel.get("allowAccessPolicyChanges")){
-        this.renderAccessPolicyControl();
-      }
-    },
-
-    /**
-    * Adds a Share button for editing the access policy
-    */
-    renderAccessPolicyControl: function(){
-      //If the AccessPolicy editor is enabled, add a button for opening it
-      if( this.isAccessPolicyEditEnabled() ){
-
-        var isHiddenBehindControl = (this.$(this.accessPolicyControlContainer).length > 0);
-
-        //Render the AccessPolicy control, if the container element is on the page
-        if( isHiddenBehindControl ){
-          //If it isn't, then add it to the page.
-          //Create an anchor tag with an icon and the text "Share" and add it to the editor controls container
-          this.$(this.accessPolicyControlContainer).prepend( $(document.createElement("a"))
-                                                    .attr("href", "#")
-                                                    .addClass("access-policy-control btn")
-                                                    .append(
-                                                      $(document.createElement("i")).addClass("icon-group icon icon-on-left"),
-                                                      "Share") );
+    /** @lends EditorView.prototype */ {
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      editorSubmitMessageTemplate: _.template(EditorSubmitMessageTemplate),
+
+      /**
+       * The element this view is contained in. A jQuery selector or the element itself.
+       * @type {string|DOMElement}
+       */
+      el: "#Content",
+
+      /**
+       * The text to use in the editor submit button
+       * @type {string}
+       */
+      submitButtonText: "Save",
+
+      /**
+       * The text to use in the editor submit button
+       * @type {string}
+       */
+      accessPolicyModalID: "editor-access-policy-modal",
+
+      /**
+       * The selector for the HTML element that will contain a button/link/control for
+       * opening the AccessPolicyView modal window. If this element doesn't exist on the page,
+       * then the AccessPolicyView will be inserted into the `accessPolicyViewContainer` directly, rather than a modal window.
+       * @type {string}
+       */
+      accessPolicyControlContainer: ".access-policy-control-container",
+
+      /**
+       * The selector for the HTML element that will contain the AccessPolicyView.
+       * If this element doesn't exist on the page, then the AccessPolicyView will not be inserted into the page.
+       * If a `accessPolicyControlContainer` element is on the page, then this element will
+       * contain the modal window element.
+       * @type {string}
+       */
+      accessPolicyViewContainer: ".access-policy-view-container",
+      /**
+       * The events this view will listen to and the associated function to call
+       * @type {Object}
+       */
+      events: {
+        "click #save-editor": "save",
+        "click .access-policy-control": "showAccessPolicyModal",
+        "keypress input:not(.ignore-changes)": "showControls",
+        "keypress textarea:not(.ignore-changes)": "showControls",
+        "keypress [contenteditable]:not(.ignore-changes)": "showControls",
+        "click .image-uploader": "showControls",
+        "change .access-policy-view": "showControls",
+        "click .access-policy-view .remove": "showControls",
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        //Style the body as an Editor
+        $("body").addClass("Editor rendering");
+
+        this.delegateEvents();
+
+        //If there is no active alternate repository, set one
+        if (
+          !MetacatUI.appModel.getActiveAltRepo() &&
+          MetacatUI.appModel.get("alternateRepositories").length
+        ) {
+          MetacatUI.appModel.setActiveAltRepo();
         }
-
-        //If the authorization has already been checked
-        if( this.model.get("isAuthorized_changePermission") === true ){
-          //Render the AccessPolicyView
-          this.renderAccessPolicy();
+      },
+
+      /**
+       * Set listeners on the view's model.
+       * This function centralizes all the listeners so that when/if the view's
+       * model is replaced, the listeners can be reset.
+       */
+      setListeners: function () {
+        //Stop listening first
+        this.stopListening(this.model, "errorSaving", this.saveError);
+        this.stopListening(this.model, "successSaving", this.saveSuccess);
+        this.stopListening(this.model, "invalid", this.showValidation);
+
+        //Set listeners
+        this.listenTo(this.model, "errorSaving", this.saveError);
+        this.listenTo(this.model, "successSaving", this.saveSuccess);
+        this.listenTo(this.model, "invalid", this.showValidation);
+
+        // //Set a beforeunload event only if there isn't one already
+        // if( !this.beforeunloadCallback ){
+        //   var view = this;
+        //   //When the Window is about to be closed, show a confirmation message
+        //   this.beforeunloadCallback = function(e){
+        //     if( !view.canClose() ){
+        //       //Browsers don't support custom confirmation messages anymore,
+        //       // so preventDefault() needs to be called or the return value has to be set
+        //       e.preventDefault();
+        //       e.returnValue = "";
+        //     }
+        //     return;
+        //   }
+        //   window.addEventListener("beforeunload", this.beforeunloadCallback);
+        // }
+      },
+
+      /**
+       * Show Sign In buttons
+       */
+      showSignIn: function () {
+        var container = $(document.createElement("div")).addClass(
+          "container center",
+        );
+        this.$el.html(container);
+        var signInButtons = new SignInView().render().el;
+        $(container).append("<h1>Sign in to submit data</h1>", signInButtons);
+      },
+
+      /**
+       * Saves the model
+       */
+      save: function () {
+        this.showSaving();
+        this.model.save();
+      },
+
+      /**
+       * Cancel all edits in the editor by simply re-rendering the view
+       */
+      cancel: function () {
+        this.render();
+      },
+
+      /**
+       * Trigger a save error with a message that the save was cancelled
+       */
+      handleSaveCancel: function () {
+        if (this.model.get("uploadStatus") == "e") {
+          this.saveError("Your submission was cancelled due to an error.");
         }
-        else{
-          //When the user's changePermission authority has been checked, edit their
-          //  access to the AccessPolicyView
-          this.listenToOnce(this.model, "change:isAuthorized_changePermission", function(){
-            //If there is an AccessPolicy control, disable it
-            if( isHiddenBehindControl ){
-
-              if( this.model.get("isAuthorized_changePermission") === false ){
-                //Disable the button for the AccessPolicyView if the user is not authorized
-                this.$(".access-policy-control").attr("disabled", "disabled")
-                                                .attr("title", "You do not have access to change the " + MetacatUI.appModel.get("accessPolicyName"))
-                                                .addClass("disabled");
-              }
-            }
-            else{
-              //Render the AccessPolicyView
-              this.renderAccessPolicy();
-            }
-          });
-
-          //Check the user's authority to change permissions on this object
-          this.model.checkAuthority("changePermission");
+      },
+
+      /**
+       * Adds top-level control elements to this editor.
+       */
+      renderEditorControls: function () {
+        //If the AccessPolicy editor is enabled, add a button for opening it
+        if (MetacatUI.appModel.get("allowAccessPolicyChanges")) {
+          this.renderAccessPolicyControl();
         }
-
-      }
-    },
-
-    /**
-    * Shows the AccessPolicyView for the object being edited.
-    *
-    * @param {Event} e - The click event
-    * @param {Backbone.Model | null} model - The model to show the view for. If
-    *   null, defaults to the model set for the view.
-    */
-    showAccessPolicyModal: function(e, model){
-      try{
-
-        // If the AccessPolicy editor is disabled in this app, or the specific
-        // .access-policy-control has theh class diasbled, then exit now
-        if (!MetacatUI.appModel.get("allowAccessPolicyChanges") ||
-          this.$(".access-policy-control").attr("disabled") == "disabled" ||
-          (e.currentTarget && $(e.currentTarget).hasClass("disabled"))) {
-          return;
+      },
+
+      /**
+       * Adds a Share button for editing the access policy
+       */
+      renderAccessPolicyControl: function () {
+        //If the AccessPolicy editor is enabled, add a button for opening it
+        if (this.isAccessPolicyEditEnabled()) {
+          var isHiddenBehindControl =
+            this.$(this.accessPolicyControlContainer).length > 0;
+
+          //Render the AccessPolicy control, if the container element is on the page
+          if (isHiddenBehindControl) {
+            //If it isn't, then add it to the page.
+            //Create an anchor tag with an icon and the text "Share" and add it to the editor controls container
+            this.$(this.accessPolicyControlContainer).prepend(
+              $(document.createElement("a"))
+                .attr("href", "#")
+                .addClass("access-policy-control btn")
+                .append(
+                  $(document.createElement("i")).addClass(
+                    "icon-group icon icon-on-left",
+                  ),
+                  "Share",
+                ),
+            );
+          }
+
+          //If the authorization has already been checked
+          if (this.model.get("isAuthorized_changePermission") === true) {
+            //Render the AccessPolicyView
+            this.renderAccessPolicy();
+          } else {
+            //When the user's changePermission authority has been checked, edit their
+            //  access to the AccessPolicyView
+            this.listenToOnce(
+              this.model,
+              "change:isAuthorized_changePermission",
+              function () {
+                //If there is an AccessPolicy control, disable it
+                if (isHiddenBehindControl) {
+                  if (
+                    this.model.get("isAuthorized_changePermission") === false
+                  ) {
+                    //Disable the button for the AccessPolicyView if the user is not authorized
+                    this.$(".access-policy-control")
+                      .attr("disabled", "disabled")
+                      .attr(
+                        "title",
+                        "You do not have access to change the " +
+                          MetacatUI.appModel.get("accessPolicyName"),
+                      )
+                      .addClass("disabled");
+                  }
+                } else {
+                  //Render the AccessPolicyView
+                  this.renderAccessPolicy();
+                }
+              },
+            );
+
+            //Check the user's authority to change permissions on this object
+            this.model.checkAuthority("changePermission");
+          }
         }
-
-
-        this.renderAccessPolicy(model);
-
-        this.on("accessPolicyViewRendered", function(){
-          //Add modal classes to the access policy view
-          this.$(".access-policy-view").addClass("access-policy-view-modal modal")
-                                      .css("height", window.outerHeight * .7)
-                                      .modal()
-                                      .modal("show");
-        });
-
-      }
-      catch(e){
-        console.error("Error trying to show the AccessPolicyView: ", e);
-      }
-    },
-
-    /**
-    * Renders the AccessPolicyView
-    * @param {Backbone.Model} model - Optional. The Model to render the
-    *   AccessPolicy of. If not passed, method uses the Editor's model
-    */
-    renderAccessPolicy: function(model){
-      // Use specified model or default to the editor's model
-      model = model || this.model;
-
-      try{
-
-        //If the AccessPolicy editor is disabled in this app, then exit now
-        if( !MetacatUI.appModel.get("allowAccessPolicyChanges")){
-          return;
+      },
+
+      /**
+       * Shows the AccessPolicyView for the object being edited.
+       *
+       * @param {Event} e - The click event
+       * @param {Backbone.Model | null} model - The model to show the view for. If
+       *   null, defaults to the model set for the view.
+       */
+      showAccessPolicyModal: function (e, model) {
+        try {
+          // If the AccessPolicy editor is disabled in this app, or the specific
+          // .access-policy-control has theh class diasbled, then exit now
+          if (
+            !MetacatUI.appModel.get("allowAccessPolicyChanges") ||
+            this.$(".access-policy-control").attr("disabled") == "disabled" ||
+            (e.currentTarget && $(e.currentTarget).hasClass("disabled"))
+          ) {
+            return;
+          }
+
+          this.renderAccessPolicy(model);
+
+          this.on("accessPolicyViewRendered", function () {
+            //Add modal classes to the access policy view
+            this.$(".access-policy-view")
+              .addClass("access-policy-view-modal modal")
+              .css("height", window.outerHeight * 0.7)
+              .modal()
+              .modal("show");
+          });
+        } catch (e) {
+          console.error("Error trying to show the AccessPolicyView: ", e);
         }
-
-        var thisView = this;
-        require(['views/AccessPolicyView'], function(AccessPolicyView){
-
+      },
+
+      /**
+       * Renders the AccessPolicyView
+       * @param {Backbone.Model} model - Optional. The Model to render the
+       *   AccessPolicy of. If not passed, method uses the Editor's model
+       */
+      renderAccessPolicy: function (model) {
+        // Use specified model or default to the editor's model
+        model = model || this.model;
+
+        try {
+          //If the AccessPolicy editor is disabled in this app, then exit now
+          if (!MetacatUI.appModel.get("allowAccessPolicyChanges")) {
+            return;
+          }
+
+          var thisView = this;
+          require(["views/AccessPolicyView"], function (AccessPolicyView) {
             // Create a new AccessPolicyView using the AccessPolicy collection
             var accessPolicyView = new AccessPolicyView({
-              collection: model.get("accessPolicy")
+              collection: model.get("accessPolicy"),
             });
 
             // Turn on accessPolicy broadcasting for metadata models
@@ -331,424 +342,467 @@ 

Source: src/js/views/EditorView.js

thisView.accessPolicyView = accessPolicyView; //Add the view to the page - thisView.$(thisView.accessPolicyViewContainer).html(accessPolicyView.el); + thisView + .$(thisView.accessPolicyViewContainer) + .html(accessPolicyView.el); //Render the AccessPolicyView accessPolicyView.render(); thisView.trigger("accessPolicyViewRendered"); - thisView.listenTo(accessPolicyView.collection, "add remove", thisView.showControls); - }); - } - catch(e){ - console.error("Error trying to render the AccessPolicyView: ", e); - } - }, - - /** - * Checks if the Access Policy editor is enabled in this instance of MetacatUI for - * the type of object being edited. - * @returns {boolean} - * @since 2.15.0 - */ - isAccessPolicyEditEnabled: function(){ - - if( !MetacatUI.appModel.get("allowAccessPolicyChanges") ){ - return false; - } - - }, - - /** - * Show the editor footer controls (Save bar) - */ - showControls: function(){ - var view = this; - this.$(".editor-controls").removeClass("hidden").slideDown(300, function(){ - if(typeof view.handleScroll === "function"){ - view.handleScroll() + thisView.listenTo( + accessPolicyView.collection, + "add remove", + thisView.showControls, + ); + }); + } catch (e) { + console.error("Error trying to render the AccessPolicyView: ", e); } - }); - - }, - - /** - * Hide the editor footer controls (Save bar) - */ - hideControls: function(){ - var view = this; - this.hideSaving(); - this.$(".editor-controls").slideUp(300, function(){ - if(typeof view.handleScroll === "function"){ - view.handleScroll() + }, + + /** + * Checks if the Access Policy editor is enabled in this instance of MetacatUI for + * the type of object being edited. + * @returns {boolean} + * @since 2.15.0 + */ + isAccessPolicyEditEnabled: function () { + if (!MetacatUI.appModel.get("allowAccessPolicyChanges")) { + return false; } - }); - }, - - /** - * Change the styling of this view to show that the object is in the process of saving - */ - showSaving: function(){ - - //Change the style of the save button - this.$("#save-editor") - .html('<i class="icon icon-spinner icon-spin"></i> Submitting ...') - .addClass("btn-disabled"); - - //Remove all the validation messaging - this.removeValidation(); - - //Get all the inputs in the Editor - var allInputs = this.$("input, textarea, select, button"); - - //Mark the disabled inputs so we can re-disable them later - allInputs.filter(":disabled") - .not(".label-container .label-input-text") - .addClass("disabled-saving"); - - //Remove the latest success or error alert - this.$el.children(".alert-container").remove(); + }, + + /** + * Show the editor footer controls (Save bar) + */ + showControls: function () { + var view = this; + this.$(".editor-controls") + .removeClass("hidden") + .slideDown(300, function () { + if (typeof view.handleScroll === "function") { + view.handleScroll(); + } + }); + }, + + /** + * Hide the editor footer controls (Save bar) + */ + hideControls: function () { + var view = this; + this.hideSaving(); + this.$(".editor-controls").slideUp(300, function () { + if (typeof view.handleScroll === "function") { + view.handleScroll(); + } + }); + }, + + /** + * Change the styling of this view to show that the object is in the process of saving + */ + showSaving: function () { + //Change the style of the save button + this.$("#save-editor") + .html('<i class="icon icon-spinner icon-spin"></i> Submitting ...') + .addClass("btn-disabled"); + + //Remove all the validation messaging + this.removeValidation(); + + //Get all the inputs in the Editor + var allInputs = this.$("input, textarea, select, button"); + + //Mark the disabled inputs so we can re-disable them later + allInputs + .filter(":disabled") + .not(".label-container .label-input-text") + .addClass("disabled-saving"); - //Disable all the inputs - allInputs.prop("disabled", true); + //Remove the latest success or error alert + this.$el.children(".alert-container").remove(); - }, + //Disable all the inputs + allInputs.prop("disabled", true); + }, - /** - * Remove the styles set in showSaving() - */ - hideSaving: function(){ - this.$("input, textarea, select, button") + /** + * Remove the styles set in showSaving() + */ + hideSaving: function () { + this.$("input, textarea, select, button") .not(".label-container .label-input-text") .prop("disabled", false); - this.$(".disabled-saving, input.disabled") + this.$(".disabled-saving, input.disabled") .not(".label-container .label-input-text") .prop("disabled", true) .removeClass("disabled-saving"); //When the package is saved, revert the Save button back to normal - this.$("#save-editor").html(this.submitButtonText).removeClass("btn-disabled"); - - }, - - /** - * Enable the Save button. Resets any changes made in {@link EditorView#disableControls} - * @since 2.17.1 - */ - enableControls: function(){ - //When the package is saved, revert the Save button back to normal - this.$("#save-editor").html(this.submitButtonText) - .removeClass("btn-disabled") - .parent() - .tooltip("destroy"); - - }, - - /** - * Disable the Save button and display a message to explain why - * @param {string} [message] - A short text message to display in the Save button - * @since 2.17.1 - */ - disableControls: function(message){ - //When the package is saved, revert the Save button back to normal - this.$("#save-editor").html(message || "Waiting for files to finish uploading...") - .addClass("btn-disabled") - .parent() //Add a tooltip to the parent element since tooltips won't work on a disabled button - .tooltip({ - placement: "top", - trigger: "hover focus click", - html: false, - title: "Saving is disabled while files are uploading. Please wait...", - container: "body", - delay: 600 - }); - - }, - - /** - * Style the view to show that it is loading - * @param {string|DOMElement} container - The element to put the loading styling in. Either a jQuery selector or the element itself. - * @param {string|DOMElement} message - The message to display next to the loading icon. Either a jQuery selector or the element itself. - */ - showLoading: function(container, message){ - if(typeof container == "undefined" || !container) - var container = this.$el; - - $(container).html(MetacatUI.appView.loadingTemplate({ msg: message })); - }, - - /** - * Remove the styles set in showLoading() - * @param {string|DOMElement} container - The element the loading message is conttained in. Either a jQuery selector or the element itself. - */ - hideLoading: function(container){ - if(typeof container == "undefined" || !container) - var container = this.$el; - - $(container).find(".loading").remove(); - }, - - /** - * Called when there is no object found with this ID - */ - showNotFound: function(){ + this.$("#save-editor") + .html(this.submitButtonText) + .removeClass("btn-disabled"); + }, + + /** + * Enable the Save button. Resets any changes made in {@link EditorView#disableControls} + * @since 2.17.1 + */ + enableControls: function () { + //When the package is saved, revert the Save button back to normal + this.$("#save-editor") + .html(this.submitButtonText) + .removeClass("btn-disabled") + .parent() + .tooltip("destroy"); + }, + + /** + * Disable the Save button and display a message to explain why + * @param {string} [message] - A short text message to display in the Save button + * @since 2.17.1 + */ + disableControls: function (message) { + //When the package is saved, revert the Save button back to normal + this.$("#save-editor") + .html(message || "Waiting for files to finish uploading...") + .addClass("btn-disabled") + .parent() //Add a tooltip to the parent element since tooltips won't work on a disabled button + .tooltip({ + placement: "top", + trigger: "hover focus click", + html: false, + title: + "Saving is disabled while files are uploading. Please wait...", + container: "body", + delay: 600, + }); + }, + + /** + * Style the view to show that it is loading + * @param {string|DOMElement} container - The element to put the loading styling in. Either a jQuery selector or the element itself. + * @param {string|DOMElement} message - The message to display next to the loading icon. Either a jQuery selector or the element itself. + */ + showLoading: function (container, message) { + if (typeof container == "undefined" || !container) + var container = this.$el; + + $(container).html(MetacatUI.appView.loadingTemplate({ msg: message })); + }, + + /** + * Remove the styles set in showLoading() + * @param {string|DOMElement} container - The element the loading message is conttained in. Either a jQuery selector or the element itself. + */ + hideLoading: function (container) { + if (typeof container == "undefined" || !container) + var container = this.$el; + + $(container).find(".loading").remove(); + }, + + /** + * Called when there is no object found with this ID + */ + showNotFound: function () { //If we haven't checked the logged-in status of the user yet, wait a bit until we show a 404 msg, in case this content is their private content - if(!MetacatUI.appUserModel.get("checked")){ - this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.showNotFound); + if (!MetacatUI.appUserModel.get("checked")) { + this.listenToOnce( + MetacatUI.appUserModel, + "change:checked", + this.showNotFound, + ); return; } //If the user is not logged in - else if(!MetacatUI.appUserModel.get("loggedIn")){ + else if (!MetacatUI.appUserModel.get("loggedIn")) { this.showSignIn(); return; } - if(!this.model.get("notFound")) return; + if (!this.model.get("notFound")) return; - var msg = "<h4>Nothing was found for one of the following reasons:</h4>" + + var msg = + "<h4>Nothing was found for one of the following reasons:</h4>" + "<ul class='indent'>" + - "<li>The ID <span id='editor-view-not-found-pid'></span> does not exist.</li>" + - '<li>This may be private content. (Are you <a href="<%= MetacatUI.root %>/signin">signed in?</a>)</li>' + - "<li>The content was removed because it was invalid.</li>" + + "<li>The ID <span id='editor-view-not-found-pid'></span> does not exist.</li>" + + '<li>This may be private content. (Are you <a href="<%= MetacatUI.root %>/signin">signed in?</a>)</li>' + + "<li>The content was removed because it was invalid.</li>" + "</ul>"; //Remove the loading messaging this.hideLoading(); //Show the not found message - MetacatUI.appView.showAlert(msg, "alert-error", this.$("#editor-body"), null, {remove: true}); + MetacatUI.appView.showAlert( + msg, + "alert-error", + this.$("#editor-body"), + null, + { remove: true }, + ); this.$("#editor-view-not-found-pid").text(this.pid); - - }, - - /** - * Check the validity of this view's model - */ - checkValidity: function(){ - if(this.model.isValid()) - this.model.trigger("valid"); - }, - - /** - * Show validation errors, if there are any - */ - showValidation: function(){ - this.saveError("Unable to save. Either required information is missing or isn't filled out correctly."); - }, - - /** - * Removes all the validation error styling and messaging from this view - */ - removeValidation: function(){ - this.$(".notification.error").removeClass("error").empty(); - this.$(".validation-error-icon").hide(); - }, - - /** - * When the object is saved successfully, tell the user - * @param {object} savedObject - the object that was successfully saved - */ - saveSuccess: function(savedObject){ - - var message = this.editorSubmitMessageTemplate({ - messageText: "Your changes have been submitted.", - viewURL: MetacatUI.appModel.get("baseUrl"), - buttonText: "Return home" + }, + + /** + * Check the validity of this view's model + */ + checkValidity: function () { + if (this.model.isValid()) this.model.trigger("valid"); + }, + + /** + * Show validation errors, if there are any + */ + showValidation: function () { + this.saveError( + "Unable to save. Either required information is missing or isn't filled out correctly.", + ); + }, + + /** + * Removes all the validation error styling and messaging from this view + */ + removeValidation: function () { + this.$(".notification.error").removeClass("error").empty(); + this.$(".validation-error-icon").hide(); + }, + + /** + * When the object is saved successfully, tell the user + * @param {object} savedObject - the object that was successfully saved + */ + saveSuccess: function (savedObject) { + var message = this.editorSubmitMessageTemplate({ + messageText: "Your changes have been submitted.", + viewURL: MetacatUI.appModel.get("baseUrl"), + buttonText: "Return home", }); - MetacatUI.appView.showAlert(message, "alert-success", this.$el, null, {remove: true}); - - this.hideSaving(); - - }, - - /** - * When the object fails to save, tell the user - * @param {string} errorMsg - The error message resulting from a failed attempt to save - */ - saveError: function(errorMsg){ + MetacatUI.appView.showAlert(message, "alert-success", this.$el, null, { + remove: true, + }); - var messageContainer = $(document.createElement("div")).append(document.createElement("p")), + this.hideSaving(); + }, + + /** + * When the object fails to save, tell the user + * @param {string} errorMsg - The error message resulting from a failed attempt to save + */ + saveError: function (errorMsg) { + var messageContainer = $(document.createElement("div")).append( + document.createElement("p"), + ), messageParagraph = messageContainer.find("p"), messageClasses = "alert-error"; - messageParagraph.append(errorMsg); - - //If the model has an error message set on it, show it in a collapseable technical details section - if( this.model.get("errorMessage") ){ - var errorId = "error" + Math.round(Math.random()*100); - messageParagraph.after($(document.createElement("p")).append($(document.createElement("a")) - .text("See technical details") - .attr("data-toggle", "collapse") - .attr("data-target", "#" + errorId) - .addClass("pointer")), - $(document.createElement("div")) - .addClass("collapse") - .attr("id", errorId) - .append($(document.createElement("pre")).text(this.model.get("errorMessage")))); - } - - MetacatUI.appView.showAlert(messageContainer, messageClasses, this.$el, null, { - emailBody: errorMsg, - remove: true - }); - - this.hideSaving(); - }, - - /** - * Shows the required icons for the sections and fields that must be completed in this editor. - * @param {object} requiredFields - A literal object that specified which fields should be required. - * The keys on the object map to model attributes, and the value is true if required, false if optional. - */ - renderRequiredIcons: function(requiredFields){ - - //If no required fields are given, exit now - if( typeof requiredFields == "undefined" ){ - return; - } - - _.each( Object.keys(requiredFields), function(field){ - - if(requiredFields[field]){ - var reqEl = this.$(".required-icon[data-category='" + field + "']"); - - //Show the required icon for this category/field - reqEl.show(); - - //Show the required icon for the section - var sectionName = reqEl.parents(".section[data-section]").attr("data-section"); - this.$(".required-icon[data-section='" + sectionName + "']").show(); + messageParagraph.append(errorMsg); + + //If the model has an error message set on it, show it in a collapseable technical details section + if (this.model.get("errorMessage")) { + var errorId = "error" + Math.round(Math.random() * 100); + messageParagraph.after( + $(document.createElement("p")).append( + $(document.createElement("a")) + .text("See technical details") + .attr("data-toggle", "collapse") + .attr("data-target", "#" + errorId) + .addClass("pointer"), + ), + $(document.createElement("div")) + .addClass("collapse") + .attr("id", errorId) + .append( + $(document.createElement("pre")).text( + this.model.get("errorMessage"), + ), + ), + ); } - }, this); - - //When new inputs have been added to this Editor, re-render these required icons. - // This is helpful when new questions are added to the editor after the intial rendering. - this.off("editorInputsAdded"); - this.on("editorInputsAdded", function(){ - this.renderRequiredIcons(requiredFields); - }, this); - }, - - /** - * Gets a list of required fields for this editor, or an empty object if there are none. - * @returns {object} - * @since 2.19.0 - */ - getRequiredFields: function(){ - return {} - - }, - - /** - * Checks if there are unsaved changes in this Editor that should prevent closing of this view. - * This function is also executed by the AppView, which controls the top-level navigation. - * @returns {boolean} Returns true if this view should be closed. False if it should remain opened and active. - */ - canClose: function(){ + MetacatUI.appView.showAlert( + messageContainer, + messageClasses, + this.$el, + null, + { + emailBody: errorMsg, + remove: true, + }, + ); + + this.hideSaving(); + }, + + /** + * Shows the required icons for the sections and fields that must be completed in this editor. + * @param {object} requiredFields - A literal object that specified which fields should be required. + * The keys on the object map to model attributes, and the value is true if required, false if optional. + */ + renderRequiredIcons: function (requiredFields) { + //If no required fields are given, exit now + if (typeof requiredFields == "undefined") { + return; + } - //If the user isn't logged in, we can leave this view without confirmation - if( !MetacatUI.appUserModel.get("loggedIn") ) - return true; + _.each( + Object.keys(requiredFields), + function (field) { + if (requiredFields[field]) { + var reqEl = this.$( + ".required-icon[data-category='" + field + "']", + ); + + //Show the required icon for this category/field + reqEl.show(); + + //Show the required icon for the section + var sectionName = reqEl + .parents(".section[data-section]") + .attr("data-section"); + this.$( + ".required-icon[data-section='" + sectionName + "']", + ).show(); + } + }, + this, + ); + + //When new inputs have been added to this Editor, re-render these required icons. + // This is helpful when new questions are added to the editor after the intial rendering. + this.off("editorInputsAdded"); + this.on( + "editorInputsAdded", + function () { + this.renderRequiredIcons(requiredFields); + }, + this, + ); + }, + + /** + * Gets a list of required fields for this editor, or an empty object if there are none. + * @returns {object} + * @since 2.19.0 + */ + getRequiredFields: function () { + return {}; + }, + + /** + * Checks if there are unsaved changes in this Editor that should prevent closing of this view. + * This function is also executed by the AppView, which controls the top-level navigation. + * @returns {boolean} Returns true if this view should be closed. False if it should remain opened and active. + */ + canClose: function () { + //If the user isn't logged in, we can leave this view without confirmation + if (!MetacatUI.appUserModel.get("loggedIn")) return true; + + //If there are no unsaved changes, we can leave this view without confirmation + if (!this.hasUnsavedChanges()) { + return true; + } - //If there are no unsaved changes, we can leave this view without confirmation - if( !this.hasUnsavedChanges() ){ + return false; + }, + + /** + * This function is called whenever the user is about to leave this view. + * @returns {string} The message that asks the user if they are sure they want to close this view + */ + getConfirmCloseMessage: function () { + //Return a confirmation message + return "Leave this page? All of your unsaved changes will be lost."; + }, + + /** + * Returns true if there are unsaved changes in this Editor + * This function should be extended by each subclass of EditorView to check for unsaved changes for that model type + * @returns {boolean} + */ + hasUnsavedChanges: function () { return true; - } - - return false; - - }, - - /** - * This function is called whenever the user is about to leave this view. - * @returns {string} The message that asks the user if they are sure they want to close this view - */ - getConfirmCloseMessage: function(){ - - //Return a confirmation message - return "Leave this page? All of your unsaved changes will be lost."; - - }, - - /** - * Returns true if there are unsaved changes in this Editor - * This function should be extended by each subclass of EditorView to check for unsaved changes for that model type - * @returns {boolean} - */ - hasUnsavedChanges: function(){ - return true; - }, - - /** - * Creates an HTML string to display this error message on the page. Errors can be - * strings, arrays of strings, arrays of literal objects with string values, or a literal object with strings as the values. - * @param {string|string[]|object} error A single error message in string format or a collection of error strings as an array or object - * @returns {string} The error message HTML - * @since 2.18.0 - */ - getErrorListItem: function(error){ - try{ - - let errorMessage = ""; - - //Strings get added to a list item HTML element - if( typeof error == "string" && error.trim().length ){ - return `<li>${error}</li>`; - } - //If the error is an array, iterate over each error in the array - else if( Array.isArray(error) ){ - _.each(error, function(subError){ - errorMessage += this.getErrorListItem(subError); - }, this); - return errorMessage; - } - //If the error is a literal object, iterate over each key in the object - else if( typeof error == "object" ){ - _.each(Object.keys(error), function(errorKey){ - errorMessage += this.getErrorListItem(error[errorKey]); - }, this); - return errorMessage; - } - //Default to returning an empty string - else{ + }, + + /** + * Creates an HTML string to display this error message on the page. Errors can be + * strings, arrays of strings, arrays of literal objects with string values, or a literal object with strings as the values. + * @param {string|string[]|object} error A single error message in string format or a collection of error strings as an array or object + * @returns {string} The error message HTML + * @since 2.18.0 + */ + getErrorListItem: function (error) { + try { + let errorMessage = ""; + + //Strings get added to a list item HTML element + if (typeof error == "string" && error.trim().length) { + return `<li>${error}</li>`; + } + //If the error is an array, iterate over each error in the array + else if (Array.isArray(error)) { + _.each( + error, + function (subError) { + errorMessage += this.getErrorListItem(subError); + }, + this, + ); + return errorMessage; + } + //If the error is a literal object, iterate over each key in the object + else if (typeof error == "object") { + _.each( + Object.keys(error), + function (errorKey) { + errorMessage += this.getErrorListItem(error[errorKey]); + }, + this, + ); + return errorMessage; + } + //Default to returning an empty string + else { + return ""; + } + } catch (e) { + console.error( + "Failed to create the error message to show in the editor: ", + e, + ); return ""; } - } - catch(e){ - console.error("Failed to create the error message to show in the editor: ", e); - return ""; - } - }, - - /** - * Perform clean-up functions when this view is about to be removed from the page or navigated away from. - */ - onClose: function(){ - - //Remove the listener on the Window - if( this.beforeunloadCallback ){ - window.removeEventListener("beforeunload", this.beforeunloadCallback); - delete this.beforeunloadCallback; - } - - //Reset the active alternate repository - MetacatUI.appModel.set("activeAlternateRepositoryId", null); - - //Remove the class from the body element - $("body").removeClass("Editor rendering"); + }, + + /** + * Perform clean-up functions when this view is about to be removed from the page or navigated away from. + */ + onClose: function () { + //Remove the listener on the Window + if (this.beforeunloadCallback) { + window.removeEventListener("beforeunload", this.beforeunloadCallback); + delete this.beforeunloadCallback; + } - //Remove listeners - this.stopListening(); - this.undelegateEvents(); + //Reset the active alternate repository + MetacatUI.appModel.set("activeAlternateRepositoryId", null); - } + //Remove the class from the body element + $("body").removeClass("Editor rendering"); - }); + //Remove listeners + this.stopListening(); + this.undelegateEvents(); + }, + }, + ); return EditorView; }); diff --git a/docs/docs/src_js_views_FooterView.js.html b/docs/docs/src_js_views_FooterView.js.html index cd5e1820c..b7b1736b4 100644 --- a/docs/docs/src_js_views_FooterView.js.html +++ b/docs/docs/src_js_views_FooterView.js.html @@ -44,26 +44,27 @@

Source: src/js/views/FooterView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'text!templates/footer.html'],
-  function ($, _, Backbone, FooterTemplate) {
-    'use strict';
-
-    /**
-    * @class FooterView
-    * @classdesc The FooterView renders the main footer for the application, at the bottom of each page.
-    * @classcategory Views
-    * @extends Backbone.View
-    */
-    var FooterView = Backbone.View.extend(
-      /** @lends FooterView.prototype */{
-
-      el: '#Footer',
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/footer.html",
+], function ($, _, Backbone, FooterTemplate) {
+  "use strict";
+
+  /**
+   * @class FooterView
+   * @classdesc The FooterView renders the main footer for the application, at the bottom of each page.
+   * @classcategory Views
+   * @extends Backbone.View
+   */
+  var FooterView = Backbone.View.extend(
+    /** @lends FooterView.prototype */ {
+      el: "#Footer",
 
       template: _.template(FooterTemplate),
 
-      initialize: function () {
-      },
+      initialize: function () {},
 
       render: function () {
         this.$el.html(this.template());
@@ -74,8 +75,8 @@ 

Source: src/js/views/FooterView.js

* @since 2.19.0 */ hide: function () { - this.el.style.setProperty('display', 'none') - document.body.style.setProperty('--footer-height', '0') + this.el.style.setProperty("display", "none"); + document.body.style.setProperty("--footer-height", "0"); }, /** @@ -83,13 +84,13 @@

Source: src/js/views/FooterView.js

* @since 2.19.0 */ show: function () { - this.el.style.removeProperty('display') - document.body.style.removeProperty('--footer-height') - } - - }); - return FooterView; - }); + this.el.style.removeProperty("display"); + document.body.style.removeProperty("--footer-height"); + }, + }, + ); + return FooterView; +});
diff --git a/docs/docs/src_js_views_GroupListView.js.html b/docs/docs/src_js_views_GroupListView.js.html index c5ae8338d..2ac8e5cd6 100644 --- a/docs/docs/src_js_views_GroupListView.js.html +++ b/docs/docs/src_js_views_GroupListView.js.html @@ -44,592 +44,710 @@

Source: src/js/views/GroupListView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'collections/UserGroup', 'models/UserModel', 'views/PagerView'],
-	function($, _, Backbone, UserGroup, UserModel, PagerView) {
-	'use strict';
-
-	/**
-	 * @class GroupListView
-	 * @classdesc Displays a list of UserModels of a UserGroup collection and allows owners to add/remove members from the group
-     * @classcategory Views
-     * @screenshot views/GroupListView.png
-	 * @extends Backbone.View
-	 */
-	var GroupListView = Backbone.View.extend(
-    /** @lends GroupListView.prototype */{
-
-		type: "GroupListView",
-
-		tagName: "ul",
-
-		className: "list-group member-list",
-
-		memberEls: [],
-
-		/*
-		 * New instances of this view should pass a UserGroup collection in the options object
-		 */
-		initialize: function(options){
-			if(typeof options == "undefined")
-				var options = {};
-
-			this.collection    = options.collection || new UserGroup();
-			this.groupId       = this.collection.groupId || null;
-			this.collapsable   = (typeof options.collapsable == "undefined")? true : options.collapsable;
-			this.showGroupName = (typeof options.showGroupName == "undefined")? true : options.showGroupName;
-			this.maxItems      = options.maxItems || 2;
-		},
-
-		events: {
-			"click .toggle"               : "toggleMemberList",
-			"click .add-member .submit"   : "addToCollection",
-			"click .remove-member"		  : "removeFromCollection",
-			"click .add-owner"            : "addOwnerToCollection",
-			"click .remove-owner"         : "removeOwnership",
-			"keypress input"              : "checkForReturn"
-		},
-
-		/*
-		 * The overall list layout is created, with a header and list of members.
-		 * This view listens to the UserGroup collection and updates the member list as
-		 * members are added and removed.
-		 */
-		render: function(){
-
-			var group = this.collection,
-				view  = this;
-
-			//Empty first
-			this.$el.empty();
-
-			//Create the header/first-level of the list
-			var listItem   = $(document.createElement("li")).addClass("list-group-header list-group-item group"),
-				//icon       = $(document.createElement("i")).addClass("icon icon-caret-down tooltip-this group"),
-				numMembers = $(document.createElement("span")).addClass("num-members").text(group.length),
-				numMembersLabel = $(document.createElement("span")).text(" members"),
-				numMembersContainer = $(document.createElement("span")).append(numMembers, numMembersLabel);
-
-			if(this.showGroupName){
-				if(!this.collection.pending){
-					var link       = $(document.createElement("a")).attr("href", MetacatUI.root + "/profile/" + group.groupId).attr("data-subject", group.groupId),
-						groupName  = $(document.createElement("span")).text(group.name);
-				}
-				else{
-					var link       = $(document.createElement("a")).attr("href", "#"),
-						groupName  = $(document.createElement("span")).text("");
-				}
-
-				//Put it all together
-				$(listItem).append($(link).prepend(/*icon, */groupName));
-				numMembersContainer.prepend(" (").append(")");
-			}
-
-			//Add the member count
-			this.$el.append(listItem.append(numMembersContainer));
-
-			//Save some elements for later use
-			this.$header = $(listItem);
-			this.$numMembers = $(numMembers);
-			this.$groupName  = $(groupName);
-
-			//Create a list of member names
-			var view = this;
-			group.forEach(function(member){
-				view.addMember(member);
-			});
-
-			//Create a pager for this list if there are many group members
-			if(group.length > 4){
-				this.pager = new PagerView({
-					pages: this.$(".member"),
-					itemsPerPage: 4,
-					classes: "list-group-item"
-				});
-				this.$el.append(this.pager.render().el);
-			}
-
-			//Add some group controls for the owners
-			if(group.isOwner(MetacatUI.appUserModel))
-				this.addControls();
-
-			this.listenTo(group, "add", this.addMember);
-			this.listenTo(group, "remove", this.removeMember);
-			this.listenTo(group, "change:isOwnerOf", this.addControls);
-
-			return this;
-		},
-
-		//-------- Adding members to the group --------//
-		/*
-		 * The specified UserModel is added to the UI, and if the current user is an owner of the group,
-		 * the owner controls are displayed
-		 */
-		addMember: function(member){
-			var username = member.get("username"),
-				name     = member.get("fullName") || member.get("usernameReadable") || member.get("username");
-
-			//If this is the currently-logged-in user, display "Me"
-			if(username == MetacatUI.appUserModel.get("username"))
-				name = name + " (Me)";
-
-			//Create a list item for this member
-			var memberListItem = $(document.createElement("li")).addClass("list-group-item member").attr("data-username", username),
-				memberNameContainer = $(document.createElement("div")).addClass("name-container"),
-				memberIcon     = $(document.createElement("i")).addClass("icon icon-user icon-on-right"),
-				memberLink     = $(document.createElement("a")).attr("href", MetacatUI.root + "/profile/" + username).attr("data-username", username).prepend(memberIcon, name),
-				memberName     = $(document.createElement("span")).addClass("details ellipsis").attr("data-username", username).text(member.get("usernameReadable"));
-
-			memberIcon.tooltip({
-				placement: "top",
-				trigger: "hover",
-				title: "Group member"
-			});
-
-			//Put all the elements together
-			var memberEl = $(memberListItem).append($(memberNameContainer).append(memberLink, memberName));
-
-			//Store this element in the view
-			this.memberEls[member.cid] = memberEl;
-
-			//Append after the last member listed
-			if(this.$(".member").length)
-				this.$(".member").last().after(memberEl);
-			//If no members are listed yet, append to the main el
-			else
-				this.$el.append(memberEl);
-
-			//Add an owner icon for owners of the group or to assign owners to the group
-			if(this.collection.isOwner(member) || this.collection.isOwner(MetacatUI.appUserModel)){
-				var ownerIcon = this.getOwnerEl(member);
-				memberLink.before(ownerIcon);
-			}
-
-			//If the current user is an owner of this group, then display a 'remove member' button - but not for themselves
-			if(this.collection.isOwner(MetacatUI.appUserModel) && (username.toLowerCase() != MetacatUI.appUserModel.get("username").toLowerCase())){
-				//Add a remove icon for each member
-				var removeIcon = $(document.createElement("i")).addClass("icon icon-remove icon-negative remove-member"),
-					clearfix   = $(document.createElement("div")).addClass('clear'),
-				    memberControls = $(document.createElement("div")).addClass("member-controls").append(removeIcon);
-				removeIcon.tooltip({
-					trigger   : "hover",
-					placement : "top",
-					title     : "Remove this person from the group"
-				});
-				memberNameContainer.addClass("has-member-controls").after(memberControls, clearfix);
-			}
-
-			//Update the header
-			this.updateHeader();
-
-			//Collapse members of this group is necessary
-			if(this.$el.is(".collapsed"))
-				this.collapseMember(memberEl);
-
-			if(this.pager)
-				this.pager.update(this.$(".member"));
-		},
-
-		/*
-		 * When the user inputs a username, a UserModel is created and added to the collection.
-		 * The collection is saved to the server. Failed and successful member additions are
-		 * handled and displayed to the user
-		 */
-		addToCollection: function(e){
-			if(e) e.preventDefault();
-
-			//Get form values
-			var username = this.$addMember.find("input[name='username']").val().trim();
-			var fullName = this.$addMember.find("input[name='fullName']").val().trim();
-
-			//Reset the form
-			this.$addMember.find("input[name='username']").val("");
-			this.$addMember.find("input[name='fullName']").val("");
-
-			if(!username){
-				this.addMemberNotification({
-					msg: "You must enter a person's username. Try searching by name or email address.",
-					status: "error"
-				});
-				return;
-			}
-
-			//Is this user already in the collection?
-			if(this.collection.findWhere({username: username})){
-				this.addMemberNotification({
-					msg: fullName + " is already in this group",
-					status: "error"
-				});
-				return;
-			}
-
-			//Don't auto-collapse the list since the user is interacting with the controls right now
-			this.preventToggle = true;
-
-			//Create User Model
-			var user = new UserModel({
-				username: username,
-				fullName: fullName
-			});
-
-			//Add this user to the collection
-			this.collection.add(user);
-
-			//If this is a pending group (in the middle of creation), then don't save it to the server
-			if(this.collection.pending) return;
-
-			//Save this user in the group
-			var view = this;
-			var success = function(response){
-				view.addMemberNotification({
-					msg: fullName + " added",
-					status: "success"
-				});
-			}
-			var error = function(response){
-				if(!fullName) fullName = "that person";
-				view.addMemberNotification({
-					msg: "Something went wrong and " + fullName + " could not be added. " +
-							"Hint: That user may not exist.",
-					status: "error"
-				});
-
-				//Remove this user from the collection and other storage
-				view.memberEls[user.cid] = null;
-				view.collection.remove(user);
-			}
-
-			//Save
-			this.collection.save(success, error);
-		},
-
-		//-------- Removing members from the group ------//
-		/*
-		 * When the user clicks on the remove icon, the member is removed from the collection
-		 * and the updated collection is saved to the server
-		 */
-		removeFromCollection: function(e){
-			e.preventDefault();
-
-			var username = $(e.target).parents(".member").attr("data-username");
-			if(!username) return;
-
-			if(username.toLowerCase() == MetacatUI.appUserModel.get("username").toLowerCase()){
-				this.addMemberNotification({
-					status: "error",
-					msg: "You can't remove yourself from a group."
-				});
-				return;
-			}
-			else if(this.collection.length == 1){
-				this.addMemberNotification({
-					status: "error",
-					msg: "You must have at least one member in a group."
-				});
-				return;
-			}
-
-			//Remove the member from the collection
-			var member = this.collection.findWhere({username: username});
-			this.collection.remove(member);
-
-			//Update the header
-			this.updateHeader();
-
-			//Only save the group to the server if its not a pending group
-			if(!this.collection.pending)
-				this.collection.save();
-		},
-
-		/*
-		 * Removes the specified member from the UI
-		 */
-		removeMember: function(member){
-			//Get DOM element for this user
-			var memberEl = this.memberEls[member.cid];
-			if((typeof memberEl === "undefined") || !memberEl)
-				memberEl = this.$("li[data-username='" + member.get("username") + "']");
-
-			//Remove from page
-			memberEl.remove();
-
-			//Remove this member el from the view storage
-			this.memberEls[member.cid] = null;
-
-			if(this.pager)
-				this.pager.update(this.$(".member"));
-		},
-
-
-		//-------------- Displaying UI elements for owners --------------//
-		/*
-		 * When a user clicks on the add-owner element, this view will add the user as an owner of the
-		 * group and will update the collection. The collection is saved to the server.
-		 */
-		addOwnerToCollection: function(e){
-			if(!e) return;
-			e.preventDefault();
-
-			var view = this;
-
-			//Get this member
-			var username = $(e.target).parents(".member").attr("data-username");
-			if(!username) return;
-			var member = this.collection.findWhere({username: username});
-
-			//Update ownership
-			member.get("isOwnerOf").push(this.collection.groupId);
-			member.trigger("change:isOwnerOf");
-
-			//Save
-			var success = function(){ view.refreshOwner(member); }
-			this.collection.save(success);
-		},
-
-		/*
-		 * When the user clicks on the remove ownership icon for an owner, the rightsHolder is removed
-		 * from the group and the updated group is saved to the server.
-		 */
-		removeOwnership: function(e){
-			if(!e) return;
-			e.preventDefault();
-
-			var view = this;
-
-			//Get this member
-			var username = $(e.target).parents(".member").attr("data-username");
-			if(!username) return;
-			var member = this.collection.findWhere({username: username});
-
-			//Make sure we have at least one owner in this group left
-			var	newOwners = _.without( this.collection.getOwners(), member);
-			if(newOwners.length < 1){
-				MetacatUI.appView.showAlert("Groups need to have at least one owner.", "aler-error", this.$el, true);
-				return;
-			}
-
-			//Update the model
-			var newOwnership = _.without(member.get("isOwnerOf"), view.collection.groupId);
-			member.set("isOwnerOf", newOwnership);
-			member.trigger("change:isOwnerOf");
-
-			//Save
-			var success = function(){ view.refreshOwner(member); }
-			this.collection.save(success);
-		},
-
-		refreshOwner: function(user){
-			//Get the member element on the page
-			var memberEl = this.memberEls[user.cid];
-			if((typeof memberEl === "undefined") || !memberEl)
-				memberEl = this.$(".member[data-username='" + user.get("username") + "'");
-
-			//Replace the owner element with the new one
-			$(memberEl).find(".owner").tooltip("destroy").replaceWith(this.getOwnerEl(user));
-		},
-
-		getOwnerEl: function(member){
-			var ownerIcon = $(document.createElement("i")).addClass("icon owner pointer");
-
-			if(this.collection.isOwner(member)){
-				ownerIcon.addClass("icon-star is-owner remove-owner").tooltip({
-					placement: "top",
-					trigger: "hover",
-					title: "Group owner",
-          delay: {
-            show: 500
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/UserGroup",
+  "models/UserModel",
+  "views/PagerView",
+], function ($, _, Backbone, UserGroup, UserModel, PagerView) {
+  "use strict";
+
+  /**
+   * @class GroupListView
+   * @classdesc Displays a list of UserModels of a UserGroup collection and allows owners to add/remove members from the group
+   * @classcategory Views
+   * @screenshot views/GroupListView.png
+   * @extends Backbone.View
+   */
+  var GroupListView = Backbone.View.extend(
+    /** @lends GroupListView.prototype */ {
+      type: "GroupListView",
+
+      tagName: "ul",
+
+      className: "list-group member-list",
+
+      memberEls: [],
+
+      /*
+       * New instances of this view should pass a UserGroup collection in the options object
+       */
+      initialize: function (options) {
+        if (typeof options == "undefined") var options = {};
+
+        this.collection = options.collection || new UserGroup();
+        this.groupId = this.collection.groupId || null;
+        this.collapsable =
+          typeof options.collapsable == "undefined"
+            ? true
+            : options.collapsable;
+        this.showGroupName =
+          typeof options.showGroupName == "undefined"
+            ? true
+            : options.showGroupName;
+        this.maxItems = options.maxItems || 2;
+      },
+
+      events: {
+        "click .toggle": "toggleMemberList",
+        "click .add-member .submit": "addToCollection",
+        "click .remove-member": "removeFromCollection",
+        "click .add-owner": "addOwnerToCollection",
+        "click .remove-owner": "removeOwnership",
+        "keypress input": "checkForReturn",
+      },
+
+      /*
+       * The overall list layout is created, with a header and list of members.
+       * This view listens to the UserGroup collection and updates the member list as
+       * members are added and removed.
+       */
+      render: function () {
+        var group = this.collection,
+          view = this;
+
+        //Empty first
+        this.$el.empty();
+
+        //Create the header/first-level of the list
+        var listItem = $(document.createElement("li")).addClass(
+            "list-group-header list-group-item group",
+          ),
+          //icon       = $(document.createElement("i")).addClass("icon icon-caret-down tooltip-this group"),
+          numMembers = $(document.createElement("span"))
+            .addClass("num-members")
+            .text(group.length),
+          numMembersLabel = $(document.createElement("span")).text(" members"),
+          numMembersContainer = $(document.createElement("span")).append(
+            numMembers,
+            numMembersLabel,
+          );
+
+        if (this.showGroupName) {
+          if (!this.collection.pending) {
+            var link = $(document.createElement("a"))
+                .attr("href", MetacatUI.root + "/profile/" + group.groupId)
+                .attr("data-subject", group.groupId),
+              groupName = $(document.createElement("span")).text(group.name);
+          } else {
+            var link = $(document.createElement("a")).attr("href", "#"),
+              groupName = $(document.createElement("span")).text("");
           }
-				});
-			}
-			else{
-				ownerIcon.addClass("icon-star-empty add-owner").tooltip({
-					placement: "top",
-					trigger: "hover",
-					title: "Add this person as a co-owner of the group",
-          delay: {
-            show: 500
-          }
-				});
-			}
-
-			return ownerIcon;
-		},
-
-		addControls: function(){
-			if(!MetacatUI.appUserModel.get("loggedIn") || (!this.collection.isOwner(MetacatUI.appUserModel)) || this.$addMember) return;
-
-			//Add a form for adding a new member
-			var addMemberInput    = $(document.createElement("input"))
-									.attr("type", "text")
-									.attr("name", "username")
-	   							    .attr("placeholder", "Username or Name (cn=me, o=my org...)")
-	   							    .attr("data-submit-callback", "addToCollection")
-									.addClass("input-xlarge account-autocomplete submit-enter"),
-				addMemberName     = $(document.createElement("input"))
-									.attr("type", "hidden")
-									.attr("name", "fullName")
-									.attr("disabled", "disabled"),
-				addMemberIcon     = $(document.createElement("i")).addClass("icon icon-plus"),
-				addMemberSubmit   = $(document.createElement("button")).addClass("btn submit").append(addMemberIcon),
-				addMemberLabel    = $(document.createElement("label")).text("Add Member - Search by username, email, or name OR enter a full username below."),
-				addMemberMsg      = $(document.createElement("div")).addClass("notification")
-									.append($(document.createElement("i")).addClass("icon"),
-											$(document.createElement("span")).addClass("msg")),
-				addMemberForm     = $(document.createElement("form")).append(addMemberLabel, addMemberInput, addMemberName, addMemberSubmit, addMemberMsg),
-				addMemberListItem = $(document.createElement("li")).addClass("list-group-item add-member input-append").append(addMemberForm);
-
-			this.$addMember = $(addMemberForm);
-			this.$addMemberMsg = $(addMemberMsg);
-
-			this.$el.append(addMemberListItem);
-
-			this.setUpAutocomplete();
-
-		},
-
-		/*
-		 * Display a notification in the "add member" form space
-		 * Pass an options object with a msg (message string) and status ('success' or 'error')
-		 */
-		addMemberNotification: function(options){
-			if(!options.status) options.status = "success";
-			if(!options.msg) return;
-
-			if(options.status == "success"){
-				this.$addMemberMsg.addClass("success").removeClass("error")
-								  .children(".icon").addClass("icon-ok").removeClass("icon-remove");
-				this.$addMemberMsg.children(".msg").text(options.msg);
-			}
-			else{
-				this.$addMemberMsg.removeClass("success").addClass("error")
-				  .children(".icon").removeClass("icon-ok").addClass("icon-remove");
-				this.$addMemberMsg.children(".msg").text(options.msg);
-			}
-
-			this.$addMemberMsg.show().delay(3000).fadeOut();
-		},
-
-		/*
-		 * Update the header of this group list, which includes the number of members and the group name
-		 */
-		updateHeader: function(){
-			if(this.$numMembers)
-				this.$numMembers.text(this.collection.length);
-			if(this.$groupName)
-				this.$groupName.text(this.collection.name);
-		},
-
-		//----------- Form utilities -------------//
-		setUpAutocomplete: function() {
-			var input = this.$(".account-autocomplete");
-			if(!input || !input.length) return;
-
-			var view = this;
-
-			// look up registered identities
-			$(input).hoverAutocomplete({
-				source: function (request, response) {
-		            var term = $.ui.autocomplete.escapeRegex(request.term);
-
-		            var list = [];
-
-		            //Ids/Usernames that we want to ignore in the autocompelte
-		            var ignoreIds = view.collection.pluck("username");
-		            _.each(ignoreIds, function(id){
-		            	ignoreIds.push(id.toLowerCase());
-		            });
-		            ignoreIds.push(MetacatUI.appUserModel.get("username").toLowerCase());
-
-		            var url = MetacatUI.appModel.get("accountsUrl") + "?query=" + encodeURIComponent(term);
-					var requestSettings = {
-							url: url,
-							type: "GET",
-							success: function(data, textStatus, xhr) {
-
-								_.each($(data).find("person"), function(person, i){
-									var item = {};
-									item.value = $(person).find("subject").text();
-
-									//Ignore certain values
-									if(_.contains(ignoreIds, item.value.toLowerCase())) return;
-
-									item.fullName = $(person).find("fullName").text() || ($(person).find("givenName").text() + " " + $(person).find("familyName").text());
-									item.label = item.fullName;
-									//item.label = "<h3>"+item.fullName+"</h3>Google!";
-
-									list.push(item);
-								});
-
-					            response(list);
-							}
-					}
-					$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-
-					//Send an ORCID search when the search string gets long enough
-					if(request.term.length > 3)
-						MetacatUI.appLookupModel.orcidSearch(request, response, false, ignoreIds);
-		        },
-				select: function(e, ui) {
-					e.preventDefault();
-
-					// set the text field
-					$(e.target).val(ui.item.value);
-					$(e.target).parents("form").find("input[name='fullName']").val(ui.item.fullName);
-				},
-				position: {
-					my: "left top",
-					at: "left bottom",
-					collision: "none"
-				}
-			});
-
-		},
-
-		checkForReturn: function(e){
-			if(e.keyCode != 13) return;
-
-			if($.contains(e.target, this.$addMember.find("input[name='fullName']"))){
-				e.preventDefault();
-				return;
-			}
-			else if($(e.target).is(".submit-enter")){
-				e.preventDefault();
-				var callback = $(e.target).attr("data-submit-callback");
-				this[callback]();
-				return;
-			}
-		},
-
-		//---------- Collapsing/Expanding the member list --------//
-		collapseMember: function(memberEl){
-			if(this.preventToggle || !this.collapsable) return;
-
-			$(memberEl).slideUp();
-		},
-
-		collapseMemberList: function(e){
-			if((this.preventToggle && !e) || !this.collapsable) return;
-
-			this.$(".member, .add-member").slideUp().addClass("collapsed");
-			this.$(".icon.group").addClass("icon-caret-right").removeClass("icon-caret-down");
-		},
-
-		toggleMemberList: function(e){
-			if(e) e.preventDefault();
-			else if(this.preventToggle || !this.collapsable) return;
-
-			this.$(".member, .add-member").slideToggle().toggleClass("collapsed");
-			this.$(".icon.group").toggleClass("icon-caret-right icon-caret-down");
-		},
-
-		// ------- When this view is closed --------//
-		onClose: function(){
-			this.remove();
-		}
-
-	});
-
-	return GroupListView;
+
+          //Put it all together
+          $(listItem).append($(link).prepend(/*icon, */ groupName));
+          numMembersContainer.prepend(" (").append(")");
+        }
+
+        //Add the member count
+        this.$el.append(listItem.append(numMembersContainer));
+
+        //Save some elements for later use
+        this.$header = $(listItem);
+        this.$numMembers = $(numMembers);
+        this.$groupName = $(groupName);
+
+        //Create a list of member names
+        var view = this;
+        group.forEach(function (member) {
+          view.addMember(member);
+        });
+
+        //Create a pager for this list if there are many group members
+        if (group.length > 4) {
+          this.pager = new PagerView({
+            pages: this.$(".member"),
+            itemsPerPage: 4,
+            classes: "list-group-item",
+          });
+          this.$el.append(this.pager.render().el);
+        }
+
+        //Add some group controls for the owners
+        if (group.isOwner(MetacatUI.appUserModel)) this.addControls();
+
+        this.listenTo(group, "add", this.addMember);
+        this.listenTo(group, "remove", this.removeMember);
+        this.listenTo(group, "change:isOwnerOf", this.addControls);
+
+        return this;
+      },
+
+      //-------- Adding members to the group --------//
+      /*
+       * The specified UserModel is added to the UI, and if the current user is an owner of the group,
+       * the owner controls are displayed
+       */
+      addMember: function (member) {
+        var username = member.get("username"),
+          name =
+            member.get("fullName") ||
+            member.get("usernameReadable") ||
+            member.get("username");
+
+        //If this is the currently-logged-in user, display "Me"
+        if (username == MetacatUI.appUserModel.get("username"))
+          name = name + " (Me)";
+
+        //Create a list item for this member
+        var memberListItem = $(document.createElement("li"))
+            .addClass("list-group-item member")
+            .attr("data-username", username),
+          memberNameContainer = $(document.createElement("div")).addClass(
+            "name-container",
+          ),
+          memberIcon = $(document.createElement("i")).addClass(
+            "icon icon-user icon-on-right",
+          ),
+          memberLink = $(document.createElement("a"))
+            .attr("href", MetacatUI.root + "/profile/" + username)
+            .attr("data-username", username)
+            .prepend(memberIcon, name),
+          memberName = $(document.createElement("span"))
+            .addClass("details ellipsis")
+            .attr("data-username", username)
+            .text(member.get("usernameReadable"));
+
+        memberIcon.tooltip({
+          placement: "top",
+          trigger: "hover",
+          title: "Group member",
+        });
+
+        //Put all the elements together
+        var memberEl = $(memberListItem).append(
+          $(memberNameContainer).append(memberLink, memberName),
+        );
+
+        //Store this element in the view
+        this.memberEls[member.cid] = memberEl;
+
+        //Append after the last member listed
+        if (this.$(".member").length) this.$(".member").last().after(memberEl);
+        //If no members are listed yet, append to the main el
+        else this.$el.append(memberEl);
+
+        //Add an owner icon for owners of the group or to assign owners to the group
+        if (
+          this.collection.isOwner(member) ||
+          this.collection.isOwner(MetacatUI.appUserModel)
+        ) {
+          var ownerIcon = this.getOwnerEl(member);
+          memberLink.before(ownerIcon);
+        }
+
+        //If the current user is an owner of this group, then display a 'remove member' button - but not for themselves
+        if (
+          this.collection.isOwner(MetacatUI.appUserModel) &&
+          username.toLowerCase() !=
+            MetacatUI.appUserModel.get("username").toLowerCase()
+        ) {
+          //Add a remove icon for each member
+          var removeIcon = $(document.createElement("i")).addClass(
+              "icon icon-remove icon-negative remove-member",
+            ),
+            clearfix = $(document.createElement("div")).addClass("clear"),
+            memberControls = $(document.createElement("div"))
+              .addClass("member-controls")
+              .append(removeIcon);
+          removeIcon.tooltip({
+            trigger: "hover",
+            placement: "top",
+            title: "Remove this person from the group",
+          });
+          memberNameContainer
+            .addClass("has-member-controls")
+            .after(memberControls, clearfix);
+        }
+
+        //Update the header
+        this.updateHeader();
+
+        //Collapse members of this group is necessary
+        if (this.$el.is(".collapsed")) this.collapseMember(memberEl);
+
+        if (this.pager) this.pager.update(this.$(".member"));
+      },
+
+      /*
+       * When the user inputs a username, a UserModel is created and added to the collection.
+       * The collection is saved to the server. Failed and successful member additions are
+       * handled and displayed to the user
+       */
+      addToCollection: function (e) {
+        if (e) e.preventDefault();
+
+        //Get form values
+        var username = this.$addMember
+          .find("input[name='username']")
+          .val()
+          .trim();
+        var fullName = this.$addMember
+          .find("input[name='fullName']")
+          .val()
+          .trim();
+
+        //Reset the form
+        this.$addMember.find("input[name='username']").val("");
+        this.$addMember.find("input[name='fullName']").val("");
+
+        if (!username) {
+          this.addMemberNotification({
+            msg: "You must enter a person's username. Try searching by name or email address.",
+            status: "error",
+          });
+          return;
+        }
+
+        //Is this user already in the collection?
+        if (this.collection.findWhere({ username: username })) {
+          this.addMemberNotification({
+            msg: fullName + " is already in this group",
+            status: "error",
+          });
+          return;
+        }
+
+        //Don't auto-collapse the list since the user is interacting with the controls right now
+        this.preventToggle = true;
+
+        //Create User Model
+        var user = new UserModel({
+          username: username,
+          fullName: fullName,
+        });
+
+        //Add this user to the collection
+        this.collection.add(user);
+
+        //If this is a pending group (in the middle of creation), then don't save it to the server
+        if (this.collection.pending) return;
+
+        //Save this user in the group
+        var view = this;
+        var success = function (response) {
+          view.addMemberNotification({
+            msg: fullName + " added",
+            status: "success",
+          });
+        };
+        var error = function (response) {
+          if (!fullName) fullName = "that person";
+          view.addMemberNotification({
+            msg:
+              "Something went wrong and " +
+              fullName +
+              " could not be added. " +
+              "Hint: That user may not exist.",
+            status: "error",
+          });
+
+          //Remove this user from the collection and other storage
+          view.memberEls[user.cid] = null;
+          view.collection.remove(user);
+        };
+
+        //Save
+        this.collection.save(success, error);
+      },
+
+      //-------- Removing members from the group ------//
+      /*
+       * When the user clicks on the remove icon, the member is removed from the collection
+       * and the updated collection is saved to the server
+       */
+      removeFromCollection: function (e) {
+        e.preventDefault();
+
+        var username = $(e.target).parents(".member").attr("data-username");
+        if (!username) return;
+
+        if (
+          username.toLowerCase() ==
+          MetacatUI.appUserModel.get("username").toLowerCase()
+        ) {
+          this.addMemberNotification({
+            status: "error",
+            msg: "You can't remove yourself from a group.",
+          });
+          return;
+        } else if (this.collection.length == 1) {
+          this.addMemberNotification({
+            status: "error",
+            msg: "You must have at least one member in a group.",
+          });
+          return;
+        }
+
+        //Remove the member from the collection
+        var member = this.collection.findWhere({ username: username });
+        this.collection.remove(member);
+
+        //Update the header
+        this.updateHeader();
+
+        //Only save the group to the server if its not a pending group
+        if (!this.collection.pending) this.collection.save();
+      },
+
+      /*
+       * Removes the specified member from the UI
+       */
+      removeMember: function (member) {
+        //Get DOM element for this user
+        var memberEl = this.memberEls[member.cid];
+        if (typeof memberEl === "undefined" || !memberEl)
+          memberEl = this.$(
+            "li[data-username='" + member.get("username") + "']",
+          );
+
+        //Remove from page
+        memberEl.remove();
+
+        //Remove this member el from the view storage
+        this.memberEls[member.cid] = null;
+
+        if (this.pager) this.pager.update(this.$(".member"));
+      },
+
+      //-------------- Displaying UI elements for owners --------------//
+      /*
+       * When a user clicks on the add-owner element, this view will add the user as an owner of the
+       * group and will update the collection. The collection is saved to the server.
+       */
+      addOwnerToCollection: function (e) {
+        if (!e) return;
+        e.preventDefault();
+
+        var view = this;
+
+        //Get this member
+        var username = $(e.target).parents(".member").attr("data-username");
+        if (!username) return;
+        var member = this.collection.findWhere({ username: username });
+
+        //Update ownership
+        member.get("isOwnerOf").push(this.collection.groupId);
+        member.trigger("change:isOwnerOf");
+
+        //Save
+        var success = function () {
+          view.refreshOwner(member);
+        };
+        this.collection.save(success);
+      },
+
+      /*
+       * When the user clicks on the remove ownership icon for an owner, the rightsHolder is removed
+       * from the group and the updated group is saved to the server.
+       */
+      removeOwnership: function (e) {
+        if (!e) return;
+        e.preventDefault();
+
+        var view = this;
+
+        //Get this member
+        var username = $(e.target).parents(".member").attr("data-username");
+        if (!username) return;
+        var member = this.collection.findWhere({ username: username });
+
+        //Make sure we have at least one owner in this group left
+        var newOwners = _.without(this.collection.getOwners(), member);
+        if (newOwners.length < 1) {
+          MetacatUI.appView.showAlert(
+            "Groups need to have at least one owner.",
+            "aler-error",
+            this.$el,
+            true,
+          );
+          return;
+        }
+
+        //Update the model
+        var newOwnership = _.without(
+          member.get("isOwnerOf"),
+          view.collection.groupId,
+        );
+        member.set("isOwnerOf", newOwnership);
+        member.trigger("change:isOwnerOf");
+
+        //Save
+        var success = function () {
+          view.refreshOwner(member);
+        };
+        this.collection.save(success);
+      },
+
+      refreshOwner: function (user) {
+        //Get the member element on the page
+        var memberEl = this.memberEls[user.cid];
+        if (typeof memberEl === "undefined" || !memberEl)
+          memberEl = this.$(
+            ".member[data-username='" + user.get("username") + "'",
+          );
+
+        //Replace the owner element with the new one
+        $(memberEl)
+          .find(".owner")
+          .tooltip("destroy")
+          .replaceWith(this.getOwnerEl(user));
+      },
+
+      getOwnerEl: function (member) {
+        var ownerIcon = $(document.createElement("i")).addClass(
+          "icon owner pointer",
+        );
+
+        if (this.collection.isOwner(member)) {
+          ownerIcon.addClass("icon-star is-owner remove-owner").tooltip({
+            placement: "top",
+            trigger: "hover",
+            title: "Group owner",
+            delay: {
+              show: 500,
+            },
+          });
+        } else {
+          ownerIcon.addClass("icon-star-empty add-owner").tooltip({
+            placement: "top",
+            trigger: "hover",
+            title: "Add this person as a co-owner of the group",
+            delay: {
+              show: 500,
+            },
+          });
+        }
+
+        return ownerIcon;
+      },
+
+      addControls: function () {
+        if (
+          !MetacatUI.appUserModel.get("loggedIn") ||
+          !this.collection.isOwner(MetacatUI.appUserModel) ||
+          this.$addMember
+        )
+          return;
+
+        //Add a form for adding a new member
+        var addMemberInput = $(document.createElement("input"))
+            .attr("type", "text")
+            .attr("name", "username")
+            .attr("placeholder", "Username or Name (cn=me, o=my org...)")
+            .attr("data-submit-callback", "addToCollection")
+            .addClass("input-xlarge account-autocomplete submit-enter"),
+          addMemberName = $(document.createElement("input"))
+            .attr("type", "hidden")
+            .attr("name", "fullName")
+            .attr("disabled", "disabled"),
+          addMemberIcon = $(document.createElement("i")).addClass(
+            "icon icon-plus",
+          ),
+          addMemberSubmit = $(document.createElement("button"))
+            .addClass("btn submit")
+            .append(addMemberIcon),
+          addMemberLabel = $(document.createElement("label")).text(
+            "Add Member - Search by username, email, or name OR enter a full username below.",
+          ),
+          addMemberMsg = $(document.createElement("div"))
+            .addClass("notification")
+            .append(
+              $(document.createElement("i")).addClass("icon"),
+              $(document.createElement("span")).addClass("msg"),
+            ),
+          addMemberForm = $(document.createElement("form")).append(
+            addMemberLabel,
+            addMemberInput,
+            addMemberName,
+            addMemberSubmit,
+            addMemberMsg,
+          ),
+          addMemberListItem = $(document.createElement("li"))
+            .addClass("list-group-item add-member input-append")
+            .append(addMemberForm);
+
+        this.$addMember = $(addMemberForm);
+        this.$addMemberMsg = $(addMemberMsg);
+
+        this.$el.append(addMemberListItem);
+
+        this.setUpAutocomplete();
+      },
+
+      /*
+       * Display a notification in the "add member" form space
+       * Pass an options object with a msg (message string) and status ('success' or 'error')
+       */
+      addMemberNotification: function (options) {
+        if (!options.status) options.status = "success";
+        if (!options.msg) return;
+
+        if (options.status == "success") {
+          this.$addMemberMsg
+            .addClass("success")
+            .removeClass("error")
+            .children(".icon")
+            .addClass("icon-ok")
+            .removeClass("icon-remove");
+          this.$addMemberMsg.children(".msg").text(options.msg);
+        } else {
+          this.$addMemberMsg
+            .removeClass("success")
+            .addClass("error")
+            .children(".icon")
+            .removeClass("icon-ok")
+            .addClass("icon-remove");
+          this.$addMemberMsg.children(".msg").text(options.msg);
+        }
+
+        this.$addMemberMsg.show().delay(3000).fadeOut();
+      },
+
+      /*
+       * Update the header of this group list, which includes the number of members and the group name
+       */
+      updateHeader: function () {
+        if (this.$numMembers) this.$numMembers.text(this.collection.length);
+        if (this.$groupName) this.$groupName.text(this.collection.name);
+      },
+
+      //----------- Form utilities -------------//
+      setUpAutocomplete: function () {
+        var input = this.$(".account-autocomplete");
+        if (!input || !input.length) return;
+
+        var view = this;
+
+        // look up registered identities
+        $(input).hoverAutocomplete({
+          source: function (request, response) {
+            var term = $.ui.autocomplete.escapeRegex(request.term);
+
+            var list = [];
+
+            //Ids/Usernames that we want to ignore in the autocompelte
+            var ignoreIds = view.collection.pluck("username");
+            _.each(ignoreIds, function (id) {
+              ignoreIds.push(id.toLowerCase());
+            });
+            ignoreIds.push(
+              MetacatUI.appUserModel.get("username").toLowerCase(),
+            );
+
+            var url =
+              MetacatUI.appModel.get("accountsUrl") +
+              "?query=" +
+              encodeURIComponent(term);
+            var requestSettings = {
+              url: url,
+              type: "GET",
+              success: function (data, textStatus, xhr) {
+                _.each($(data).find("person"), function (person, i) {
+                  var item = {};
+                  item.value = $(person).find("subject").text();
+
+                  //Ignore certain values
+                  if (_.contains(ignoreIds, item.value.toLowerCase())) return;
+
+                  item.fullName =
+                    $(person).find("fullName").text() ||
+                    $(person).find("givenName").text() +
+                      " " +
+                      $(person).find("familyName").text();
+                  item.label = item.fullName;
+                  //item.label = "<h3>"+item.fullName+"</h3>Google!";
+
+                  list.push(item);
+                });
+
+                response(list);
+              },
+            };
+            $.ajax(
+              _.extend(
+                requestSettings,
+                MetacatUI.appUserModel.createAjaxSettings(),
+              ),
+            );
+
+            //Send an ORCID search when the search string gets long enough
+            if (request.term.length > 3)
+              MetacatUI.appLookupModel.orcidSearch(
+                request,
+                response,
+                false,
+                ignoreIds,
+              );
+          },
+          select: function (e, ui) {
+            e.preventDefault();
+
+            // set the text field
+            $(e.target).val(ui.item.value);
+            $(e.target)
+              .parents("form")
+              .find("input[name='fullName']")
+              .val(ui.item.fullName);
+          },
+          position: {
+            my: "left top",
+            at: "left bottom",
+            collision: "none",
+          },
+        });
+      },
+
+      checkForReturn: function (e) {
+        if (e.keyCode != 13) return;
+
+        if (
+          $.contains(e.target, this.$addMember.find("input[name='fullName']"))
+        ) {
+          e.preventDefault();
+          return;
+        } else if ($(e.target).is(".submit-enter")) {
+          e.preventDefault();
+          var callback = $(e.target).attr("data-submit-callback");
+          this[callback]();
+          return;
+        }
+      },
+
+      //---------- Collapsing/Expanding the member list --------//
+      collapseMember: function (memberEl) {
+        if (this.preventToggle || !this.collapsable) return;
+
+        $(memberEl).slideUp();
+      },
+
+      collapseMemberList: function (e) {
+        if ((this.preventToggle && !e) || !this.collapsable) return;
+
+        this.$(".member, .add-member").slideUp().addClass("collapsed");
+        this.$(".icon.group")
+          .addClass("icon-caret-right")
+          .removeClass("icon-caret-down");
+      },
+
+      toggleMemberList: function (e) {
+        if (e) e.preventDefault();
+        else if (this.preventToggle || !this.collapsable) return;
+
+        this.$(".member, .add-member").slideToggle().toggleClass("collapsed");
+        this.$(".icon.group").toggleClass("icon-caret-right icon-caret-down");
+      },
+
+      // ------- When this view is closed --------//
+      onClose: function () {
+        this.remove();
+      },
+    },
+  );
+
+  return GroupListView;
 });
 
diff --git a/docs/docs/src_js_views_ImageUploaderView.js.html b/docs/docs/src_js_views_ImageUploaderView.js.html index da6e62e1e..c52c2988f 100644 --- a/docs/docs/src_js_views_ImageUploaderView.js.html +++ b/docs/docs/src_js_views_ImageUploaderView.js.html @@ -44,580 +44,622 @@

Source: src/js/views/ImageUploaderView.js

-
define(['underscore',
-        'jquery',
-        'backbone',
-        "models/DataONEObject",
-        'collections/ObjectFormats',
-        "Dropzone",
-        "text!templates/imageUploader.html",
-        "corejs"],
-function(_, $, Backbone, DataONEObject, ObjectFormats, Dropzone, Template, corejs){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/DataONEObject",
+  "collections/ObjectFormats",
+  "Dropzone",
+  "text!templates/imageUploader.html",
+  "corejs",
+], function (
+  _,
+  $,
+  Backbone,
+  DataONEObject,
+  ObjectFormats,
+  Dropzone,
+  Template,
+  corejs,
+) {
   /**
-  * @class ImageUploaderView
-  * @classdesc A view that allows a person to upload an image to the repository
-  * @classcategory Views
-  * @extends Backbone.View
-  */
+   * @class ImageUploaderView
+   * @classdesc A view that allows a person to upload an image to the repository
+   * @classcategory Views
+   * @extends Backbone.View
+   */
   var ImageUploaderView = Backbone.View.extend(
-    /** @lends ImageUploaderView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "ImageUploader",
-
-    /**
-    * The HTML tag name to use for this view's element
-    * @type {string}
-    */
-    tagName: "div",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "image-uploader",
-
-    /**
-    * The DataONEObject or PortalImage that is being edited
-    * @type {DataONEObject|PortalImage}
-    */
-    model: undefined,
-
-    /**
-    * The URL for the image. If a DataONEObject model is provided to the view
-    * instead, the url is automatically set to the output of DataONEObject.url()
-    * @type {string}
-    */
-    url: undefined,
-
-    /**
-    * Text to instruct the user how to upload an image
-    * @type {string[]}
-    */
-    uploadInstructions: ["Drag & drop an image or click here to upload"],
-
-    /**
-    * The maximum display height of the image preview. This is only used for the
-    * css height propery, and doesn't influence the size of the saved image. If
-    * set to false, no css height property is set.
-    * @type {number}
-    */
-    height: false,
-
-    /**
-    * The display width of the image preview. This is only used for the
-    * css width propery, and doesn't influence the size of the saved image. If
-    * set to false, no css width property is set.
-    * @type {number}
-    */
-    width: false,
-
-    /**
-     * The minimum required height of the image file. If set, the uploader will
-     * reject images that are shorter than this. If null, any image height is
-     * accepted.
-     * @type {number}
-     */
-    minHeight: null,
-
-    /**
-     * The minimum required height of the image file. If set, the uploader will
-     * reject images that are shorter than this. If null, any image height is
-     * accepted.
-     * @type {number}
-     */
-    minWidth: null,
-
-    /**
-     * The maximum height for uploaded files. If a file is taller than this, it
-     * will be resized without warning before being uploaded. If set to null,
-     * the image won't be resized based on height (but might be depending on
-     * maxWidth).
-     * @type {number}
-     */
-    maxHeight: null,
-
-    /**
-     * The maximum width for uploaded files. If a file is wider than this, it
-     * will be resized without warning before being uploaded. If set to null,
-     * the image won't be resized based on width (but might be depending on
-     * maxHeight).
-     * @type {number}
-     */
-    maxWidth: null,
-
-    /**
-     * The HTML tag name to insert the uploaded image into. Options are "img",
-     * in which case the image is inserted as an HTML <img>, or "div", in which
-     * case the image is inserted as the background of a div.
-     * @type {string}
-     */
-    imageTagName: "div",
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    */
-    template: _.template(Template),
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "mouseover .icon-remove.remove"  : "previewImageRemove",
-      "mouseout  .icon-remove.remove"  : "previewImageRemove"
-    },
-
-    /**
-    * Creates a new ImageUploaderView
-    * @param {Object} options - A literal object with options to pass to the view
-    * @property {DataONEObject}  options.model - Gets set as ImageUploaderView.model
-    * @property {string[]}  options.uploadInstructions - Gets set as ImageUploaderView.uploadInstructions
-    * @property {string}  options.url - Gets set as ImageUploaderView.url
-    * @property {string}  options.imageTagName - Gets set as ImageUploaderView.imageTagName
-    * @property {number}  options.height - Gets set as ImageUploaderView.height
-    * @property {number}  options.width - Gets set as ImageUploaderView.width
-    * @property {number}  options.minWidth - Gets set as ImageUploaderView.minWidth
-    * @property {number}  options.minHeight - Gets set as ImageUploaderView.minHeight
-    * @property {number}  options.maxWidth - Gets set as ImageUploaderView.maxWidth
-    * @property {number}  options.maxHeight - Gets set as ImageUploaderView.maxHeight
-    */
-    initialize: function(options){
-
-      try {
-        if( typeof options == "object" ){
-
-          this.model              = options.model;
-          this.uploadInstructions = options.uploadInstructions;
-          this.url                = options.url;
-          this.imageTagName       = options.imageTagName;
-          this.height             = options.height;
-          this.width              = options.width;
-          this.minHeight          = options.minHeight;
-          this.minWidth           = options.minWidth;
-          this.maxHeight          = options.maxHeight;
-          this.maxWidth           = options.maxWidth;
-
-          if( !this.model ){
-            this.model = new DataONEObject({
-              synced: true
-           });
-          }
+    /** @lends ImageUploaderView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "ImageUploader",
+
+      /**
+       * The HTML tag name to use for this view's element
+       * @type {string}
+       */
+      tagName: "div",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "image-uploader",
+
+      /**
+       * The DataONEObject or PortalImage that is being edited
+       * @type {DataONEObject|PortalImage}
+       */
+      model: undefined,
+
+      /**
+       * The URL for the image. If a DataONEObject model is provided to the view
+       * instead, the url is automatically set to the output of DataONEObject.url()
+       * @type {string}
+       */
+      url: undefined,
+
+      /**
+       * Text to instruct the user how to upload an image
+       * @type {string[]}
+       */
+      uploadInstructions: ["Drag & drop an image or click here to upload"],
+
+      /**
+       * The maximum display height of the image preview. This is only used for the
+       * css height propery, and doesn't influence the size of the saved image. If
+       * set to false, no css height property is set.
+       * @type {number}
+       */
+      height: false,
+
+      /**
+       * The display width of the image preview. This is only used for the
+       * css width propery, and doesn't influence the size of the saved image. If
+       * set to false, no css width property is set.
+       * @type {number}
+       */
+      width: false,
+
+      /**
+       * The minimum required height of the image file. If set, the uploader will
+       * reject images that are shorter than this. If null, any image height is
+       * accepted.
+       * @type {number}
+       */
+      minHeight: null,
+
+      /**
+       * The minimum required height of the image file. If set, the uploader will
+       * reject images that are shorter than this. If null, any image height is
+       * accepted.
+       * @type {number}
+       */
+      minWidth: null,
+
+      /**
+       * The maximum height for uploaded files. If a file is taller than this, it
+       * will be resized without warning before being uploaded. If set to null,
+       * the image won't be resized based on height (but might be depending on
+       * maxWidth).
+       * @type {number}
+       */
+      maxHeight: null,
+
+      /**
+       * The maximum width for uploaded files. If a file is wider than this, it
+       * will be resized without warning before being uploaded. If set to null,
+       * the image won't be resized based on width (but might be depending on
+       * maxHeight).
+       * @type {number}
+       */
+      maxWidth: null,
+
+      /**
+       * The HTML tag name to insert the uploaded image into. Options are "img",
+       * in which case the image is inserted as an HTML <img>, or "div", in which
+       * case the image is inserted as the background of a div.
+       * @type {string}
+       */
+      imageTagName: "div",
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      template: _.template(Template),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "mouseover .icon-remove.remove": "previewImageRemove",
+        "mouseout  .icon-remove.remove": "previewImageRemove",
+      },
+
+      /**
+       * Creates a new ImageUploaderView
+       * @param {Object} options - A literal object with options to pass to the view
+       * @property {DataONEObject}  options.model - Gets set as ImageUploaderView.model
+       * @property {string[]}  options.uploadInstructions - Gets set as ImageUploaderView.uploadInstructions
+       * @property {string}  options.url - Gets set as ImageUploaderView.url
+       * @property {string}  options.imageTagName - Gets set as ImageUploaderView.imageTagName
+       * @property {number}  options.height - Gets set as ImageUploaderView.height
+       * @property {number}  options.width - Gets set as ImageUploaderView.width
+       * @property {number}  options.minWidth - Gets set as ImageUploaderView.minWidth
+       * @property {number}  options.minHeight - Gets set as ImageUploaderView.minHeight
+       * @property {number}  options.maxWidth - Gets set as ImageUploaderView.maxWidth
+       * @property {number}  options.maxHeight - Gets set as ImageUploaderView.maxHeight
+       */
+      initialize: function (options) {
+        try {
+          if (typeof options == "object") {
+            this.model = options.model;
+            this.uploadInstructions = options.uploadInstructions;
+            this.url = options.url;
+            this.imageTagName = options.imageTagName;
+            this.height = options.height;
+            this.width = options.width;
+            this.minHeight = options.minHeight;
+            this.minWidth = options.minWidth;
+            this.maxHeight = options.maxHeight;
+            this.maxWidth = options.maxWidth;
+
+            if (!this.model) {
+              this.model = new DataONEObject({
+                synced: true,
+              });
+            }
 
-          if (!this.url && this.model) {
-            this.url = this.model.url();
+            if (!this.url && this.model) {
+              this.url = this.model.url();
+            }
           }
 
-        }
-
-        // Ensure the object formats are cached for uploader's use
-        if ( typeof MetacatUI.objectFormats === "undefined" ) {
+          // Ensure the object formats are cached for uploader's use
+          if (typeof MetacatUI.objectFormats === "undefined") {
             MetacatUI.objectFormats = new ObjectFormats();
             MetacatUI.objectFormats.fetch();
-        }
-
-        // Bug fix: Overwrite a dropzone function that causes a bug in Edge 16 &
-        // 17 browser. If we update our dropzone with a fallback, this function
-        // should return the fallback element.
-        Dropzone.prototype.getExistingFallback = function(){
-          return false
-        };
-
-        // Identify which zones should be drag & drop manually
-        Dropzone.autoDiscover = false;
-
-      } catch (e) {
-        console.log("ImageUploaderView failed to initialize. Error message: " + e);
-      }
-
-
-    },
+          }
 
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-      try{
-        // Reference to the view
-        var view = this,
-        // The overall template which holds two sub-templates
-        fullTemplate = view.template({
-          height: this.height,
-          width: this.width,
-          uploadInstructions: this.uploadInstructions,
-          imageTagName: this.imageTagName
-        }),
-        // The outer template
-        dropzoneTemplate = $(fullTemplate).find(".dropzone")[0].outerHTML,
-        // The inner template inserted when an image is added
-        previewTemplate = $(fullTemplate)
-                              .find(".dz-preview")[0]
-                              .outerHTML;
-
-        // Insert the main template for this view
-        view.$el.html(dropzoneTemplate);
-
-        console.log(view.model.get("imageURL"), view.model.url())
-
-        // Add upload & drag and drop functionality to the dropzone div.
-        // For config details, see: https://www.dropzonejs.com/#configuration
-        var $dropZone = view.$(".dropzone").dropzone({
-
-          url: view.model.get("imageURL") || view.model.url(),
-          acceptedFiles: "image/*",
-          addRemoveLinks: false,
-          maxFiles: 1,
-          parallelUploads: 1,
-          uploadMultiple: false,
-          resizeHeight: view.maxHeight,
-          resizeWidth: view.maxWidth,
-          thumbnailHeight: view.maxHeight < view.height ? view.maxHeight : null,
-          thumbnailWidth: view.maxWidth < view.width ? view.maxWidth : null,
-          dictInvalidFileType: "This file type is not allowed. Please select an image file",
-          autoProcessQueue: true,
-          previewTemplate: previewTemplate,
-          withCredentials: true,
-          paramName: "object",
-          hiddenInputContainer: this.el,
-
-          headers: {
+          // Bug fix: Overwrite a dropzone function that causes a bug in Edge 16 &
+          // 17 browser. If we update our dropzone with a fallback, this function
+          // should return the fallback element.
+          Dropzone.prototype.getExistingFallback = function () {
+            return false;
+          };
+
+          // Identify which zones should be drag & drop manually
+          Dropzone.autoDiscover = false;
+        } catch (e) {
+          console.log(
+            "ImageUploaderView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        try {
+          // Reference to the view
+          var view = this,
+            // The overall template which holds two sub-templates
+            fullTemplate = view.template({
+              height: this.height,
+              width: this.width,
+              uploadInstructions: this.uploadInstructions,
+              imageTagName: this.imageTagName,
+            }),
+            // The outer template
+            dropzoneTemplate = $(fullTemplate).find(".dropzone")[0].outerHTML,
+            // The inner template inserted when an image is added
+            previewTemplate = $(fullTemplate).find(".dz-preview")[0].outerHTML;
+
+          // Insert the main template for this view
+          view.$el.html(dropzoneTemplate);
+
+          console.log(view.model.get("imageURL"), view.model.url());
+
+          // Add upload & drag and drop functionality to the dropzone div.
+          // For config details, see: https://www.dropzonejs.com/#configuration
+          var $dropZone = view.$(".dropzone").dropzone({
+            url: view.model.get("imageURL") || view.model.url(),
+            acceptedFiles: "image/*",
+            addRemoveLinks: false,
+            maxFiles: 1,
+            parallelUploads: 1,
+            uploadMultiple: false,
+            resizeHeight: view.maxHeight,
+            resizeWidth: view.maxWidth,
+            thumbnailHeight:
+              view.maxHeight < view.height ? view.maxHeight : null,
+            thumbnailWidth: view.maxWidth < view.width ? view.maxWidth : null,
+            dictInvalidFileType:
+              "This file type is not allowed. Please select an image file",
+            autoProcessQueue: true,
+            previewTemplate: previewTemplate,
+            withCredentials: true,
+            paramName: "object",
+            hiddenInputContainer: this.el,
+
+            headers: {
               "Cache-Control": null,
               "X-Requested-With": null,
-              "Authorization": MetacatUI.appUserModel.createAjaxSettings().headers.Authorization
-          },
-
-          // Override dropzone's function for showing images in the upload zone
-          // so that we have the option to display them as a background images.
-          // Check for minimum dimensions at this stage because dropzone has
-          // calculated the file's height here.
-          thumbnail: function(file, dataURL){
-            try {
-              // Don't bother size check for SVG images since they're vector
-              var dimCheck = file.type === "image/svg+xml" ? true : view.checkMinDimensions(file.width, file.height);
-              if(dimCheck != true){
-                if(file.rejectDimensions){
-                  // Send reason for rejection rejectDimensions function
-                  file.rejectDimensions(dimCheck);
+              Authorization:
+                MetacatUI.appUserModel.createAjaxSettings().headers
+                  .Authorization,
+            },
+
+            // Override dropzone's function for showing images in the upload zone
+            // so that we have the option to display them as a background images.
+            // Check for minimum dimensions at this stage because dropzone has
+            // calculated the file's height here.
+            thumbnail: function (file, dataURL) {
+              try {
+                // Don't bother size check for SVG images since they're vector
+                var dimCheck =
+                  file.type === "image/svg+xml"
+                    ? true
+                    : view.checkMinDimensions(file.width, file.height);
+                if (dimCheck != true) {
+                  if (file.rejectDimensions) {
+                    // Send reason for rejection rejectDimensions function
+                    file.rejectDimensions(dimCheck);
+                  }
+                } else {
+                  if (file.acceptDimensions) {
+                    file.acceptDimensions();
+                  }
+                  view.showImage(file, dataURL);
                 }
-              } else {
-                if(file.acceptDimensions){
-                  file.acceptDimensions();
+              } catch (e) {
+                console.error(
+                  "Error generating thumbnail image, error message: " + e,
+                );
+              }
+            },
+
+            // Dropzone will check filetype = options.acceptedFiles. Add functions
+            // for when the image is too small.
+            accept: function accept(file, done) {
+              try {
+                file.rejectDimensions = function (message) {
+                  done(message);
                 };
-                view.showImage(file, dataURL);
-              };
-            } catch (e) {
-              console.error("Error generating thumbnail image, error message: " + e);
-            }
-
-          },
-
-          // Dropzone will check filetype = options.acceptedFiles. Add functions
-          // for when the image is too small.
-          accept: function accept(file, done) {
-            try {
-              file.rejectDimensions = function(message) {  done(message)  };
-              file.acceptDimensions = function(){  done()  };
-            } catch (e) {
-              console.error("Error during dropzone's accept function. Error code: " + e);
-            }
-          },
-
-
-          // After the file is accepted (correct filetype and min size requirements),
-          // resize the image if it's too large in height or width, then
-          // provide image data to a dataOne object model and calulate checksum.
-          transformFile: function(file, done){
-            try {
-              // Only resize images if dimensions are too large.
-              // Once the image is resized (or not), save the data to the model and get a checksum.
-              var resizeWidth = (file.width > this.options.resizeWidth) ? this.options.resizeWidth : null;
-              var resizeHeight = (file.height > this.options.resizeHeight) ? this.options.resizeHeight : null;
-              if (resizeHeight || resizeWidth) {
-                return this.resizeImage(file, resizeWidth, resizeHeight, this.options.resizeMethod, function(blob){
-                  view.prepareD1Model(blob, file.name, file.type, done);
+                file.acceptDimensions = function () {
+                  done();
+                };
+              } catch (e) {
+                console.error(
+                  "Error during dropzone's accept function. Error code: " + e,
+                );
+              }
+            },
+
+            // After the file is accepted (correct filetype and min size requirements),
+            // resize the image if it's too large in height or width, then
+            // provide image data to a dataOne object model and calulate checksum.
+            transformFile: function (file, done) {
+              try {
+                // Only resize images if dimensions are too large.
+                // Once the image is resized (or not), save the data to the model and get a checksum.
+                var resizeWidth =
+                  file.width > this.options.resizeWidth
+                    ? this.options.resizeWidth
+                    : null;
+                var resizeHeight =
+                  file.height > this.options.resizeHeight
+                    ? this.options.resizeHeight
+                    : null;
+                if (resizeHeight || resizeWidth) {
+                  return this.resizeImage(
+                    file,
+                    resizeWidth,
+                    resizeHeight,
+                    this.options.resizeMethod,
+                    function (blob) {
+                      view.prepareD1Model(blob, file.name, file.type, done);
+                    },
+                  );
+                } else {
+                  return view.prepareD1Model(file, file.name, file.type, done);
+                }
+              } catch (e) {
+                console.error(
+                  "Error during dropzone's transformFile function. Error code: " +
+                    e,
+                );
+              }
+            },
+
+            // Add some required formData right before the image is uploaded
+            sending: function (file, xhr, formData) {
+              try {
+                //Create the system metadata XML & send as blob
+                var sysMetaXML = view.model.serializeSysMeta();
+                var xmlBlob = new Blob([sysMetaXML], {
+                  type: "application/xml",
                 });
-              } else {
-                return view.prepareD1Model(file, file.name, file.type, done);
+                formData.append("sysmeta", xmlBlob, "sysmeta.xml");
+                formData.append("pid", view.model.get("id"));
+              } catch (e) {
+                console.error(
+                  "Error during dropzone's sending function. Error code: " + e,
+                );
               }
-            } catch (e) {
-              console.error("Error during dropzone's transformFile function. Error code: " + e);
-            }
-          },
-
-          // Add some required formData right before the image is uploaded
-          sending: function(file, xhr, formData) {
-            try {
-              //Create the system metadata XML & send as blob
-              var sysMetaXML = view.model.serializeSysMeta();
-              var xmlBlob = new Blob([sysMetaXML], {type : 'application/xml'});
-              formData.append("sysmeta", xmlBlob, "sysmeta.xml");
-              formData.append("pid", view.model.get("id"));
-            } catch (e) {
-              console.error("Error during dropzone's sending function. Error code: " + e);
-            }
-          },
-
-          // If there are any errors during the entire process...
-          error: function error(file, message, xhr) {
-            try {
-              view.trigger("error");
-              // Give a readable error if it's a server error
-              if(xhr){
-                console.error(message);
-                message = "There was an error uploading your file. Please try again later."
+            },
+
+            // If there are any errors during the entire process...
+            error: function error(file, message, xhr) {
+              try {
+                view.trigger("error");
+                // Give a readable error if it's a server error
+                if (xhr) {
+                  console.error(message);
+                  message =
+                    "There was an error uploading your file. Please try again later.";
+                }
+                // Make sure image isn't showing (src for <img> and style for background images)
+                $(file.previewElement).find(".image-container").attr({
+                  src: "",
+                  style: "",
+                });
+                // Show error using dropzone's default behaviour
+                this.defaultOptions.error(file, message);
+              } catch (e) {
+                console.error("Problem handling error, message: " + e);
               }
-              // Make sure image isn't showing (src for <img> and style for background images)
-              $(file.previewElement).find(".image-container").attr({
-                src: "",
-                style: ""
-              });
-              // Show error using dropzone's default behaviour
-              this.defaultOptions.error(file, message);
-            } catch (e) {
-              console.error("Problem handling error, message: " + e);
-            }
-          },
-
-          init: function() {
-            try {
-              this.on("addedfile", function(file){
-                // Make sure only the most recently added image is shown in the upload zone
-                view.limitFileInput();
-                // Required for parent views to use listenTo() on dropzone events
-                view.trigger("addedfile");
-              });
-              // Hide the remove buttons and text when an image is removed
-              this.on("removedfile", function(file){
-                view.previewImageRemove();
-                // Required for parent views to use listenTo() on dropzone events
-                view.trigger("removedfile");
-              });
-              this.on("success", function(){
-                view.trigger("successSaving", view.model);
-              });
-            } catch (e) {
-              console.error("Issue initializing dropzone, error message: " + e);
-            }
-          }
-
-        });
+            },
+
+            init: function () {
+              try {
+                this.on("addedfile", function (file) {
+                  // Make sure only the most recently added image is shown in the upload zone
+                  view.limitFileInput();
+                  // Required for parent views to use listenTo() on dropzone events
+                  view.trigger("addedfile");
+                });
+                // Hide the remove buttons and text when an image is removed
+                this.on("removedfile", function (file) {
+                  view.previewImageRemove();
+                  // Required for parent views to use listenTo() on dropzone events
+                  view.trigger("removedfile");
+                });
+                this.on("success", function () {
+                  view.trigger("successSaving", view.model);
+                });
+              } catch (e) {
+                console.error(
+                  "Issue initializing dropzone, error message: " + e,
+                );
+              }
+            },
+          });
 
-        // Save the dropzone element for other functions to access later
-        view.imageDropzone = $dropZone[0].dropzone;
+          // Save the dropzone element for other functions to access later
+          view.imageDropzone = $dropZone[0].dropzone;
 
-        // Fetch the image if a URL was provided and show thumbnail
-        if(view.url){
-          view.showSavedImage();
+          // Fetch the image if a URL was provided and show thumbnail
+          if (view.url) {
+            view.showSavedImage();
+          }
+        } catch (error) {
+          console.error(
+            "ImageUploaderView could not be rendered, error message: ",
+            error,
+          );
         }
-      }
-      catch(error){
-        console.error("ImageUploaderView could not be rendered, error message: ", error);
-      }
-    },
-
-    /**
-     * prepareD1Model - Called once an image file is resized or once it's
-     * determined the the image does not need to be resized. This function adds
-     * data about the image added by the user to a new DataOne model, then
-     * calculates the checksum. When the checksum is finished being calculated,
-     * calls the callback function (i.e. dropzone's done()).
-     *
-     * @param  {Blob|File} object Either the Blob or File to be saved to the server
-     * @param  {string} filename the name of the file
-     * @param  {string} filetype the filetype
-     * @param  {function} callback a function to call once the checksum is calculated.
-     */
-    prepareD1Model: function(object, filename, filetype, callback){
-
-      try{
-
-        var modelAttributes = {
-          synced: true,
-          type: "image",
-          fileName: filename,
-          mediaType: filetype,
-          size: object.size,
-          uploadFile: object
+      },
+
+      /**
+       * prepareD1Model - Called once an image file is resized or once it's
+       * determined the the image does not need to be resized. This function adds
+       * data about the image added by the user to a new DataOne model, then
+       * calculates the checksum. When the checksum is finished being calculated,
+       * calls the callback function (i.e. dropzone's done()).
+       *
+       * @param  {Blob|File} object Either the Blob or File to be saved to the server
+       * @param  {string} filename the name of the file
+       * @param  {string} filetype the filetype
+       * @param  {function} callback a function to call once the checksum is calculated.
+       */
+      prepareD1Model: function (object, filename, filetype, callback) {
+        try {
+          var modelAttributes = {
+            synced: true,
+            type: "image",
+            fileName: filename,
+            mediaType: filetype,
+            size: object.size,
+            uploadFile: object,
+          };
+
+          // Each file upload must be a new DataONE object
+          this.model = new DataONEObject(modelAttributes);
+          this.model.updateID();
+          this.model.set("obsoletes", null);
+          this.model.get("accessPolicy").makePublic();
+
+          // Start checksum, and call the callback function when it's complete
+          this.model.stopListening(this.model, "checksumCalculated");
+          this.model.listenToOnce(
+            this.model,
+            "checksumCalculated",
+            function () {
+              callback(object);
+            },
+          );
+          this.model.calculateChecksum();
+        } catch (exception) {
+          console.log(
+            "there was a problem calculating the checksum, exception: " +
+              exception,
+          );
         }
-
-        // Each file upload must be a new DataONE object
-        this.model = new DataONEObject(modelAttributes);
-        this.model.updateID();
-        this.model.set("obsoletes", null);
-        this.model.get("accessPolicy").makePublic();
-
-        // Start checksum, and call the callback function when it's complete
-        this.model.stopListening(this.model, "checksumCalculated");
-        this.model.listenToOnce(this.model, "checksumCalculated", function(){
-            callback(object);
-        });
-        this.model.calculateChecksum();
-
-      } catch (exception) {
-        console.log("there was a problem calculating the checksum, exception: " + exception);
-      }
-
-    },
-
-
-    /**
-     * limitFileInput - Ensures only the most recently added image is shown in
-     * the upload zone, as we limit each zone to 1 image but dropzone is
-     * designed to accept multiple files. Called whenever a file is added to a
-     * dropzone element.
-     */
-    limitFileInput: function(){
-      if (this.imageDropzone.files[1]!=null){
-        this.imageDropzone.removeFile(this.imageDropzone.files[0]);
-      }
-    },
-
-
-    /**
-     * checkMinDimensions - called from dropzone's thumbnail function before the
-     * image is displayed. Checks that the image meets at least the minimum
-     * height and width requirements provided to view.minHeight and
-     * view.minWidth.
-     *
-     * @param  {number} width  the image's height.
-     * @param  {number} height the image's width.
-     * @return {string|boolean}  returns true if the image is at least as wide as and as tall as the given height and width. Otherwise returns an error message to display to the user.
-     */
-    checkMinDimensions: function(width, height){
-
-      try{
-        if(width < this.minWidth && height < this.minHeight){
-          return("This image is too small. Please choose an image that's at least " + this.minWidth +"px wide and " + this.minHeight + "px tall.");
-        } else if (width < this.minWidth) {
-          return("This image is too narrow. Please choose an image that's at least " + this.minWidth +"px wide.")
-        } else if (height < this.minHeight){
-          return("This image is too short. Please choose an image that's at least " + this.minHeight +"px tall.")
-        } else {
-          // minimum height and width are met. If too large, then image will be resized.
-          return true
+      },
+
+      /**
+       * limitFileInput - Ensures only the most recently added image is shown in
+       * the upload zone, as we limit each zone to 1 image but dropzone is
+       * designed to accept multiple files. Called whenever a file is added to a
+       * dropzone element.
+       */
+      limitFileInput: function () {
+        if (this.imageDropzone.files[1] != null) {
+          this.imageDropzone.removeFile(this.imageDropzone.files[0]);
         }
-      } catch(error){
-        console.log("Error checking the min dimensions of added file. Error message:" + error);
-        // Better to show an image that's too small in this case.
-        return true
-      }
-    },
-
-    /**
-     * showImage - General function for displaying an image file in the upload zone, whether
-     * just added or already uploaded. This is the function that we use to override
-     * dropzone's thumbnail() function. It displays the image as the background of
-     * a div if this view's imageTagName attribute is set to "div", or as an image
-     * element if imageTagName is set to "img".
-     * @param  {object} file    Information about the image file
-     * @param  {string} dataURL A URL for the image to be displayed
-     */
-    showImage: function(file, dataURL){
-
-      try{
-        // Don't show files that are the wrong size or type
-        if(!this.url && !file.accepted){
-          return
-        };
-
-        var previewEl = $(file.previewElement).find(".image-container")[0];
-
-        if(this.imageTagName == "img"){
-          previewEl.src = dataURL;
-        } else if (this.imageTagName == "div"){
-          $(previewEl).css("background-image", "url(" + dataURL + ")");
+      },
+
+      /**
+       * checkMinDimensions - called from dropzone's thumbnail function before the
+       * image is displayed. Checks that the image meets at least the minimum
+       * height and width requirements provided to view.minHeight and
+       * view.minWidth.
+       *
+       * @param  {number} width  the image's height.
+       * @param  {number} height the image's width.
+       * @return {string|boolean}  returns true if the image is at least as wide as and as tall as the given height and width. Otherwise returns an error message to display to the user.
+       */
+      checkMinDimensions: function (width, height) {
+        try {
+          if (width < this.minWidth && height < this.minHeight) {
+            return (
+              "This image is too small. Please choose an image that's at least " +
+              this.minWidth +
+              "px wide and " +
+              this.minHeight +
+              "px tall."
+            );
+          } else if (width < this.minWidth) {
+            return (
+              "This image is too narrow. Please choose an image that's at least " +
+              this.minWidth +
+              "px wide."
+            );
+          } else if (height < this.minHeight) {
+            return (
+              "This image is too short. Please choose an image that's at least " +
+              this.minHeight +
+              "px tall."
+            );
+          } else {
+            // minimum height and width are met. If too large, then image will be resized.
+            return true;
+          }
+        } catch (error) {
+          console.log(
+            "Error checking the min dimensions of added file. Error message:" +
+              error,
+          );
+          // Better to show an image that's too small in this case.
+          return true;
         }
+      },
+
+      /**
+       * showImage - General function for displaying an image file in the upload zone, whether
+       * just added or already uploaded. This is the function that we use to override
+       * dropzone's thumbnail() function. It displays the image as the background of
+       * a div if this view's imageTagName attribute is set to "div", or as an image
+       * element if imageTagName is set to "img".
+       * @param  {object} file    Information about the image file
+       * @param  {string} dataURL A URL for the image to be displayed
+       */
+      showImage: function (file, dataURL) {
+        try {
+          // Don't show files that are the wrong size or type
+          if (!this.url && !file.accepted) {
+            return;
+          }
 
-      } catch(error) {
-        console.log(error);
-        this.showError($(file.previewElement));
-      }
-
-    },
-
-    /**
-    * Display an image in the upload zone that's already saved. This gets called
-    * when an image url is provided to this view.
-    */
-    showSavedImage: function(){
-
-      try{
+          var previewEl = $(file.previewElement).find(".image-container")[0];
 
-        //If there is no URL or the model hasn't been saved yet, then don't show the image
-        if( !this.url || this.model.isNew() ){
-          return;
+          if (this.imageTagName == "img") {
+            previewEl.src = dataURL;
+          } else if (this.imageTagName == "div") {
+            $(previewEl).css("background-image", "url(" + dataURL + ")");
+          }
+        } catch (error) {
+          console.log(error);
+          this.showError($(file.previewElement));
         }
+      },
+
+      /**
+       * Display an image in the upload zone that's already saved. This gets called
+       * when an image url is provided to this view.
+       */
+      showSavedImage: function () {
+        try {
+          //If there is no URL or the model hasn't been saved yet, then don't show the image
+          if (!this.url || this.model.isNew()) {
+            return;
+          }
 
-        // A mock image file to identify the image provided to this view
-        var imageFile = {
-          url: this.url
-        };
-
-        // Add it to filelist so excess images can be removed if needed
-        this.imageDropzone.files[0] = imageFile;
-        // Call the default addedfile event handler
-        this.imageDropzone.emit("addedfile", imageFile);
-        // Show the thumbnail of the file
-        this.imageDropzone.emit("thumbnail", imageFile, imageFile.url);
-        // Make sure that there is no progress bar, etc...
-        this.imageDropzone.emit("complete", imageFile);
-
-      }
-      catch(error){
-        console.log("image could not be displayed, error message: " + error);
-        // When the preview image fails to render, show some explanatory text
-        this.showError($(this.imageDropzone.element));
-
-      }
-
-    },
-
-    /**
-     * showError - Indicates to the user that the image uploader may not work
-     * due to browser issues.
-     * @param {jQuery} dropzoneEl - The dropzone element to show the error for.
-     */
-    showError: function(dropzoneEl){
-      dropzoneEl.addClass("error");
-      dropzoneEl.find(".dz-error-message span").text("Error previewing image");
-      dropzoneEl.tooltip({
-        placement: "bottom",
-        trigger: "hover",
-        title: "Image previews cannot be shown. Your browser may be out-of-date."
-      });
-    },
-
-    /**
-     * previewImageRemove - When the user hovers over the remove button,
-     * indicates to the user that the button will remove the image by 1) changing
-     * the upload instruction text to a message about removing the image,
-     * and 2) adding a warning class to the message div.
-     */
-    previewImageRemove: function(e){
-
-      try {
-
-        if(e){
-          this.$el.toggleClass("remove-preview");
-        } else {
-          this.$el.removeClass("remove-preview");
+          // A mock image file to identify the image provided to this view
+          var imageFile = {
+            url: this.url,
+          };
+
+          // Add it to filelist so excess images can be removed if needed
+          this.imageDropzone.files[0] = imageFile;
+          // Call the default addedfile event handler
+          this.imageDropzone.emit("addedfile", imageFile);
+          // Show the thumbnail of the file
+          this.imageDropzone.emit("thumbnail", imageFile, imageFile.url);
+          // Make sure that there is no progress bar, etc...
+          this.imageDropzone.emit("complete", imageFile);
+        } catch (error) {
+          console.log("image could not be displayed, error message: " + error);
+          // When the preview image fails to render, show some explanatory text
+          this.showError($(this.imageDropzone.element));
         }
-
-
-      } catch (error) {
-        console.log(error);
-      }
-    }
-
-  });
+      },
+
+      /**
+       * showError - Indicates to the user that the image uploader may not work
+       * due to browser issues.
+       * @param {jQuery} dropzoneEl - The dropzone element to show the error for.
+       */
+      showError: function (dropzoneEl) {
+        dropzoneEl.addClass("error");
+        dropzoneEl
+          .find(".dz-error-message span")
+          .text("Error previewing image");
+        dropzoneEl.tooltip({
+          placement: "bottom",
+          trigger: "hover",
+          title:
+            "Image previews cannot be shown. Your browser may be out-of-date.",
+        });
+      },
+
+      /**
+       * previewImageRemove - When the user hovers over the remove button,
+       * indicates to the user that the button will remove the image by 1) changing
+       * the upload instruction text to a message about removing the image,
+       * and 2) adding a warning class to the message div.
+       */
+      previewImageRemove: function (e) {
+        try {
+          if (e) {
+            this.$el.toggleClass("remove-preview");
+          } else {
+            this.$el.removeClass("remove-preview");
+          }
+        } catch (error) {
+          console.log(error);
+        }
+      },
+    },
+  );
 
   return ImageUploaderView;
-
 });
 
diff --git a/docs/docs/src_js_views_MarkdownEditorView.js.html b/docs/docs/src_js_views_MarkdownEditorView.js.html index e91ce23b3..6ac252709 100644 --- a/docs/docs/src_js_views_MarkdownEditorView.js.html +++ b/docs/docs/src_js_views_MarkdownEditorView.js.html @@ -44,565 +44,596 @@

Source: src/js/views/MarkdownEditorView.js

-
define(["underscore",
-        "jquery",
-        "backbone",
-        "woofmark",
-        "models/metadata/eml220/EMLText",
-        "views/ImageUploaderView",
-        "views/MarkdownView",
-        "views/TableEditorView",
-        "text!templates/markdownEditor.html"],
-function(_, $, Backbone, Woofmark, EMLText, ImageUploader, MarkdownView, TableEditor, Template){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "woofmark",
+  "models/metadata/eml220/EMLText",
+  "views/ImageUploaderView",
+  "views/MarkdownView",
+  "views/TableEditorView",
+  "text!templates/markdownEditor.html",
+], function (
+  _,
+  $,
+  Backbone,
+  Woofmark,
+  EMLText,
+  ImageUploader,
+  MarkdownView,
+  TableEditor,
+  Template,
+) {
   /**
-  * @class MarkdownEditorView
-  * @classdesc A view of an HTML textarea with markdown editor UI and preview tab
-  * @classcategory Views
-  * @extends Backbone.View
-  * @constructor
-  */
+   * @class MarkdownEditorView
+   * @classdesc A view of an HTML textarea with markdown editor UI and preview tab
+   * @classcategory Views
+   * @extends Backbone.View
+   * @constructor
+   */
   var MarkdownEditorView = Backbone.View.extend(
-    /** @lends MarkdownEditorView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    * @readonly
-    */
-    type: "MarkdownEditor",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "markdown-editor",
-
-    /**
-    * References to templates for this view. HTML files are converted to
-    * Underscore.js templates
-    * @type {Underscore.Template}
-    */
-    template: _.template(Template),
-
-    /*
-    * Markdown to insert into the textarea when the view is first rendered
-    * @type {string}
-    */
-    // markdown: "",
-
-    /**
-    * EMLText model that contains a markdown attribute to edit. The markdown is
-    * inserted into the textarea when the view is first rendered. If there's no markdown,
-    * then the view looks for markdown from the markdownExample attribute in the model.
-    * Note that if there are multiple markdown strings in the model, only the first
-    * is rendered/edited.
-    * @type {EMLText}
-    */
-    model: null,
-
-    /**
-    * The placeholder text to display in the textarea when it's empty
-    * @type {string}
-    */
-    markdownPlaceholder: "",
-
-    /**
-    * The placeholder text to display in the preview area when there's no
-    * markdown
-    * @type {string}
-    */
-    previewPlaceholder: "",
-
-    /**
-    * Indicates whether or not to render a table of contents for the markdown
-    * preview. If set to true, a table of contents will be shown in the preview
-    * if there two or more top-level headers are rendered from the markdown.
-    * @type {boolean}
-    */
-    showTOC: false,
-
-    /**
-     * The maximum height for uploaded image files. If a file is taller than this, it
-     * will be resized without warning before being uploaded. If set to null,
-     * the image won't be resized based on height (but might be depending on
-     * maxImageWidth).
-     * @type {number}
-     * @default 1200
-     * @since 2.15.0
-     */
-    maxImageHeight: 1200,
-
-    /**
-     * The maximum width for uploaded image files. If a file is wider than this, it
-     * will be resized without warning before being uploaded. If set to null,
-     * the image won't be resized based on width (but might be depending on
-     * maxImageHeight).
-     * @type {number}
-     * @default 1200
-     * @since 2.15.0
-     */
-    maxImageWidth: 1200,
-
-    /**
-    * A jQuery selector for the HTML textarea element that will contain the
-    * markdown text.
-    * @type {string}
-    */
-    textarea: ".markdown-textarea",
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "click #markdown-preview-link"   :    "previewMarkdown",
-      "focusout .markdown-textarea"    :    "updateMarkdown"
-    },
-
-    /**
-    * Initialize is executed when a new markdownEditor is created.
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options){
-      if(typeof options !== "undefined"){
-          this.model               =  options.model               || new EMLText();
-          this.markdownPlaceholder =  options.markdownPlaceholder || "";
-          this.previewPlaceholder  =  options.previewPlaceholder  || "";
-          this.showTOC             =  options.showTOC             || false;
-      }
-    },
-
-    /**
-     * render - Renders the markdownEditor - add UI for adding and editing
-     * markdown to a textarea
-     */
-    render: function(){
-
-      try {
-
-        // Save the view
-        var view = this;
-
-        // The markdown attribute in the model may be a string or an array of strings.
-        // Although EML211 can comprise an array of markdown elements,
-        // this view will only render/edit the first if there are multiple.
-        var markdown = this.model.get("markdown");
-        if(Array.isArray(markdown) && markdown.length){
-          markdown = markdown[0]
-        }
-        if(!markdown || !markdown.length){
-          markdown = this.model.get("markdownExample")
-        }
-
-        // Insert the template into the view
-        this.$el.html(this.template({
-          markdown: markdown || "",
-          markdownPlaceholder: this.markdownPlaceholder || "",
-          previewPlaceholder: this.previewPlaceholder || "",
-          cid: this.cid
-        })).data("view", this);
-
-        // The textarea element that the markdown editor buttons & functions will edit
-        var textarea = this.$el.find(this.textarea);
-
-        if(textarea && textarea.length){
-          textarea = textarea[0]
-        }
-
-        if(!textarea){
-          console.log("error: the markdown editor view was not rendered because no textarea element was found.");
-          return
+    /** @lends MarkdownEditorView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       * @readonly
+       */
+      type: "MarkdownEditor",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "markdown-editor",
+
+      /**
+       * References to templates for this view. HTML files are converted to
+       * Underscore.js templates
+       * @type {Underscore.Template}
+       */
+      template: _.template(Template),
+
+      /*
+       * Markdown to insert into the textarea when the view is first rendered
+       * @type {string}
+       */
+      // markdown: "",
+
+      /**
+       * EMLText model that contains a markdown attribute to edit. The markdown is
+       * inserted into the textarea when the view is first rendered. If there's no markdown,
+       * then the view looks for markdown from the markdownExample attribute in the model.
+       * Note that if there are multiple markdown strings in the model, only the first
+       * is rendered/edited.
+       * @type {EMLText}
+       */
+      model: null,
+
+      /**
+       * The placeholder text to display in the textarea when it's empty
+       * @type {string}
+       */
+      markdownPlaceholder: "",
+
+      /**
+       * The placeholder text to display in the preview area when there's no
+       * markdown
+       * @type {string}
+       */
+      previewPlaceholder: "",
+
+      /**
+       * Indicates whether or not to render a table of contents for the markdown
+       * preview. If set to true, a table of contents will be shown in the preview
+       * if there two or more top-level headers are rendered from the markdown.
+       * @type {boolean}
+       */
+      showTOC: false,
+
+      /**
+       * The maximum height for uploaded image files. If a file is taller than this, it
+       * will be resized without warning before being uploaded. If set to null,
+       * the image won't be resized based on height (but might be depending on
+       * maxImageWidth).
+       * @type {number}
+       * @default 1200
+       * @since 2.15.0
+       */
+      maxImageHeight: 1200,
+
+      /**
+       * The maximum width for uploaded image files. If a file is wider than this, it
+       * will be resized without warning before being uploaded. If set to null,
+       * the image won't be resized based on width (but might be depending on
+       * maxImageHeight).
+       * @type {number}
+       * @default 1200
+       * @since 2.15.0
+       */
+      maxImageWidth: 1200,
+
+      /**
+       * A jQuery selector for the HTML textarea element that will contain the
+       * markdown text.
+       * @type {string}
+       */
+      textarea: ".markdown-textarea",
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "click #markdown-preview-link": "previewMarkdown",
+        "focusout .markdown-textarea": "updateMarkdown",
+      },
+
+      /**
+       * Initialize is executed when a new markdownEditor is created.
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        if (typeof options !== "undefined") {
+          this.model = options.model || new EMLText();
+          this.markdownPlaceholder = options.markdownPlaceholder || "";
+          this.previewPlaceholder = options.previewPlaceholder || "";
+          this.showTOC = options.showTOC || false;
         }
+      },
+
+      /**
+       * render - Renders the markdownEditor - add UI for adding and editing
+       * markdown to a textarea
+       */
+      render: function () {
+        try {
+          // Save the view
+          var view = this;
+
+          // The markdown attribute in the model may be a string or an array of strings.
+          // Although EML211 can comprise an array of markdown elements,
+          // this view will only render/edit the first if there are multiple.
+          var markdown = this.model.get("markdown");
+          if (Array.isArray(markdown) && markdown.length) {
+            markdown = markdown[0];
+          }
+          if (!markdown || !markdown.length) {
+            markdown = this.model.get("markdownExample");
+          }
 
-        // Set woofmark options. See https://github.com/bevacqua/woofmark
-        var woofmarkOptions =
-          {
-              fencing: true,
-              html: false,
-              wysiwyg: false,
-              defaultMode: "markdown",
-              render: {
-                // Hide buttons that switch between markdown, WYSIWYG, & HTML for now
-                modes: function (button, id) {
-                  button.remove();
-                }
-              }
-          };
+          // Insert the template into the view
+          this.$el
+            .html(
+              this.template({
+                markdown: markdown || "",
+                markdownPlaceholder: this.markdownPlaceholder || "",
+                previewPlaceholder: this.previewPlaceholder || "",
+                cid: this.cid,
+              }),
+            )
+            .data("view", this);
+
+          // The textarea element that the markdown editor buttons & functions will edit
+          var textarea = this.$el.find(this.textarea);
+
+          if (textarea && textarea.length) {
+            textarea = textarea[0];
+          }
 
-        // Set options for all the buttons that will be shown in the toolbar.
-        // Buttons will be shown in the order they are listed.
-        // Defaults from Woofmark will be used unless they are replaced here,
-        // see: https://github.com/bevacqua/woofmark/blob/master/src/strings.js.
-        // They key is the ID for the button.
-        //    remove: if set to true, the button will be removed (use this to hide default woofmark buttons)
-        //    icon: the name of the font awesome icon to show in the button. If no button or svg is set, the ID/key will be displayed instead.
-        //    svg: svg code to show in the button. If no button or svg is set, the ID/key will be displayed instead.
-        //    title: The title to show on hover
-        //    function: The function to call when the button is pressed. It will be passed chunks, cmd, e (see Woofmark docs), plus the ID/key. Called with view as the this (context).
-        //    shortcut: The keyboard shortcut to use for the button. This will only work if there is also a custom function set.
-        //    insertDividerAfter: If set to true, a visual divider will be placed after this button.
-        var buttonOptions = {
-          // Default woofmark buttons to remove
-          attachment: {
-            remove: true
-          },
-          heading: {
-            remove: true
-          },
-          hr: {
-            remove: true
-          },
-          // Remove the default image uploader button so we can add our own that
-          // uploads the image as a dataone object.
-          image: {
-            remove: true
-          },
-          // Default woofmark buttons to keep, with custom properties, + custom buttons
-          bold: {
-            icon: "bold"
-          },
-          italic: {
-            icon: "italic",
-          },
-          strike: {
-            title: "Strikethrough",
-            icon: "strikethrough",
-            shortcut: "Ctrl+Shift+X",
-            function: view.strikethrough,
-            insertDividerAfter: true
-          },
-          h1: {
-            svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 2.9V0h7.8v2.9l-2 .5v7h7.7v-7l-2-.5V0h7.7v2.9l-2 .5v17.2l2 .5V24h-7.8v-2.9l2-.5V14H5.9v6.6l2 .5V24H0v-2.9l2-.5V3.4z"/><path fill-rule="nonzero" d="M24 16.4v-1.9h-1.4V5.8h-4.1v1.8H20v7h-1.4v1.8z"/></svg>`,
-            title: "Top-level heading",
-            function: view.addHeader
-          },
-          h2: {
-            svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="nonzero" d="M23.2 17.3l.1-3.1h-2v1.3H18c.1-.7.8-1.5 2-2.2L22 12a4 4 0 001-1l.3-1.5c0-.9-.3-1.6-1-2.1-.6-.5-1.5-.8-2.6-.8-1.3 0-2.2.3-2.9 1-.6.6-1 1.5-1 2.8l2.2.1c0-.8.2-1.3.4-1.7.3-.3.6-.5 1.1-.5.4 0 .7.1.9.3.2.2.3.5.3.8 0 .4-.1.7-.4 1-.2.4-.6.7-1.1 1a17 17 0 00-2.1 1.9c-.5.5-.8 1-1 1.6-.3.6-.4 1.4-.4 2.3h7.6z"/><path d="M.5 4.6V2.3h6.3v2.3L5.2 5v5.6h6.2V5l-1.6-.4V2.3H16v2.3l-1.6.4v14l1.6.4v2.3H9.8v-2.3l1.6-.4v-5.3H5.2V19l1.6.4v2.3H.5v-2.3l1.6-.4V5z"/></svg>`,
-            title: "Second-level heading",
-            function: view.addHeader
-          },
-          h3: {
-            svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="nonzero" d="M18.1 17.3c.7 0 1.4-.1 2-.4.6-.2 1-.6 1.4-1 .3-.6.5-1.1.5-1.7 0-1.2-.7-2-2-2.5 1-.5 1.6-1.2 1.6-2.2 0-.9-.4-1.6-1-2-.6-.6-1.5-.8-2.6-.8-1 0-1.9.3-2.5.8a3 3 0 00-1 2.2l2.1.1c0-.9.4-1.3 1.3-1.3.3 0 .6 0 .9.3.2.2.3.4.3.8s-.2.7-.5.9a3 3 0 01-1.5.3v1.9h.6c.5 0 1 0 1.2.3.3.3.5.7.5 1 0 .5-.2.8-.4 1.1-.3.3-.7.5-1.1.5-1 0-1.5-.6-1.5-1.8l-2.2.1c.1 2.3 1.4 3.4 4 3.4z"/><path d="M2 6.6V4.8h4.7v1.8l-1.2.3V11H10V6.9l-1.2-.3V4.8h4.6v1.8l-1.2.3V17l1.2.3v1.8H8.9v-1.8l1.2-.3v-3.9H5.5v4l1.2.2v1.8H2v-1.8l1.2-.3V7z"/></svg>`,
-            title: "Tertiary heading",
-            function: view.addHeader,
-            insertDividerAfter: true
-          },
-          divider: {
-            svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="22" height="3" x="1" y="10.58" fill-rule="evenodd"/></svg>`,
-            function: view.addDivider,
-            title: "Top-level heading",
-            shortcut: "Ctrl+Enter"
-          },
-          ol: {
-            title: "Ordered list",
-            icon: "list-ol",
-          },
-          ul: {
-            title: "Un-ordered list",
-            icon: "list-ul",
-            insertDividerAfter: true
-          },
-          quote: {
-            icon: "quote-left",
-          },
-          code: {
-            icon: "code",
-            insertDividerAfter: true
-          },
-          link: {
-            icon: "link"
-          },
-          d1Image: {
-            icon: "picture",
-            function: view.addMdImage,
-            title: 'Image',
-            // use Ctrl+G to overwrite the built-in woofmark image function
-            shortcut: "Ctrl+G"
-          },
-          table: {
-            icon: "table",
-            function: view.addTable,
-            insertDividerAfter: true
+          if (!textarea) {
+            console.log(
+              "error: the markdown editor view was not rendered because no textarea element was found.",
+            );
+            return;
           }
-        }
 
-        var buttonKeys = Object.keys(buttonOptions);
+          // Set woofmark options. See https://github.com/bevacqua/woofmark
+          var woofmarkOptions = {
+            fencing: true,
+            html: false,
+            wysiwyg: false,
+            defaultMode: "markdown",
+            render: {
+              // Hide buttons that switch between markdown, WYSIWYG, & HTML for now
+              modes: function (button, id) {
+                button.remove();
+              },
+            },
+          };
 
-        // PRE-RENDER WOOFMARK
-        // Set titles on buttons before the Woofmark text editor is rendered.
-        // This way we can use Woofmark's build in functionality to convert "Ctrl"
-        // to "Cmd" symbol if user is on mac.
-        _.each(buttonKeys, function(key, i) {
-          var options = buttonOptions[key],
-              title = options.title || key.charAt(0).toUpperCase() + key.slice(1),
-              presetShortcut = "";
+          // Set options for all the buttons that will be shown in the toolbar.
+          // Buttons will be shown in the order they are listed.
+          // Defaults from Woofmark will be used unless they are replaced here,
+          // see: https://github.com/bevacqua/woofmark/blob/master/src/strings.js.
+          // They key is the ID for the button.
+          //    remove: if set to true, the button will be removed (use this to hide default woofmark buttons)
+          //    icon: the name of the font awesome icon to show in the button. If no button or svg is set, the ID/key will be displayed instead.
+          //    svg: svg code to show in the button. If no button or svg is set, the ID/key will be displayed instead.
+          //    title: The title to show on hover
+          //    function: The function to call when the button is pressed. It will be passed chunks, cmd, e (see Woofmark docs), plus the ID/key. Called with view as the this (context).
+          //    shortcut: The keyboard shortcut to use for the button. This will only work if there is also a custom function set.
+          //    insertDividerAfter: If set to true, a visual divider will be placed after this button.
+          var buttonOptions = {
+            // Default woofmark buttons to remove
+            attachment: {
+              remove: true,
+            },
+            heading: {
+              remove: true,
+            },
+            hr: {
+              remove: true,
+            },
+            // Remove the default image uploader button so we can add our own that
+            // uploads the image as a dataone object.
+            image: {
+              remove: true,
+            },
+            // Default woofmark buttons to keep, with custom properties, + custom buttons
+            bold: {
+              icon: "bold",
+            },
+            italic: {
+              icon: "italic",
+            },
+            strike: {
+              title: "Strikethrough",
+              icon: "strikethrough",
+              shortcut: "Ctrl+Shift+X",
+              function: view.strikethrough,
+              insertDividerAfter: true,
+            },
+            h1: {
+              svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 2.9V0h7.8v2.9l-2 .5v7h7.7v-7l-2-.5V0h7.7v2.9l-2 .5v17.2l2 .5V24h-7.8v-2.9l2-.5V14H5.9v6.6l2 .5V24H0v-2.9l2-.5V3.4z"/><path fill-rule="nonzero" d="M24 16.4v-1.9h-1.4V5.8h-4.1v1.8H20v7h-1.4v1.8z"/></svg>`,
+              title: "Top-level heading",
+              function: view.addHeader,
+            },
+            h2: {
+              svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="nonzero" d="M23.2 17.3l.1-3.1h-2v1.3H18c.1-.7.8-1.5 2-2.2L22 12a4 4 0 001-1l.3-1.5c0-.9-.3-1.6-1-2.1-.6-.5-1.5-.8-2.6-.8-1.3 0-2.2.3-2.9 1-.6.6-1 1.5-1 2.8l2.2.1c0-.8.2-1.3.4-1.7.3-.3.6-.5 1.1-.5.4 0 .7.1.9.3.2.2.3.5.3.8 0 .4-.1.7-.4 1-.2.4-.6.7-1.1 1a17 17 0 00-2.1 1.9c-.5.5-.8 1-1 1.6-.3.6-.4 1.4-.4 2.3h7.6z"/><path d="M.5 4.6V2.3h6.3v2.3L5.2 5v5.6h6.2V5l-1.6-.4V2.3H16v2.3l-1.6.4v14l1.6.4v2.3H9.8v-2.3l1.6-.4v-5.3H5.2V19l1.6.4v2.3H.5v-2.3l1.6-.4V5z"/></svg>`,
+              title: "Second-level heading",
+              function: view.addHeader,
+            },
+            h3: {
+              svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="nonzero" d="M18.1 17.3c.7 0 1.4-.1 2-.4.6-.2 1-.6 1.4-1 .3-.6.5-1.1.5-1.7 0-1.2-.7-2-2-2.5 1-.5 1.6-1.2 1.6-2.2 0-.9-.4-1.6-1-2-.6-.6-1.5-.8-2.6-.8-1 0-1.9.3-2.5.8a3 3 0 00-1 2.2l2.1.1c0-.9.4-1.3 1.3-1.3.3 0 .6 0 .9.3.2.2.3.4.3.8s-.2.7-.5.9a3 3 0 01-1.5.3v1.9h.6c.5 0 1 0 1.2.3.3.3.5.7.5 1 0 .5-.2.8-.4 1.1-.3.3-.7.5-1.1.5-1 0-1.5-.6-1.5-1.8l-2.2.1c.1 2.3 1.4 3.4 4 3.4z"/><path d="M2 6.6V4.8h4.7v1.8l-1.2.3V11H10V6.9l-1.2-.3V4.8h4.6v1.8l-1.2.3V17l1.2.3v1.8H8.9v-1.8l1.2-.3v-3.9H5.5v4l1.2.2v1.8H2v-1.8l1.2-.3V7z"/></svg>`,
+              title: "Tertiary heading",
+              function: view.addHeader,
+              insertDividerAfter: true,
+            },
+            divider: {
+              svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="22" height="3" x="1" y="10.58" fill-rule="evenodd"/></svg>`,
+              function: view.addDivider,
+              title: "Top-level heading",
+              shortcut: "Ctrl+Enter",
+            },
+            ol: {
+              title: "Ordered list",
+              icon: "list-ol",
+            },
+            ul: {
+              title: "Un-ordered list",
+              icon: "list-ul",
+              insertDividerAfter: true,
+            },
+            quote: {
+              icon: "quote-left",
+            },
+            code: {
+              icon: "code",
+              insertDividerAfter: true,
+            },
+            link: {
+              icon: "link",
+            },
+            d1Image: {
+              icon: "picture",
+              function: view.addMdImage,
+              title: "Image",
+              // use Ctrl+G to overwrite the built-in woofmark image function
+              shortcut: "Ctrl+G",
+            },
+            table: {
+              icon: "table",
+              function: view.addTable,
+              insertDividerAfter: true,
+            },
+          };
 
-              if(Woofmark.strings.titles[key] && Woofmark.strings.titles[key].match(/Ctrl\+.*$/) ){
-                presetShortcut = Woofmark.strings.titles[key].match(/Ctrl\+.*$/)[0];
+          var buttonKeys = Object.keys(buttonOptions);
+
+          // PRE-RENDER WOOFMARK
+          // Set titles on buttons before the Woofmark text editor is rendered.
+          // This way we can use Woofmark's build in functionality to convert "Ctrl"
+          // to "Cmd" symbol if user is on mac.
+          _.each(
+            buttonKeys,
+            function (key, i) {
+              var options = buttonOptions[key],
+                title =
+                  options.title || key.charAt(0).toUpperCase() + key.slice(1),
+                presetShortcut = "";
+
+              if (
+                Woofmark.strings.titles[key] &&
+                Woofmark.strings.titles[key].match(/Ctrl\+.*$/)
+              ) {
+                presetShortcut =
+                  Woofmark.strings.titles[key].match(/Ctrl\+.*$/)[0];
               }
 
               var shortcut = options.shortcut || presetShortcut;
 
-          if(title){
-            Woofmark.strings.titles[key] = [title, shortcut].join(" ")
-          }
-          // So that we can identify buttons that we want to manipulate after
-          // they are rendered, use the key as the button text for now.
-          Woofmark.strings.buttons[key] = key;
-        }, this);
-
-        // RENDER WOOFMARK
-        // Initialize the woofmark markdown editor
-        this.markdownEditor = new Woofmark(textarea, woofmarkOptions);
-
-        // POST-RENDER WOOFMARK
-        // After the markdown editor is initialized..
-
-        // Add custom functions
-        _.each(buttonKeys, function(key, i) {
-          var options = buttonOptions[key];
-          if(options.function){
-            // addCommandButton uses cmd, not ctrl
-            var shortcut = ""
-            if(options.shortcut){
-              shortcut = options.shortcut.replace("Ctrl", "cmd");
-            }
-            view.markdownEditor.addCommandButton(key, shortcut, function(e, mode, chunks){
-              options.function.call(view, e, mode, chunks, key)
-            })
-          }
-        });
+              if (title) {
+                Woofmark.strings.titles[key] = [title, shortcut].join(" ");
+              }
+              // So that we can identify buttons that we want to manipulate after
+              // they are rendered, use the key as the button text for now.
+              Woofmark.strings.buttons[key] = key;
+            },
+            this,
+          );
 
-        // Modify the button order & appearance
-        var buttonContainer = $(view.markdownEditor.textarea).parent().find(".wk-commands");
-        var buttons = buttonContainer.find(".wk-command");
-        _.each(buttonKeys, function(key, i) {
-          // Re-order buttons based on the order in buttonOptions, and remove
-          // any that are marked for removal
-          var options = buttonOptions[key];
-          var buttonEl = buttonContainer.find(".wk-command").filter(function (){
-              return this.innerHTML == key;
-          });
-          if(options.remove !== true){
-            // Add tooltip
-            buttonEl.tooltip({
-      				placement: "top",
-      				delay: 500,
-              trigger: "hover"
-      			});
-            // Add font awesome icon or SVG
-            if(options.icon){
-              buttonEl.html("<i class='icon-" +options.icon + "'></i>");
-            } else if (options.svg){
-              buttonEl.html(options.svg);
-              buttonEl.find("svg").height("13px").width("auto");
-            }
-            buttonContainer.append(buttonEl);
-            if(options.insertDividerAfter === true ){
-              buttonContainer.append("<div class='wk-commands-divider'></div>")
+          // RENDER WOOFMARK
+          // Initialize the woofmark markdown editor
+          this.markdownEditor = new Woofmark(textarea, woofmarkOptions);
+
+          // POST-RENDER WOOFMARK
+          // After the markdown editor is initialized..
+
+          // Add custom functions
+          _.each(buttonKeys, function (key, i) {
+            var options = buttonOptions[key];
+            if (options.function) {
+              // addCommandButton uses cmd, not ctrl
+              var shortcut = "";
+              if (options.shortcut) {
+                shortcut = options.shortcut.replace("Ctrl", "cmd");
+              }
+              view.markdownEditor.addCommandButton(
+                key,
+                shortcut,
+                function (e, mode, chunks) {
+                  options.function.call(view, e, mode, chunks, key);
+                },
+              );
             }
-          } else {
-            buttonEl.remove();
-          }
-        });
-
-      } catch (e) {
-        console.log(e);
-        console.log("Failed to render the markdown editor UI, error message: " + e );
-      }
-
-    },
-
-    /**
-     * addHeader - description
-     *
-     * @param  {event}  e      is the original event object
-     * @param  {string} mode   can be markdown, html, or wysiwyg
-     * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
-     * @param  {string} id     the ID of the function, set as they key in buttonOptions in the render function
-     */
-    addHeader: function(e, mode, chunks, id){
-
-      // Get the header level from the ID
-      var levelToCreate = parseInt(id.replace( /^\D+/g, ''));
-
-      chunks.selection = chunks.selection
-        .replace(/\s+/g, ' ')
-        .replace(/(^\s+|\s+$)/g, '');
-
-      if (!chunks.selection) {
-        chunks.startTag = new Array(levelToCreate + 1).join('#') + ' ';
-        chunks.selection = Woofmark.strings.placeholders.heading;
-        chunks.endTag = '';
-        chunks.skip({ before: 1, after: 1 });
-        return;
-      }
-
-      chunks.findTags(/#+[ ]*/, /[ ]*#+/);
+          });
 
-      if (/#+/.test(chunks.startTag)) {
-        level = RegExp.lastMatch.length;
-      }
+          // Modify the button order & appearance
+          var buttonContainer = $(view.markdownEditor.textarea)
+            .parent()
+            .find(".wk-commands");
+          var buttons = buttonContainer.find(".wk-command");
+          _.each(buttonKeys, function (key, i) {
+            // Re-order buttons based on the order in buttonOptions, and remove
+            // any that are marked for removal
+            var options = buttonOptions[key];
+            var buttonEl = buttonContainer
+              .find(".wk-command")
+              .filter(function () {
+                return this.innerHTML == key;
+              });
+            if (options.remove !== true) {
+              // Add tooltip
+              buttonEl.tooltip({
+                placement: "top",
+                delay: 500,
+                trigger: "hover",
+              });
+              // Add font awesome icon or SVG
+              if (options.icon) {
+                buttonEl.html("<i class='icon-" + options.icon + "'></i>");
+              } else if (options.svg) {
+                buttonEl.html(options.svg);
+                buttonEl.find("svg").height("13px").width("auto");
+              }
+              buttonContainer.append(buttonEl);
+              if (options.insertDividerAfter === true) {
+                buttonContainer.append(
+                  "<div class='wk-commands-divider'></div>",
+                );
+              }
+            } else {
+              buttonEl.remove();
+            }
+          });
+        } catch (e) {
+          console.log(e);
+          console.log(
+            "Failed to render the markdown editor UI, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * addHeader - description
+       *
+       * @param  {event}  e      is the original event object
+       * @param  {string} mode   can be markdown, html, or wysiwyg
+       * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
+       * @param  {string} id     the ID of the function, set as they key in buttonOptions in the render function
+       */
+      addHeader: function (e, mode, chunks, id) {
+        // Get the header level from the ID
+        var levelToCreate = parseInt(id.replace(/^\D+/g, ""));
+
+        chunks.selection = chunks.selection
+          .replace(/\s+/g, " ")
+          .replace(/(^\s+|\s+$)/g, "");
+
+        if (!chunks.selection) {
+          chunks.startTag = new Array(levelToCreate + 1).join("#") + " ";
+          chunks.selection = Woofmark.strings.placeholders.heading;
+          chunks.endTag = "";
+          chunks.skip({ before: 1, after: 1 });
+          return;
+        }
 
-      chunks.startTag = chunks.endTag = '';
-      chunks.findTags(null, /\s?(-+|=+)/);
+        chunks.findTags(/#+[ ]*/, /[ ]*#+/);
 
-      if (/=+/.test(chunks.endTag)) {
-        level = 1;
-      }
+        if (/#+/.test(chunks.startTag)) {
+          level = RegExp.lastMatch.length;
+        }
 
-      if (/-+/.test(chunks.endTag)) {
-        level = 2;
-      }
+        chunks.startTag = chunks.endTag = "";
+        chunks.findTags(null, /\s?(-+|=+)/);
 
-      chunks.startTag = chunks.endTag = '';
-      chunks.skip({ before: 1, after: 1 });
+        if (/=+/.test(chunks.endTag)) {
+          level = 1;
+        }
 
-      if (levelToCreate > 0) {
-        chunks.startTag = new Array(levelToCreate + 1).join('#') + ' ';
-      }
+        if (/-+/.test(chunks.endTag)) {
+          level = 2;
+        }
 
-    },
+        chunks.startTag = chunks.endTag = "";
+        chunks.skip({ before: 1, after: 1 });
 
-    /**
-     * addDivider - Add or remove a divider
-     *
-     * @param  {event} e      is the original event object
-     * @param  {string} mode   can be markdown, html, or wysiwyg
-     * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
-     */
-    addDivider: function(e, mode, chunks){
-
-      // If the selection includes a divider, remove it
-      var markdown = chunks.before + chunks.selection + chunks.after;
-      var startSel = chunks.before.length;
-      var endSel = startSel + chunks.selection.length + 1;
-      var dividerRE = /(\r\n|\r|\n){2}-{3,}/gm;
-      var dividerDeleted = false;
-      while((match = dividerRE.exec(markdown)) !== null) {
-        // +1 so that we don't delete the divider if selection is at the newlines before a divider
-        var startDivider = match.index + 2;
-        // +1 so that if the selection is at the end of a divider, it will still be deleted
-        var endDivider = match.index + match[0].length + 1;
-        if((endSel > startDivider && endSel <= endDivider) || (startSel < endDivider && startSel >= startDivider)){
-          tableString = match[0];
-          chunks.before = markdown.slice(0,startDivider-2) + "\n";
-          chunks.selection = "";
-          chunks.after = markdown.slice(endDivider);
-          dividerDeleted = true;
-          break;
+        if (levelToCreate > 0) {
+          chunks.startTag = new Array(levelToCreate + 1).join("#") + " ";
         }
-      }
-      // If the divider was not deleted (therefore not detected), then add one
-      if(!dividerDeleted){
-        var dividerToAdd = "\n\n--------------------\n";
-        if(/(\r\n|\r|\n){1}$/.test(chunks.before)){
-          dividerToAdd = "\n--------------------\n";
+      },
+
+      /**
+       * addDivider - Add or remove a divider
+       *
+       * @param  {event} e      is the original event object
+       * @param  {string} mode   can be markdown, html, or wysiwyg
+       * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
+       */
+      addDivider: function (e, mode, chunks) {
+        // If the selection includes a divider, remove it
+        var markdown = chunks.before + chunks.selection + chunks.after;
+        var startSel = chunks.before.length;
+        var endSel = startSel + chunks.selection.length + 1;
+        var dividerRE = /(\r\n|\r|\n){2}-{3,}/gm;
+        var dividerDeleted = false;
+        while ((match = dividerRE.exec(markdown)) !== null) {
+          // +1 so that we don't delete the divider if selection is at the newlines before a divider
+          var startDivider = match.index + 2;
+          // +1 so that if the selection is at the end of a divider, it will still be deleted
+          var endDivider = match.index + match[0].length + 1;
+          if (
+            (endSel > startDivider && endSel <= endDivider) ||
+            (startSel < endDivider && startSel >= startDivider)
+          ) {
+            tableString = match[0];
+            chunks.before = markdown.slice(0, startDivider - 2) + "\n";
+            chunks.selection = "";
+            chunks.after = markdown.slice(endDivider);
+            dividerDeleted = true;
+            break;
+          }
         }
-        chunks.before = chunks.before + dividerToAdd
-      }
-    },
+        // If the divider was not deleted (therefore not detected), then add one
+        if (!dividerDeleted) {
+          var dividerToAdd = "\n\n--------------------\n";
+          if (/(\r\n|\r|\n){1}$/.test(chunks.before)) {
+            dividerToAdd = "\n--------------------\n";
+          }
+          chunks.before = chunks.before + dividerToAdd;
+        }
+      },
+
+      /**
+       * addTable - Creates the UI for editing and adding tables to the textarea.
+       * Detects whether the selection contained any part of a markdown table,
+       * then opens a woofmark dialog box and inserts a table editor view. If a
+       * table was selected, the table information is imported into the table
+       * editor where the user can edit it. If no table was selected, then it
+       * creates an empty table where the user can add data.
+       *
+       * @param  {event} e      is the original event object
+       * @param  {string} mode   can be markdown, html, or wysiwyg
+       * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
+       */
+      addTable: function (e, mode, chunks) {
+        // Use a modified version of the link dialog
+        this.markdownEditor.showLinkDialog();
 
-    /**
-     * addTable - Creates the UI for editing and adding tables to the textarea.
-     * Detects whether the selection contained any part of a markdown table,
-     * then opens a woofmark dialog box and inserts a table editor view. If a
-     * table was selected, the table information is imported into the table
-     * editor where the user can edit it. If no table was selected, then it
-     * creates an empty table where the user can add data.
-     *
-     * @param  {event} e      is the original event object
-     * @param  {string} mode   can be markdown, html, or wysiwyg
-     * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
-     */
-    addTable: function(e, mode, chunks){
-
-      // Use a modified version of the link dialog
-      this.markdownEditor.showLinkDialog();
-
-      // Select the image upload dialog elements so that we can customize it
-      var dialog = $(".wk-prompt"),
+        // Select the image upload dialog elements so that we can customize it
+        var dialog = $(".wk-prompt"),
           dialogContent = dialog.find(".wk-prompt-input-container"),
           dialogTitle = dialog.find(".wk-prompt-title"),
           dialogDescription = dialog.find(".wk-prompt-description"),
           dialogOkBtn = dialog.find(".wk-prompt-ok");
 
-      // Detect whether the selection includes a markdown table.
-      // If it does, ensure the complete table is selected, and save the
-      // markdown table string segment to be parsed.
-      var markdown = chunks.before + chunks.selection + chunks.after;
-      var startSel = chunks.before.length;
-      var endSel = startSel + chunks.selection.length + 1;
-      var tableRE = /((\|[^|\r\n]*)+\|(\r?\n|\r)?)+((?:\s*\|\s*:?\s*[-=]+\s*:?\s*)+\|)(\n\s*(?:\|[^\n]+\|\r?\n?)*)?$/gm;
-      // The regular expression used by showdown to detect tables:
-      // var tableRE = /^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm;
-      var tables = markdown.match(tableRE);
-      var tableString = "";
-      while((match = tableRE.exec(markdown)) !== null) {
-        var startTab = match.index;
-        var endTab = match.index + match[0].length;
-        if((endSel > startTab && endSel <= endTab) || (startSel < endTab && startSel >= startTab)){
-          tableString = match[0];
-          chunks.before = markdown.slice(0,startTab);
-          chunks.selection = markdown.slice(startTab, endTab);
-          chunks.after = markdown.slice(endTab);
-          // Just use the first table match in which there is also at least partial selection
-          break;
+        // Detect whether the selection includes a markdown table.
+        // If it does, ensure the complete table is selected, and save the
+        // markdown table string segment to be parsed.
+        var markdown = chunks.before + chunks.selection + chunks.after;
+        var startSel = chunks.before.length;
+        var endSel = startSel + chunks.selection.length + 1;
+        var tableRE =
+          /((\|[^|\r\n]*)+\|(\r?\n|\r)?)+((?:\s*\|\s*:?\s*[-=]+\s*:?\s*)+\|)(\n\s*(?:\|[^\n]+\|\r?\n?)*)?$/gm;
+        // The regular expression used by showdown to detect tables:
+        // var tableRE = /^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm;
+        var tables = markdown.match(tableRE);
+        var tableString = "";
+        while ((match = tableRE.exec(markdown)) !== null) {
+          var startTab = match.index;
+          var endTab = match.index + match[0].length;
+          if (
+            (endSel > startTab && endSel <= endTab) ||
+            (startSel < endTab && startSel >= startTab)
+          ) {
+            tableString = match[0];
+            chunks.before = markdown.slice(0, startTab);
+            chunks.selection = markdown.slice(startTab, endTab);
+            chunks.after = markdown.slice(endTab);
+            // Just use the first table match in which there is also at least partial selection
+            break;
+          }
         }
-      }
-
-      // Clone the chunks object at this point in case the textarea loses focus
-      // and the selection changes before the "ok" buttons is pressed
-      const chunksClone = JSON.parse(JSON.stringify(chunks));
-
-      // Add a table editor view.
-      // Pass the parsesd markdown table, if there is one
-      var tableEditor = new TableEditor({
-        markdown: tableString
-      });
-      // Render the table editor
-      tableEditor.render();
-      // Add the rendered table editor to the dialog, update the dialog.
-      dialogContent.html(tableEditor.el);
-      dialogDescription.remove();
-      dialogTitle.text("Insert Table");
-
-      // Listen for when the OK button is clicked. Attach listener to the dialog
-      // so that it's destroyed when the dialog is destroyed. It won't be called
-      // if the user presses cancel.
-      var view = this;
-      dialogOkBtn.off('click');
-      dialogOkBtn.on('click', function insertText(event) {
-        var tableMarkdown = tableEditor.getMarkdown();
-        view.markdownEditor.runCommand( function(chunks, mode){
-          chunks.before = chunksClone.before;
-          chunks.after = chunksClone.after;
-          chunks.selection = tableMarkdown;
-        });
-      });
-
 
-    },
+        // Clone the chunks object at this point in case the textarea loses focus
+        // and the selection changes before the "ok" buttons is pressed
+        const chunksClone = JSON.parse(JSON.stringify(chunks));
 
-    /**
-     * addMdImage - The function that gets called when a user clicks the custom
-     * add image button added to the markdown editor. It uses the UI created by
-     * the ImageUploaderView to allow a user to select & upload an image to the
-     * repository, and uses Woofmark's built-in add image functionality to
-     * insert the correct markdown into the textarea. This function must be
-     * called such that "this" is the markdownEditor view.
-     */
-    addMdImage: function() {
-
-      try {
+        // Add a table editor view.
+        // Pass the parsesd markdown table, if there is one
+        var tableEditor = new TableEditor({
+          markdown: tableString,
+        });
+        // Render the table editor
+        tableEditor.render();
+        // Add the rendered table editor to the dialog, update the dialog.
+        dialogContent.html(tableEditor.el);
+        dialogDescription.remove();
+        dialogTitle.text("Insert Table");
+
+        // Listen for when the OK button is clicked. Attach listener to the dialog
+        // so that it's destroyed when the dialog is destroyed. It won't be called
+        // if the user presses cancel.
         var view = this;
-
-        // Show woofmark's default image upload dialog, inserted at the end of body
-        view.markdownEditor.showImageDialog();
-
-        // Select the image upload dialog elements so that we can customize it
-        var imageDialog = $(".wk-prompt"),
+        dialogOkBtn.off("click");
+        dialogOkBtn.on("click", function insertText(event) {
+          var tableMarkdown = tableEditor.getMarkdown();
+          view.markdownEditor.runCommand(function (chunks, mode) {
+            chunks.before = chunksClone.before;
+            chunks.after = chunksClone.after;
+            chunks.selection = tableMarkdown;
+          });
+        });
+      },
+
+      /**
+       * addMdImage - The function that gets called when a user clicks the custom
+       * add image button added to the markdown editor. It uses the UI created by
+       * the ImageUploaderView to allow a user to select & upload an image to the
+       * repository, and uses Woofmark's built-in add image functionality to
+       * insert the correct markdown into the textarea. This function must be
+       * called such that "this" is the markdownEditor view.
+       */
+      addMdImage: function () {
+        try {
+          var view = this;
+
+          // Show woofmark's default image upload dialog, inserted at the end of body
+          view.markdownEditor.showImageDialog();
+
+          // Select the image upload dialog elements so that we can customize it
+          var imageDialog = $(".wk-prompt"),
             imageDialogInput = imageDialog.find(".wk-prompt-input"),
             imageDialogDescription = imageDialog.find(".wk-prompt-description"),
             imageDialogOkBtn = imageDialog.find(".wk-prompt-ok"),
@@ -610,223 +641,230 @@ 

Source: src/js/views/MarkdownEditorView.js

// temporarily during image upload imageDialogOkBtnTxt = imageDialogOkBtn.html(); - // Create an ImageUploaderView and insert into this view. - mdImageUploader = new ImageUploader({ - uploadInstructions: "Drag & drop an image here or click to upload", - imageTagName: "img", - height: "175", - width: "300", - maxHeight: view.maxImageHeight || null, - maxWidth: view.maxImageWidth || null - }); - - // Show when image is uploading; temporarily disable the OK button - view.stopListening(mdImageUploader, "addedfile"); - view.listenTo(mdImageUploader, "addedfile", function(){ - // Disable the button during upload; - imageDialogOkBtn.prop('disabled', true); - imageDialogOkBtn.css({"opacity":"0.5", "cursor":"not-allowed"}); - imageDialogOkBtn.html( - "<i class='icon-spinner icon-spin icon-large loading icon'></i> "+ - "Uploading..." - ); - }); - - // Update the image input URL when the image is successfully uploaded - view.stopListening(mdImageUploader, "successSaving"); - view.listenTo(mdImageUploader, "successSaving", function(dataONEObject){ - - //Execute the DataONEObject function that performs various functions after - // a successful save - dataONEObject.onSuccessfulSave(); - - // Re-enable the button - imageDialogOkBtn.prop('disabled', false); - imageDialogOkBtn.html(imageDialogOkBtnTxt); - imageDialogOkBtn.css({"opacity":"1", "cursor":"pointer"}); - - // Get the uploaded image's url. - //var url = dataONEObject.url(); - var url = ""; - - if( MetacatUI.appModel.get("isCN") ){ - var sourceRepo; - - //Use the object service URL from the origin MN/datasource - if( dataONEObject.get("datasource") ){ - sourceRepo = MetacatUI.nodeModel.getMember(dataONEObject.get("datasource")); - } - //Use the object service URL from the alt repo - if( !sourceRepo ){ - sourceRepo = MetacatUI.appModel.getActiveAltRepo(); - } - - if( sourceRepo ){ - url = sourceRepo.objectServiceUrl; - } - } + // Create an ImageUploaderView and insert into this view. + mdImageUploader = new ImageUploader({ + uploadInstructions: "Drag & drop an image here or click to upload", + imageTagName: "img", + height: "175", + width: "300", + maxHeight: view.maxImageHeight || null, + maxWidth: view.maxImageWidth || null, + }); - //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel - if( !url ){ - url = MetacatUI.appModel.get("objectServiceUrl") || MetacatUI.appModel.get("resolveServiceUrl"); - } + // Show when image is uploading; temporarily disable the OK button + view.stopListening(mdImageUploader, "addedfile"); + view.listenTo(mdImageUploader, "addedfile", function () { + // Disable the button during upload; + imageDialogOkBtn.prop("disabled", true); + imageDialogOkBtn.css({ opacity: "0.5", cursor: "not-allowed" }); + imageDialogOkBtn.html( + "<i class='icon-spinner icon-spin icon-large loading icon'></i> " + + "Uploading...", + ); + }); - url = url + dataONEObject.get("id"); + // Update the image input URL when the image is successfully uploaded + view.stopListening(mdImageUploader, "successSaving"); + view.listenTo( + mdImageUploader, + "successSaving", + function (dataONEObject) { + //Execute the DataONEObject function that performs various functions after + // a successful save + dataONEObject.onSuccessfulSave(); + + // Re-enable the button + imageDialogOkBtn.prop("disabled", false); + imageDialogOkBtn.html(imageDialogOkBtnTxt); + imageDialogOkBtn.css({ opacity: "1", cursor: "pointer" }); + + // Get the uploaded image's url. + //var url = dataONEObject.url(); + var url = ""; + + if (MetacatUI.appModel.get("isCN")) { + var sourceRepo; + + //Use the object service URL from the origin MN/datasource + if (dataONEObject.get("datasource")) { + sourceRepo = MetacatUI.nodeModel.getMember( + dataONEObject.get("datasource"), + ); + } + //Use the object service URL from the alt repo + if (!sourceRepo) { + sourceRepo = MetacatUI.appModel.getActiveAltRepo(); + } - // Create title out of file name without extension. - var title = dataONEObject.get("fileName"); - if(title && title.lastIndexOf(".") > 0) { - title = title.substring(0, title.lastIndexOf(".")); - } + if (sourceRepo) { + url = sourceRepo.objectServiceUrl; + } + } - // Add the url + title to the input - imageDialogInput.val(url + ' "' + title + '"' ); - }); + //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel + if (!url) { + url = + MetacatUI.appModel.get("objectServiceUrl") || + MetacatUI.appModel.get("resolveServiceUrl"); + } - // Clear the input when the image is removed - view.stopListening(mdImageUploader, "removedfile"); - view.listenTo(mdImageUploader, "removedfile", function(){ - imageDialogInput.val(""); - }); + url = url + dataONEObject.get("id"); - // Render the image uploader and insert it just after the upload - // instructions in the image upload dialog box. - mdImageUploader.render(); - // The instructions for uploading in image that displays in the prompt/dialog - imageDialogDescription.text("Click or drag & drop to upload an image") - $(mdImageUploader.el).insertAfter(imageDialogDescription); - // Hide the input box for now, to keep the uploader simple - imageDialogInput.hide(); + // Create title out of file name without extension. + var title = dataONEObject.get("fileName"); + if (title && title.lastIndexOf(".") > 0) { + title = title.substring(0, title.lastIndexOf(".")); + } - } catch (e) { - console.log("Failed to load the UI for adding markdown images. Error: " + e); - } + // Add the url + title to the input + imageDialogInput.val(url + ' "' + title + '"'); + }, + ); - }, + // Clear the input when the image is removed + view.stopListening(mdImageUploader, "removedfile"); + view.listenTo(mdImageUploader, "removedfile", function () { + imageDialogInput.val(""); + }); - /** - * strikethrough - Add or remove the markdown syntax for strike through to - * the textarea. If there is text selected, then strike through formatting - * will be added or removed from that selection. If no selection, - * some placeholder text will be added surrounded by the strikethrough - * delimiters. - * - * @param {event} e is the original event object - * @param {string} mode can be markdown, html, or wysiwyg - * @param {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks - */ - strikethrough: function(e, mode, chunks){ - try { - var markup = "~~"; - // exactly two tiles - var tildes = '\\~{2}'; - // 2 tildes at the start of a string - var rleading = /^(\~{2})/; - // 2 tildes at the end of a string - var rtrailing = /(\~{2}$)/; - // 0-1 spaces at the end of a string - var rtrailingspace = /(\s?)$/; - // 2+ line breaks - var rnewlines = /\n{2,}/g; - // the text to add when no text is selected - var placeholder = "strikethrough text"; - - // Remove leading & trailing white space from selection - // (but do not remove from the user's text) - chunks.trim(); - // Replace 2+ consecutive line breaks with 1 linebreak, otherwise - // strikethrough syntax is incorrect and won't render HTML as expected - chunks.selection = chunks.selection.replace(rnewlines, '\n'); - - // See if the text before or after already contains ~~ at the start/end - var leadTildes = rtrailing.exec(chunks.before); - var trailTildes = rleading.exec(chunks.after); - // See if the selected text already contains ~~ at start or end - var selectLeadTildes = rleading.exec(chunks.selection); - var selectTrailTildes = rtrailing.exec(chunks.selection); - - // If the selection is already surrounded by ~~, remove them - if(leadTildes && trailTildes){ - chunks.before = chunks.before.replace(rtrailing, ""); - chunks.after = chunks.after.replace(rleading, ""); - // If the selection starts & ends with ~~, remove them - } else if (selectLeadTildes && selectTrailTildes) { - chunks.selection = chunks.selection.replace(rleading, ""); - chunks.selection = chunks.selection.replace(rtrailing, ""); - // Otherwise, add a set of ~~ - } else { - chunks.before = chunks.before + markup; - chunks.after = markup + chunks.after; - // Add the placeholder text if there was no selection - if (chunks.selection.length <= 0){ - chunks.selection = placeholder + // Render the image uploader and insert it just after the upload + // instructions in the image upload dialog box. + mdImageUploader.render(); + // The instructions for uploading in image that displays in the prompt/dialog + imageDialogDescription.text( + "Click or drag & drop to upload an image", + ); + $(mdImageUploader.el).insertAfter(imageDialogDescription); + // Hide the input box for now, to keep the uploader simple + imageDialogInput.hide(); + } catch (e) { + console.log( + "Failed to load the UI for adding markdown images. Error: " + e, + ); + } + }, + + /** + * strikethrough - Add or remove the markdown syntax for strike through to + * the textarea. If there is text selected, then strike through formatting + * will be added or removed from that selection. If no selection, + * some placeholder text will be added surrounded by the strikethrough + * delimiters. + * + * @param {event} e is the original event object + * @param {string} mode can be markdown, html, or wysiwyg + * @param {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks + */ + strikethrough: function (e, mode, chunks) { + try { + var markup = "~~"; + // exactly two tiles + var tildes = "\\~{2}"; + // 2 tildes at the start of a string + var rleading = /^(\~{2})/; + // 2 tildes at the end of a string + var rtrailing = /(\~{2}$)/; + // 0-1 spaces at the end of a string + var rtrailingspace = /(\s?)$/; + // 2+ line breaks + var rnewlines = /\n{2,}/g; + // the text to add when no text is selected + var placeholder = "strikethrough text"; + + // Remove leading & trailing white space from selection + // (but do not remove from the user's text) + chunks.trim(); + // Replace 2+ consecutive line breaks with 1 linebreak, otherwise + // strikethrough syntax is incorrect and won't render HTML as expected + chunks.selection = chunks.selection.replace(rnewlines, "\n"); + + // See if the text before or after already contains ~~ at the start/end + var leadTildes = rtrailing.exec(chunks.before); + var trailTildes = rleading.exec(chunks.after); + // See if the selected text already contains ~~ at start or end + var selectLeadTildes = rleading.exec(chunks.selection); + var selectTrailTildes = rtrailing.exec(chunks.selection); + + // If the selection is already surrounded by ~~, remove them + if (leadTildes && trailTildes) { + chunks.before = chunks.before.replace(rtrailing, ""); + chunks.after = chunks.after.replace(rleading, ""); + // If the selection starts & ends with ~~, remove them + } else if (selectLeadTildes && selectTrailTildes) { + chunks.selection = chunks.selection.replace(rleading, ""); + chunks.selection = chunks.selection.replace(rtrailing, ""); + // Otherwise, add a set of ~~ + } else { + chunks.before = chunks.before + markup; + chunks.after = markup + chunks.after; + // Add the placeholder text if there was no selection + if (chunks.selection.length <= 0) { + chunks.selection = placeholder; + } } + } catch (e) { + console.log( + "Failed to add or remove strikethrough formatting from markdown. Error: " + + e, + ); } - } catch (e) { - console.log("Failed to add or remove strikethrough formatting from markdown. Error: " + e ); - } - }, - - /** - * updateMarkdown - Update the markdown attribute in this view using the - * value of the markdown textarea - */ - updateMarkdown: function(){ - try { - - newMarkdown = this.$(this.textarea).val(); - - // The markdown attribute in the model may be a string or an array of strings. - // Although EML211 can comprise an array of markdown elements, - // this view will only edit the first if there are multiple. - if(Array.isArray(this.model.get("markdown")) && markdown.length){ - // Clone then update arary before setting it on the model - // so that the backbone "change" event is fired. - // See https://stackoverflow.com/a/10240697 - var newMarkdownArray = _.clone(this.model.get("markdown")); - newMarkdownArray[0] = newMarkdown; - this.model.set("markdown", newMarkdownArray); - } else { - this.model.set("markdown", newMarkdown) + }, + + /** + * updateMarkdown - Update the markdown attribute in this view using the + * value of the markdown textarea + */ + updateMarkdown: function () { + try { + newMarkdown = this.$(this.textarea).val(); + + // The markdown attribute in the model may be a string or an array of strings. + // Although EML211 can comprise an array of markdown elements, + // this view will only edit the first if there are multiple. + if (Array.isArray(this.model.get("markdown")) && markdown.length) { + // Clone then update arary before setting it on the model + // so that the backbone "change" event is fired. + // See https://stackoverflow.com/a/10240697 + var newMarkdownArray = _.clone(this.model.get("markdown")); + newMarkdownArray[0] = newMarkdown; + this.model.set("markdown", newMarkdownArray); + } else { + this.model.set("markdown", newMarkdown); + } + } catch (e) { + console.log("Failed to the view's markdown attribute, error: " + e); } - } catch (e) { - console.log("Failed to the view's markdown attribute, error: " + e); - } - }, - - /** - * previewMarkdown - render the markdown preview. - */ - previewMarkdown: function(){ + }, + + /** + * previewMarkdown - render the markdown preview. + */ + previewMarkdown: function () { + try { + var markdown = this.model.get("markdown"); + if (Array.isArray(markdown)) { + markdown = markdown[0]; + } - try{ + var markdownPreview = new MarkdownView({ + markdown: markdown || this.previewPlaceholder, + showTOC: this.showTOC || false, + }); - var markdown = this.model.get("markdown") - if(Array.isArray(markdown)){ - markdown = markdown[0] + // Render the preview + markdownPreview.render(); + // Add the rendered markdown to the preview tab + this.$("#markdown-preview-" + this.cid).html(markdownPreview.el); + } catch (e) { + console.log( + "Failed to preview markdown content. Error message: " + e, + ); } - - var markdownPreview = new MarkdownView({ - markdown: markdown || this.previewPlaceholder, - showTOC: this.showTOC || false - }); - - // Render the preview - markdownPreview.render(); - // Add the rendered markdown to the preview tab - this.$("#markdown-preview-"+this.cid).html(markdownPreview.el); - } - - catch(e){ - console.log("Failed to preview markdown content. Error message: " + e); - } - + }, }, - - }); + ); return MarkdownEditorView; - });
diff --git a/docs/docs/src_js_views_MarkdownView.js.html b/docs/docs/src_js_views_MarkdownView.js.html index 27d74677d..7ba787d97 100644 --- a/docs/docs/src_js_views_MarkdownView.js.html +++ b/docs/docs/src_js_views_MarkdownView.js.html @@ -44,392 +44,400 @@

Source: src/js/views/MarkdownView.js

-
define([    "jquery", "underscore", "backbone",
-            "showdown",
-            "text!templates/markdown.html",
-            "text!templates/loading.html"
-        ],
-
-    function($, _, Backbone,
-        showdown,
-        markdownTemplate,
-        LoadingTemplate ){
-
-    /**
-    * @class MarkdownView
-    * @classdesc A view of markdown content rendered into HTML with optional table of contents
-    * @classcategory Views
-    * @extends Backbone.View
-    * @constructor
-    */
-    var MarkdownView = Backbone.View.extend(
-      /** @lends MarkdownView.prototype */{
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: "markdown",
-
-        /**
-        * The type of View this is
-        * @type {string}
-        * @readonly
-        */
-        type: "markdown",
-
-        /**
-         * Renders the compiled template into HTML
-         * @type {UnderscoreTemplate}
-         */
-        template: _.template(markdownTemplate),
-        loadingTemplate: _.template(LoadingTemplate),
-
-        /**
-        * Markdown to render into HTML
-        * @type {string}
-        */
-        markdown: "",
-
-        /**
-        * An array of literature cited
-        * @type {Array}
-        */
-        citations: [],
-
-        /**
-        * Indicates whether or not to render a table of contents for this view.
-        * If set to true, a table of contents will be shown if there two or more
-        * top-level headers are rendered from the markdown.
-        * @type {boolean}
-        */
-        showTOC: false,
-
-        /**
-        * The events this view will listen to and the associated function to
-        * call.
-        * @type {Object}
-        */
-        events: {
-        },
-
-        /**
-        * Initialize is executed when a new MarkdownView is created.
-        * @param {Object} options - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-            // highlightStyle = the name of the code syntax highlight style we
-            // want to use for showdown's highlight extension.
-            this.highlightStyle = "atom-one-light";
-
-            if(typeof options !== "undefined"){
-                this.markdown  = options.markdown  || "";
-                this.citations = options.citations || [];
-                this.showTOC   = options.showTOC   || false;
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "showdown",
+  "text!templates/markdown.html",
+  "text!templates/loading.html",
+], function ($, _, Backbone, showdown, markdownTemplate, LoadingTemplate) {
+  /**
+   * @class MarkdownView
+   * @classdesc A view of markdown content rendered into HTML with optional table of contents
+   * @classcategory Views
+   * @extends Backbone.View
+   * @constructor
+   */
+  var MarkdownView = Backbone.View.extend(
+    /** @lends MarkdownView.prototype */ {
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "markdown",
+
+      /**
+       * The type of View this is
+       * @type {string}
+       * @readonly
+       */
+      type: "markdown",
+
+      /**
+       * Renders the compiled template into HTML
+       * @type {UnderscoreTemplate}
+       */
+      template: _.template(markdownTemplate),
+      loadingTemplate: _.template(LoadingTemplate),
+
+      /**
+       * Markdown to render into HTML
+       * @type {string}
+       */
+      markdown: "",
+
+      /**
+       * An array of literature cited
+       * @type {Array}
+       */
+      citations: [],
+
+      /**
+       * Indicates whether or not to render a table of contents for this view.
+       * If set to true, a table of contents will be shown if there two or more
+       * top-level headers are rendered from the markdown.
+       * @type {boolean}
+       */
+      showTOC: false,
+
+      /**
+       * The events this view will listen to and the associated function to
+       * call.
+       * @type {Object}
+       */
+      events: {},
+
+      /**
+       * Initialize is executed when a new MarkdownView is created.
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        // highlightStyle = the name of the code syntax highlight style we
+        // want to use for showdown's highlight extension.
+        this.highlightStyle = "atom-one-light";
+
+        if (typeof options !== "undefined") {
+          this.markdown = options.markdown || "";
+          this.citations = options.citations || [];
+          this.showTOC = options.showTOC || false;
+        }
+      },
+
+      /**
+       * render - Renders the MarkdownView; converts markdown to HTML and
+       * displays it.
+       */
+      render: function () {
+        // Show a loading message while we render the markdown to HTML
+        this.$el.html(
+          this.loadingTemplate({
+            msg: "Retrieving content...",
+          }),
+        );
+
+        // Once required extensions are tested for and loaded, convert and
+        // append markdown
+        this.stopListening();
+        this.listenTo(
+          this,
+          "requiredExtensionsLoaded",
+          function (SDextensions) {
+            var converter = new showdown.Converter({
+              metadata: true,
+              simplifiedAutoLink: true,
+              customizedHeaderId: true,
+              parseImgDimension: true,
+              tables: true,
+              tablesHeaderId: true,
+              strikethrough: true,
+              tasklists: true,
+              emoji: true,
+              extensions: SDextensions,
+            });
+
+            // If there are citations in the markdown text, add it to the markdown
+            // so it gets rendered.
+            if (
+              _.contains(SDextensions, "showdown-citation") &&
+              this.citations.length
+            ) {
+              // Put the bibtex into the markdown so it can be processed by
+              // the showdown-citations extension.
+              this.markdown =
+                this.markdown + "\n<bibtex>" + this.citations + "</bibtex>";
             }
-        },
-
-        /**
-         * render - Renders the MarkdownView; converts markdown to HTML and
-         * displays it.
-         */
-        render: function () {
-
-            // Show a loading message while we render the markdown to HTML
-            this.$el.html(this.loadingTemplate({
-              msg: "Retrieving content..."
-            }));
-
-            // Once required extensions are tested for and loaded, convert and
-            // append markdown
-            this.stopListening();
-            this.listenTo(this, "requiredExtensionsLoaded", function(SDextensions){
-
-                var converter  = new showdown.Converter({
-                            metadata: true,
-                            simplifiedAutoLink:true,
-                            customizedHeaderId:true,
-                            parseImgDimension: true,
-                            tables:true,
-                            tablesHeaderId:true,
-                            strikethrough: true,
-                            tasklists: true,
-                            emoji: true,
-                            extensions: SDextensions
-                });
-
-                // If there are citations in the markdown text, add it to the markdown
-                // so it gets rendered.
-                if( _.contains(SDextensions, "showdown-citation") && this.citations.length ){
-                  // Put the bibtex into the markdown so it can be processed by
-                  // the showdown-citations extension.
-                  this.markdown = this.markdown + "\n<bibtex>" + this.citations + "</bibtex>";
-                }
-
-                try{
-                  // Use the Showdown converter to make HTML from the Markdown string
-                  htmlFromMD = converter.makeHtml( this.markdown );
-                }
-                // If there was a Showdown error, show an error message instead of the Markdown preview.
-                catch(e){
-                  //Create a temporary div to hold the error message
-                  var errorMsgTempContainer = document.createElement("div");
-                  //Create the error message
-                  MetacatUI.appView.showAlert("This content can't be displayed.",
-                    "alert-error",
-                    errorMsgTempContainer,
-                    {
-                      remove: false
-                    });
-                  // Get the inner HTML of the temporary div
-                  htmlFromMD = errorMsgTempContainer.innerHTML;
-                }
-
-                this.$el.html(this.template({ markdown: htmlFromMD }));
-
-                if( this.showTOC ){
-                  this.renderTOC();
-                }
-              
-                this.trigger("mdRendered");
 
-            });
+            try {
+              // Use the Showdown converter to make HTML from the Markdown string
+              htmlFromMD = converter.makeHtml(this.markdown);
+            } catch (e) {
+              // If there was a Showdown error, show an error message instead of the Markdown preview.
+              //Create a temporary div to hold the error message
+              var errorMsgTempContainer = document.createElement("div");
+              //Create the error message
+              MetacatUI.appView.showAlert(
+                "This content can't be displayed.",
+                "alert-error",
+                errorMsgTempContainer,
+                {
+                  remove: false,
+                },
+              );
+              // Get the inner HTML of the temporary div
+              htmlFromMD = errorMsgTempContainer.innerHTML;
+            }
 
-            // Detect which extensions we'll need
-            this.listRequiredExtensions( this.markdown );
-
-            return this;
-
-        },
-
-
-        /**
-         * listRequiredExtensions - test which extensions are needed, then load
-         * them
-         *
-         * @param  {string} markdown - The markdown string before it's converted
-         * into HTML
-         */
-        listRequiredExtensions: function(markdown){
-
-            var view = this;
-
-            // SDextensions lists the desired order* of all potentailly required showdown extensions (* order matters! )
-            var SDextensions = ["xssfilter", "katex", "highlight", "docbook",
-                                "showdown-htags", "bootstrap", "footnotes",
-                                "showdown-citation", "showdown-images"];
-
-            var numTestsTodo = SDextensions.length;
-
-            // Each time an extension is tested for (and loaded if required),
-            // updateExtensionList is called. When all tests are completed
-            // (numTestsTodo == 0), an event is triggered. When this event is
-            // triggered, markdown is converted and appended (see render)
-            var updateExtensionList = function(extensionName, required){
-
-                numTestsTodo = numTestsTodo - 1;
-
-                if(required == false){
-                    var n = SDextensions.indexOf(extensionName);
-                    SDextensions.splice(n, 1);
-                }
-
-                if(numTestsTodo == 0){
-                    view.trigger("requiredExtensionsLoaded", SDextensions);
-                }
-            };
-
-            // ================================================================
-            // Regular expressions used to test whether showdown
-            // extensions are required.
-            // NOTE: These expressions test the *markdown* and *not* the HTML
-
-            var regexHighlight  = new RegExp("`.*`"), // too general?
-                regexDocbook    = new RegExp("<(title|citetitle|emphasis|para|ulink|literallayout|itemizedlist|orderedlist|listitem|subscript|superscript).*>"),
-                regexFootnotes1     = /^\[\^([\d\w]+)\]:( |\n)((.+\n)*.+)$/m,
-                regexFootnotes2     = /^\[\^([\d\w]+)\]:\s*((\n+(\s{2,4}|\t).+)+)$/m,
-                regexFootnotes3     = /\[\^([\d\w]+)\]/m,
-                // test for all of the math/katex delimiters
-                regexKatex      = new RegExp("\\$\\$.*\\$\\$|\\~.*\\~|\\$.*\\$|```asciimath.*```|```latex.*```"),
-                regexCitation   = /\[@.+\]/;
-                // test for any <h.> tags
-                regexHtags      = new RegExp('#\\s'),
-                regexImages     = /!\[.*\]\(\S+\)/;
-
-            // ================================================================
-            // Test for and load each as required each showdown extension
-
-            // --- Test for XSS --- //
-
-            // There is no test for the xss filter because it should always be
-            // included. It's included via the updateExtensionList function for
-            // consistency with the other, optional extensions.
-            require(["showdownXssFilter"], function(showdownXss){
-              updateExtensionList("xssfilter", required=true);
-            })
-
-            // --- Test for katex --- //
-
-            if( regexKatex.test(markdown) ){
-
-                require([
-                  "showdownKatex",
-                  "text!" + MetacatUI.root + "/components/showdown/extensions/showdown-katex/katex.min.css",
-                ], function(showdownKatex, showdownKatexCss){
-                    // custom config needed for katex
-                    var katex = showdownKatex({
-                        delimiters: [
-                            { left: "$", right: "$", display: false },
-                            { left: "$$", right: "$$", display: false},
-                            { left: '~', right: '~', display: false }
-                        ]
-                    });
-                    // Add CSS required to render katex math symbols correctly
-                    MetacatUI.appModel.addCSS(showdownKatexCss, "showdownKatex");
-                    // Because custom config, register katex with showdown
-                    showdown.extension("katex", katex);
-                    updateExtensionList("katex", required=true);
-                });
-
-            } else {
-                updateExtensionList("katex", required=false);
-            };
-
-
-            // --- Test for highlight --- //
-
-            if( regexHighlight.test(markdown) ){
-                require([
-                  "showdownHighlight",
-                  "text!" + MetacatUI.root + "/components/showdown/extensions/showdown-highlight/styles/atom-one-light.css"
-                ],
-                function(showdownHighlight, showdownHighlightCss){
-                  updateExtensionList("highlight", required=true);
-                  // CSS needed for highlight
-                  MetacatUI.appModel.addCSS(showdownHighlightCss, "showdownHighlight");
-                });
-            } else {
-                updateExtensionList("highlight", required=false);
-            };
-
-            // --- Test for docbooks --- //
-
-            if( regexDocbook.test(markdown) ){
-                require(["showdownDocbook"], function(showdownDocbook){
-                  updateExtensionList("docbook", required=true);
-                });
-            } else {
-                updateExtensionList("docbook", required=false);
-            };
-
-            // --- Test for htag --- //
-
-            if( regexHtags.test(markdown) ){
-                require(["showdownHtags"], function(showdownHtags){
-                   updateExtensionList("showdown-htags", required=true);
-                });
-            } else {
-                updateExtensionList("showdown-htags", required=false);
-            };
-
-
-            // --- Test for bootstrap --- //
-            // The custom bootstrap library is small and only adds some classes
-            // for tables and images, and maybe other HTML elements in the future.
-            // Testing for tables in markdown using regular expressions isn't
-            // straight forward. Better to just load this extension whether or
-            // not it's required.
-            require(["showdownBootstrap"], function(showdownBootstrap){
-                updateExtensionList("bootstrap", required=true);
-            });
+            this.$el.html(this.template({ markdown: htmlFromMD }));
+
+            if (this.showTOC) {
+              this.renderTOC();
+            }
 
-            // --- Test for footnotes --- //
-
-            if( regexFootnotes1.test(markdown) || regexFootnotes2.test(markdown) || regexFootnotes3.test(markdown) ){
-                require(["showdownFootnotes"], function(showdownFootnotes){
-                    updateExtensionList("footnotes", required=true);
-                });
-            } else {
-                updateExtensionList("footnotes", required=false);
-            };
-
-            // --- Test for citations --- //
-
-            // showdownCitation throws error...
-            if( regexCitation.test(markdown) ){
-                    require(["showdownCitation"], function(showdownCitation){
-                        updateExtensionList("showdown-citation", required=true);
-                    });
-                } else {
-                    updateExtensionList("showdown-citation", required=false);
-            };
-
-            // --- Test for images --- //
-            if( regexImages.test(markdown) ){
-                require(["showdownImages"], function(showdownImages){
-                    updateExtensionList("showdown-images", required=true);
-                });
-            } else {
-                updateExtensionList("showdown-images", required=false);
-            };
-
-        },
-
-
-        /**
-        * Renders a table of contents (a TOCView) that links to different sections of the MarkdownView
-        */
-        renderTOC: function(){
-
-          if(this.showTOC === false){
-            return
+            this.trigger("mdRendered");
+          },
+        );
+
+        // Detect which extensions we'll need
+        this.listRequiredExtensions(this.markdown);
+
+        return this;
+      },
+
+      /**
+       * listRequiredExtensions - test which extensions are needed, then load
+       * them
+       *
+       * @param  {string} markdown - The markdown string before it's converted
+       * into HTML
+       */
+      listRequiredExtensions: function (markdown) {
+        var view = this;
+
+        // SDextensions lists the desired order* of all potentailly required showdown extensions (* order matters! )
+        var SDextensions = [
+          "xssfilter",
+          "katex",
+          "highlight",
+          "docbook",
+          "showdown-htags",
+          "bootstrap",
+          "footnotes",
+          "showdown-citation",
+          "showdown-images",
+        ];
+
+        var numTestsTodo = SDextensions.length;
+
+        // Each time an extension is tested for (and loaded if required),
+        // updateExtensionList is called. When all tests are completed
+        // (numTestsTodo == 0), an event is triggered. When this event is
+        // triggered, markdown is converted and appended (see render)
+        var updateExtensionList = function (extensionName, required) {
+          numTestsTodo = numTestsTodo - 1;
+
+          if (required == false) {
+            var n = SDextensions.indexOf(extensionName);
+            SDextensions.splice(n, 1);
           }
 
-          var view = this;
+          if (numTestsTodo == 0) {
+            view.trigger("requiredExtensionsLoaded", SDextensions);
+          }
+        };
+
+        // ================================================================
+        // Regular expressions used to test whether showdown
+        // extensions are required.
+        // NOTE: These expressions test the *markdown* and *not* the HTML
+
+        var regexHighlight = new RegExp("`.*`"), // too general?
+          regexDocbook = new RegExp(
+            "<(title|citetitle|emphasis|para|ulink|literallayout|itemizedlist|orderedlist|listitem|subscript|superscript).*>",
+          ),
+          regexFootnotes1 = /^\[\^([\d\w]+)\]:( |\n)((.+\n)*.+)$/m,
+          regexFootnotes2 = /^\[\^([\d\w]+)\]:\s*((\n+(\s{2,4}|\t).+)+)$/m,
+          regexFootnotes3 = /\[\^([\d\w]+)\]/m,
+          // test for all of the math/katex delimiters
+          regexKatex = new RegExp(
+            "\\$\\$.*\\$\\$|\\~.*\\~|\\$.*\\$|```asciimath.*```|```latex.*```",
+          ),
+          regexCitation = /\[@.+\]/;
+        // test for any <h.> tags
+        (regexHtags = new RegExp("#\\s")), (regexImages = /!\[.*\]\(\S+\)/);
+
+        // ================================================================
+        // Test for and load each as required each showdown extension
+
+        // --- Test for XSS --- //
+
+        // There is no test for the xss filter because it should always be
+        // included. It's included via the updateExtensionList function for
+        // consistency with the other, optional extensions.
+        require(["showdownXssFilter"], function (showdownXss) {
+          updateExtensionList("xssfilter", (required = true));
+        });
+
+        // --- Test for katex --- //
+
+        if (regexKatex.test(markdown)) {
+          require([
+            "showdownKatex",
+            "text!" +
+              MetacatUI.root +
+              "/components/showdown/extensions/showdown-katex/katex.min.css",
+          ], function (showdownKatex, showdownKatexCss) {
+            // custom config needed for katex
+            var katex = showdownKatex({
+              delimiters: [
+                { left: "$", right: "$", display: false },
+                { left: "$$", right: "$$", display: false },
+                { left: "~", right: "~", display: false },
+              ],
+            });
+            // Add CSS required to render katex math symbols correctly
+            MetacatUI.appModel.addCSS(showdownKatexCss, "showdownKatex");
+            // Because custom config, register katex with showdown
+            showdown.extension("katex", katex);
+            updateExtensionList("katex", (required = true));
+          });
+        } else {
+          updateExtensionList("katex", (required = false));
+        }
 
-          require(["views/TOCView"], function(TOCView){
+        // --- Test for highlight --- //
+
+        if (regexHighlight.test(markdown)) {
+          require([
+            "showdownHighlight",
+            "text!" +
+              MetacatUI.root +
+              "/components/showdown/extensions/showdown-highlight/styles/atom-one-light.css",
+          ], function (showdownHighlight, showdownHighlightCss) {
+            updateExtensionList("highlight", (required = true));
+            // CSS needed for highlight
+            MetacatUI.appModel.addCSS(
+              showdownHighlightCss,
+              "showdownHighlight",
+            );
+          });
+        } else {
+          updateExtensionList("highlight", (required = false));
+        }
 
-            //Create a table of contents view
-            view.tocView = new TOCView({
-              contentEl: view.el,
-              className: "toc toc-view",
-              addScrollspy: true,
-              affix: true
-            });
+        // --- Test for docbooks --- //
 
-            view.tocView.render();
+        if (regexDocbook.test(markdown)) {
+          require(["showdownDocbook"], function (showdownDocbook) {
+            updateExtensionList("docbook", (required = true));
+          });
+        } else {
+          updateExtensionList("docbook", (required = false));
+        }
 
-            // If more than one link was created in the TOCView, add it to this
-            // view. Limit to `.desktop` items (i.e. exclude .mobile items) so
-            // that the length isn't doubled
-            if( view.tocView.$el.find(".desktop li").length > 1){
-              ($(view.tocView.el)).insertBefore(view.$el);
-              // Make a two-column layout
-              view.tocView.$el.addClass("span3");
-              view.$el.addClass("span9");
-            }
+        // --- Test for htag --- //
 
-            view.tocView.setAffix();
+        if (regexHtags.test(markdown)) {
+          require(["showdownHtags"], function (showdownHtags) {
+            updateExtensionList("showdown-htags", (required = true));
+          });
+        } else {
+          updateExtensionList("showdown-htags", (required = false));
+        }
 
+        // --- Test for bootstrap --- //
+        // The custom bootstrap library is small and only adds some classes
+        // for tables and images, and maybe other HTML elements in the future.
+        // Testing for tables in markdown using regular expressions isn't
+        // straight forward. Better to just load this extension whether or
+        // not it's required.
+        require(["showdownBootstrap"], function (showdownBootstrap) {
+          updateExtensionList("bootstrap", (required = true));
+        });
+
+        // --- Test for footnotes --- //
+
+        if (
+          regexFootnotes1.test(markdown) ||
+          regexFootnotes2.test(markdown) ||
+          regexFootnotes3.test(markdown)
+        ) {
+          require(["showdownFootnotes"], function (showdownFootnotes) {
+            updateExtensionList("footnotes", (required = true));
           });
+        } else {
+          updateExtensionList("footnotes", (required = false));
+        }
 
-        },
+        // --- Test for citations --- //
 
+        // showdownCitation throws error...
+        if (regexCitation.test(markdown)) {
+          require(["showdownCitation"], function (showdownCitation) {
+            updateExtensionList("showdown-citation", (required = true));
+          });
+        } else {
+          updateExtensionList("showdown-citation", (required = false));
+        }
 
-        /**
-         * onClose - Close and destroy the view
-         */
-        onClose: function() {
-            // Remove for the DOM, stop listening
-            this.remove();
-            // Remove appended html
-            this.$el.html("");
+        // --- Test for images --- //
+        if (regexImages.test(markdown)) {
+          require(["showdownImages"], function (showdownImages) {
+            updateExtensionList("showdown-images", (required = true));
+          });
+        } else {
+          updateExtensionList("showdown-images", (required = false));
+        }
+      },
+
+      /**
+       * Renders a table of contents (a TOCView) that links to different sections of the MarkdownView
+       */
+      renderTOC: function () {
+        if (this.showTOC === false) {
+          return;
         }
 
-    });
+        var view = this;
+
+        require(["views/TOCView"], function (TOCView) {
+          //Create a table of contents view
+          view.tocView = new TOCView({
+            contentEl: view.el,
+            className: "toc toc-view",
+            addScrollspy: true,
+            affix: true,
+          });
+
+          view.tocView.render();
+
+          // If more than one link was created in the TOCView, add it to this
+          // view. Limit to `.desktop` items (i.e. exclude .mobile items) so
+          // that the length isn't doubled
+          if (view.tocView.$el.find(".desktop li").length > 1) {
+            $(view.tocView.el).insertBefore(view.$el);
+            // Make a two-column layout
+            view.tocView.$el.addClass("span3");
+            view.$el.addClass("span9");
+          }
 
-    return MarkdownView;
+          view.tocView.setAffix();
+        });
+      },
+
+      /**
+       * onClose - Close and destroy the view
+       */
+      onClose: function () {
+        // Remove for the DOM, stop listening
+        this.remove();
+        // Remove appended html
+        this.$el.html("");
+      },
+    },
+  );
+
+  return MarkdownView;
 });
 
diff --git a/docs/docs/src_js_views_MdqRunView.js.html b/docs/docs/src_js_views_MdqRunView.js.html index 061252a6d..4afcb238a 100644 --- a/docs/docs/src_js_views_MdqRunView.js.html +++ b/docs/docs/src_js_views_MdqRunView.js.html @@ -44,159 +44,195 @@

Source: src/js/views/MdqRunView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'd3',
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "d3",
   "models/SolrResult",
-  'DonutChart', 'views/CitationView',
-  'text!templates/mdqRun.html', 'text!templates/mdqSuites.html', 'text!templates/loading-metrics.html', 'collections/QualityReport'],
-  function($, _, Backbone, d3,
-    SolrResult,
-    DonutChart, CitationView,
-    MdqRunTemplate, SuitesTemplate, LoadingTemplate, QualityReport) {
-  'use strict';
+  "DonutChart",
+  "views/CitationView",
+  "text!templates/mdqRun.html",
+  "text!templates/mdqSuites.html",
+  "text!templates/loading-metrics.html",
+  "collections/QualityReport",
+], function (
+  $,
+  _,
+  Backbone,
+  d3,
+  SolrResult,
+  DonutChart,
+  CitationView,
+  MdqRunTemplate,
+  SuitesTemplate,
+  LoadingTemplate,
+  QualityReport,
+) {
+  "use strict";
 
   /**
-  * @class MdqRunView
-  * @classdesc A view that fetches and displays a Metadata Assessment Report
-  * @classcategory Views
-  * @name MdqRunView
-  * @extends Backbone.View
-  * @constructs
-  */
+   * @class MdqRunView
+   * @classdesc A view that fetches and displays a Metadata Assessment Report
+   * @classcategory Views
+   * @name MdqRunView
+   * @extends Backbone.View
+   * @constructs
+   */
   var MdqRunView = Backbone.View.extend(
-    /** @lends MdqRunView.prototype */{
-
-    el: '#Content',
-
-    events: {
-      "change #suiteId" : "switchSuite"
-    },
-
-    url: null,
-    pid: null,
-    /**
-    * The currently selected/requested suite
-    * @type {string}
-    */
-    suiteId: null,
-    /**
-    * The list of all potential suites for this theme
-    * @type {string[]}
-    */
-    suiteIdList: [],
-    loadingTemplate: _.template(LoadingTemplate),
-    template: _.template(MdqRunTemplate),
-    breadcrumbContainer: "#breadcrumb-container",
-
-    /**
-     * A JQuery selector for the element in the template that will contain the loading
-     * image
-     * @type {string}
-     * @since 2.15.0
-     */
-    loadingContainer: "#mdqResult",
-
-    initialize: function () {
-
-    },
-
-    switchSuite: function(event) {
-      var select = $(event.target);
-      var suiteId = $(select).val();
-      MetacatUI.uiRouter.navigate("quality/s=" + suiteId + "/" + encodeURIComponent(this.pid), {trigger: false});
-      this.suiteId = suiteId;
-      this.render();
-      return false;
-    },
+    /** @lends MdqRunView.prototype */ {
+      el: "#Content",
+
+      events: {
+        "change #suiteId": "switchSuite",
+      },
+
+      url: null,
+      pid: null,
+      /**
+       * The currently selected/requested suite
+       * @type {string}
+       */
+      suiteId: null,
+      /**
+       * The list of all potential suites for this theme
+       * @type {string[]}
+       */
+      suiteIdList: [],
+      loadingTemplate: _.template(LoadingTemplate),
+      template: _.template(MdqRunTemplate),
+      breadcrumbContainer: "#breadcrumb-container",
+
+      /**
+       * A JQuery selector for the element in the template that will contain the loading
+       * image
+       * @type {string}
+       * @since 2.15.0
+       */
+      loadingContainer: "#mdqResult",
+
+      initialize: function () {},
+
+      switchSuite: function (event) {
+        var select = $(event.target);
+        var suiteId = $(select).val();
+        MetacatUI.uiRouter.navigate(
+          "quality/s=" + suiteId + "/" + encodeURIComponent(this.pid),
+          { trigger: false },
+        );
+        this.suiteId = suiteId;
+        this.render();
+        return false;
+      },
+
+      render: function () {
+        var viewRef = this;
+
+        // The suite use for rendering can initially be set via the theme AppModel.
+        // If a suite id is request via the metacatui route, then we have to display that
+        // suite, and in addition have to display all possible suites for this theme in
+        // a selection list, if the user wants to view a different one.
+        if (!this.suiteId) {
+          this.suiteId = MetacatUI.appModel.get("mdqSuiteIds")[0];
+        }
 
-    render: function () {
-
-      var viewRef = this;
-
-      // The suite use for rendering can initially be set via the theme AppModel.
-      // If a suite id is request via the metacatui route, then we have to display that
-      // suite, and in addition have to display all possible suites for this theme in
-      // a selection list, if the user wants to view a different one.
-      if (!this.suiteId) {
-        this.suiteId = MetacatUI.appModel.get("mdqSuiteIds")[0];
-      }
-
-      this.suiteIdList = MetacatUI.appModel.get("mdqSuiteIds");
-      this.suiteLabels = MetacatUI.appModel.get("mdqSuiteLabels");
-      //this.url = this.mdqRunsServiceUrl + "/" + this.suiteId + "/" + this.pid;
-
-      // Insert the basic template
-      this.$el.html(this.template({}));
-      // Show breadcrumbs leading back to the dataset & data search
-      this.insertBreadcrumbs();
-      // Insert the loading image
-      this.showLoading();
-
-      if (!this.pid){
-        var searchLink = $(document.createElement("a"))
-          .attr("href", MetacatUI.root + "/data")
-          .text("Search our database");
-        var message = $(document.createElement("span"))
-          .text(" to see an assessment report for a dataset")
-          .prepend(searchLink);
-        this.showMessage(message, true, false)
-        return
-      }
-
-      // Fetch a quality report from the quality server and display it.
-      var qualityUrl = MetacatUI.appModel.get("mdqRunsServiceUrl") + viewRef.suiteId + "/" + viewRef.pid;
-      var qualityReport = new QualityReport([], { url: qualityUrl, pid: viewRef.pid });
-      qualityReport.fetch({ url: qualityUrl });
-
-      this.listenToOnce(qualityReport, "fetchError", function() {
-        // Inspect the results to see if a quality report was returned.
-        // If not, then submit a request to the quality engine to create the
-        // quality report for this pid/suiteId, and inform the user of this.
-        var msgText;
-        console.log("Error status: " + qualityReport.fetchResponse.status);
-        if(qualityReport.fetchResponse.status == 404) {
-          msgText = "The assessment report for this dataset is not ready yet. Try checking back in 24 hours to see these results.";
-        } else {
-          msgText = "There was an error retrieving the assessment report for this dataset.";
-          if(typeof qualityReport.fetchResponse.statusText !== 'undefined' && typeof qualityReport.fetchResponse.status !== 'undefined') {
-            if(qualityReport.fetchResponse.status != 0)
-              msgText += "Error details: " + qualityReport.fetchResponse.statusText;
-          }
+        this.suiteIdList = MetacatUI.appModel.get("mdqSuiteIds");
+        this.suiteLabels = MetacatUI.appModel.get("mdqSuiteLabels");
+        //this.url = this.mdqRunsServiceUrl + "/" + this.suiteId + "/" + this.pid;
+
+        // Insert the basic template
+        this.$el.html(this.template({}));
+        // Show breadcrumbs leading back to the dataset & data search
+        this.insertBreadcrumbs();
+        // Insert the loading image
+        this.showLoading();
+
+        if (!this.pid) {
+          var searchLink = $(document.createElement("a"))
+            .attr("href", MetacatUI.root + "/data")
+            .text("Search our database");
+          var message = $(document.createElement("span"))
+            .text(" to see an assessment report for a dataset")
+            .prepend(searchLink);
+          this.showMessage(message, true, false);
+          return;
         }
-        this.showMessage(msgText);
-      }),
-
-      this.listenToOnce(qualityReport, "fetchComplete", function() {
-        var msgText;
-        if(qualityReport.runStatus != "success") {
-          if(qualityReport.runStatus == "failure") {
-              msgText = "There was an error generating the assessment report. The Assessment Server reported this error: " + qualityReport.errorDescription;
-          } else if (qualityReport.runStatus == "queued") {
-              msgText = "The assessment report is in the Assessment Server queue to be generated. It was queued at: " + qualityReport.timestamp;
+
+        // Fetch a quality report from the quality server and display it.
+        var qualityUrl =
+          MetacatUI.appModel.get("mdqRunsServiceUrl") +
+          viewRef.suiteId +
+          "/" +
+          viewRef.pid;
+        var qualityReport = new QualityReport([], {
+          url: qualityUrl,
+          pid: viewRef.pid,
+        });
+        qualityReport.fetch({ url: qualityUrl });
+
+        this.listenToOnce(qualityReport, "fetchError", function () {
+          // Inspect the results to see if a quality report was returned.
+          // If not, then submit a request to the quality engine to create the
+          // quality report for this pid/suiteId, and inform the user of this.
+          var msgText;
+          console.log("Error status: " + qualityReport.fetchResponse.status);
+          if (qualityReport.fetchResponse.status == 404) {
+            msgText =
+              "The assessment report for this dataset is not ready yet. Try checking back in 24 hours to see these results.";
           } else {
-              msgText = "There was an error retrieving the assessment report."
+            msgText =
+              "There was an error retrieving the assessment report for this dataset.";
+            if (
+              typeof qualityReport.fetchResponse.statusText !== "undefined" &&
+              typeof qualityReport.fetchResponse.status !== "undefined"
+            ) {
+              if (qualityReport.fetchResponse.status != 0)
+                msgText +=
+                  "Error details: " + qualityReport.fetchResponse.statusText;
+            }
           }
           this.showMessage(msgText);
-          return
-        } else {
-          viewRef.hideLoading();
-        }
-
-        // Filter out the checks with level 'METADATA', as these checks are intended
-        // to pass info to metadig-engine indexing (for search, faceting), and not intended for display.
-        qualityReport.reset(_.reject(qualityReport.models, function (model) {
-            var check = model.get("check");
-            if (check.level == "METADATA") {
-                return true
+        }),
+          this.listenToOnce(qualityReport, "fetchComplete", function () {
+            var msgText;
+            if (qualityReport.runStatus != "success") {
+              if (qualityReport.runStatus == "failure") {
+                msgText =
+                  "There was an error generating the assessment report. The Assessment Server reported this error: " +
+                  qualityReport.errorDescription;
+              } else if (qualityReport.runStatus == "queued") {
+                msgText =
+                  "The assessment report is in the Assessment Server queue to be generated. It was queued at: " +
+                  qualityReport.timestamp;
+              } else {
+                msgText =
+                  "There was an error retrieving the assessment report.";
+              }
+              this.showMessage(msgText);
+              return;
             } else {
-                return false;
+              viewRef.hideLoading();
             }
-        }));
 
-        var groupedResults = qualityReport.groupResults(qualityReport.models);
-        var groupedByType = qualityReport.groupByType(qualityReport.models);
+            // Filter out the checks with level 'METADATA', as these checks are intended
+            // to pass info to metadig-engine indexing (for search, faceting), and not intended for display.
+            qualityReport.reset(
+              _.reject(qualityReport.models, function (model) {
+                var check = model.get("check");
+                if (check.level == "METADATA") {
+                  return true;
+                } else {
+                  return false;
+                }
+              }),
+            );
 
-        var data = {
+            var groupedResults = qualityReport.groupResults(
+              qualityReport.models,
+            );
+            var groupedByType = qualityReport.groupByType(qualityReport.models);
+
+            var data = {
               objectIdentifier: qualityReport.id,
               suiteId: viewRef.suiteId,
               suiteIdList: viewRef.suiteIdList,
@@ -205,210 +241,259 @@ 

Source: src/js/views/MdqRunView.js

groupedByType: groupedByType, timestamp: _.now(), id: viewRef.pid, - checkCount: qualityReport.length - }; - - viewRef.$el.html(viewRef.template(data)); - viewRef.insertBreadcrumbs(); - viewRef.drawScoreChart(qualityReport.models, groupedResults); - viewRef.showCitation(); - viewRef.show(); - viewRef.$('.popover-this').popover(); - }); - - }, + checkCount: qualityReport.length, + }; + + viewRef.$el.html(viewRef.template(data)); + viewRef.insertBreadcrumbs(); + viewRef.drawScoreChart(qualityReport.models, groupedResults); + viewRef.showCitation(); + viewRef.show(); + viewRef.$(".popover-this").popover(); + }); + }, + + /** + * Updates the message in the loading image + * @param {string} message The new message to display + * @param {boolean} [showHelp=true] If set to true, and an email contact is configured + * in MetacatUI, then the contact email will be shown at the bottom of the message. + * @param {boolean} [showLink=true] If set to true, a link back to the dataset will be + * appended to the end of the message. + * @since 2.15.0 + */ + showMessage: function (message, showHelp = true, showLink = true) { + try { + var view = this; + var messageEl = this.loadingEl.find(".message"); + + if (!messageEl) { + return; + } - /** - * Updates the message in the loading image - * @param {string} message The new message to display - * @param {boolean} [showHelp=true] If set to true, and an email contact is configured - * in MetacatUI, then the contact email will be shown at the bottom of the message. - * @param {boolean} [showLink=true] If set to true, a link back to the dataset will be - * appended to the end of the message. - * @since 2.15.0 - */ - showMessage : function(message, showHelp = true, showLink = true){ - try { + // Update the message + messageEl.html(message); - var view = this; - var messageEl = this.loadingEl.find(".message"); + // Create a link back to the data set + if (showLink) { + var viewURL = "/view/" + encodeURIComponent(this.pid); + var backLink = $(document.createElement("a")).text( + " Return to the dataset", + ); + backLink.on("click", function () { + view.hideLoading(); + MetacatUI.uiRouter.navigate(viewURL, { + trigger: true, + replace: true, + }); + }); + messageEl.append(backLink); + } - if(!messageEl){ - return + // Show how the user can get more help + if (showHelp) { + var emailAddress = MetacatUI.appModel.get("emailContact"); + // Don't show help if there's no contact email configured + if (emailAddress) { + var helpEl = $( + "<p class='webmaster-email' style='margin-top:20px'>" + + "<i class='icon-envelope-alt icon icon-on-left'></i>" + + "Need help? Email us at </p>", + ); + var emailLink = $(document.createElement("a")) + .attr("href", "mailto:" + emailAddress) + .text(emailAddress); + helpEl.append(emailLink); + messageEl.append(helpEl); + } + } + } catch (error) { + console.log( + "There was an error showing a message in a MdqRunView" + + ". Error details: " + + error, + ); } - - // Update the message - messageEl.html(message); - - // Create a link back to the data set - if(showLink){ - var viewURL = "/view/" + encodeURIComponent(this.pid); - var backLink = $(document.createElement("a")) - .text(" Return to the dataset") - backLink.on("click", function(){ - view.hideLoading(); - MetacatUI.uiRouter.navigate(viewURL, { trigger: true, replace: true }); - }) - messageEl.append(backLink) + }, + + /** + * Render a loading image with message + */ + showLoading: function () { + try { + var loadingEl = this.loadingTemplate({ + message: "Retrieving assessment report...", + character: "none", + type: "barchart", + }); + this.loadingEl = $(loadingEl); + this.$el.find(this.loadingContainer).html(this.loadingEl); + } catch (error) { + console.log( + "There was an error showing the loading image in a MdqRunView" + + ". Error details: " + + error, + ); } - - // Show how the user can get more help - if(showHelp){ - var emailAddress = MetacatUI.appModel.get('emailContact') - // Don't show help if there's no contact email configured - if(emailAddress){ - var helpEl = $( - "<p class='webmaster-email' style='margin-top:20px'>" + - "<i class='icon-envelope-alt icon icon-on-left'></i>" + - "Need help? Email us at </p>" - ); - var emailLink = $(document.createElement("a")) - .attr("href", "mailto:" + emailAddress) - .text(emailAddress); - helpEl.append(emailLink) - messageEl.append(helpEl) - } + }, + + /** + * Remove the loading image and message. + */ + hideLoading: function () { + try { + this.loadingEl.remove(); + } catch (error) { + console.log( + "There was an error hiding a loading image in a MdqRunView" + + ". Error details: " + + error, + ); } - } + }, - catch (error) { - console.log( - 'There was an error showing a message in a MdqRunView' + - '. Error details: ' + error - ); - } - }, - - /** - * Render a loading image with message - */ - showLoading: function() { - try { - var loadingEl = this.loadingTemplate({ - message: "Retrieving assessment report...", - character: "none", - type: "barchart" + showCitation: function () { + var solrResultModel = new SolrResult({ + id: this.pid, }); - this.loadingEl = $(loadingEl) - this.$el.find(this.loadingContainer).html(this.loadingEl) - } - catch (error) { - console.log( - 'There was an error showing the loading image in a MdqRunView' + - '. Error details: ' + error - ); - } - }, - /** - * Remove the loading image and message. - */ - hideLoading: function() { - try { - this.loadingEl.remove(); - } - catch (error) { - console.log( - 'There was an error hiding a loading image in a MdqRunView' + - '. Error details: ' + error - ); - } - }, + this.listenTo(solrResultModel, "sync", function () { + var citationView = new CitationView({ + model: solrResultModel, + createLink: false, + createTitleLink: true, + }); - showCitation: function(){ + citationView.render(); - var solrResultModel = new SolrResult({ - id: this.pid - }); - - this.listenTo(solrResultModel, "sync", function(){ - var citationView = new CitationView({ - model: solrResultModel, - createLink: false, - createTitleLink: true + this.$("#mdqCitation").prepend(citationView.el); }); + solrResultModel.getInfo(); + }, - citationView.render(); - - this.$("#mdqCitation").prepend(citationView.el); - }); - solrResultModel.getInfo(); - - }, - - show: function() { - var view = this; - this.$el.hide(); - this.$el.fadeIn({duration: "slow"}); - }, - - drawScoreChart: function(results, groupedResults){ - - var dataCount = results.length; - var data = [ - {label: "Pass", count: groupedResults.GREEN.length, perc: groupedResults.GREEN.length/results.length }, - {label: "Warn", count: groupedResults.ORANGE.length, perc: groupedResults.ORANGE.length/results.length}, - {label: "Fail", count: groupedResults.RED.length, perc: groupedResults.RED.length/results.length}, - {label: "Info", count: groupedResults.BLUE.length, perc: groupedResults.BLUE.length/results.length}, - ]; - - var svgClass = "data"; - - //If d3 isn't supported in this browser or didn't load correctly, insert a text title instead - if(!d3){ - this.$('.format-charts-data').html("<h2 class='" + svgClass + " fallback'>" + MetacatUI.appView.commaSeparateNumber(dataCount) + " data files</h2>"); - - return; - } - - //Draw a donut chart - var donut = new DonutChart({ - id: "data-chart", - data: data, - total: dataCount, - titleText: "checks", - titleCount: dataCount, - svgClass: svgClass, - countClass: "data", - height: 250, - width: 250, - keepOrder: true, - formatLabel: function(name) { - return name; - } - }); - this.$('.format-charts-data').html(donut.render().el); - }, + show: function () { + var view = this; + this.$el.hide(); + this.$el.fadeIn({ duration: "slow" }); + }, + + drawScoreChart: function (results, groupedResults) { + var dataCount = results.length; + var data = [ + { + label: "Pass", + count: groupedResults.GREEN.length, + perc: groupedResults.GREEN.length / results.length, + }, + { + label: "Warn", + count: groupedResults.ORANGE.length, + perc: groupedResults.ORANGE.length / results.length, + }, + { + label: "Fail", + count: groupedResults.RED.length, + perc: groupedResults.RED.length / results.length, + }, + { + label: "Info", + count: groupedResults.BLUE.length, + perc: groupedResults.BLUE.length / results.length, + }, + ]; + + var svgClass = "data"; + + //If d3 isn't supported in this browser or didn't load correctly, insert a text title instead + if (!d3) { + this.$(".format-charts-data").html( + "<h2 class='" + + svgClass + + " fallback'>" + + MetacatUI.appView.commaSeparateNumber(dataCount) + + " data files</h2>", + ); + + return; + } - insertBreadcrumbs: function(){ - var breadcrumbs = $(document.createElement("ol")) - .addClass("breadcrumb") - .append($(document.createElement("li")) - .addClass("home") - .append($(document.createElement("a")) - .attr("href", MetacatUI.root? MetacatUI.root : "/") - .addClass("home") - .text("Home"))) - .append($(document.createElement("li")) - .addClass("search") - .append($(document.createElement("a")) - .attr("href", MetacatUI.root + "/data" + ((MetacatUI.appModel.get("page") > 0)? ("/page/" + (parseInt(MetacatUI.appModel.get("page"))+1)) : "")) - .addClass("search") - .text("Search"))) - .append($(document.createElement("li")) - .append($(document.createElement("a")) - .attr("href", MetacatUI.root + "/view/" + encodeURIComponent(this.pid)) - .addClass("inactive") - .text("Metadata"))) - .append($(document.createElement("li")) - .append($(document.createElement("a")) - .attr("href", MetacatUI.root + "/quality/" + encodeURIComponent(this.pid)) - .addClass("inactive") - .text("Assessment Report"))); - - this.$(this.breadcrumbContainer).html(breadcrumbs); + //Draw a donut chart + var donut = new DonutChart({ + id: "data-chart", + data: data, + total: dataCount, + titleText: "checks", + titleCount: dataCount, + svgClass: svgClass, + countClass: "data", + height: 250, + width: 250, + keepOrder: true, + formatLabel: function (name) { + return name; + }, + }); + this.$(".format-charts-data").html(donut.render().el); + }, + + insertBreadcrumbs: function () { + var breadcrumbs = $(document.createElement("ol")) + .addClass("breadcrumb") + .append( + $(document.createElement("li")) + .addClass("home") + .append( + $(document.createElement("a")) + .attr("href", MetacatUI.root ? MetacatUI.root : "/") + .addClass("home") + .text("Home"), + ), + ) + .append( + $(document.createElement("li")) + .addClass("search") + .append( + $(document.createElement("a")) + .attr( + "href", + MetacatUI.root + + "/data" + + (MetacatUI.appModel.get("page") > 0 + ? "/page/" + + (parseInt(MetacatUI.appModel.get("page")) + 1) + : ""), + ) + .addClass("search") + .text("Search"), + ), + ) + .append( + $(document.createElement("li")).append( + $(document.createElement("a")) + .attr( + "href", + MetacatUI.root + "/view/" + encodeURIComponent(this.pid), + ) + .addClass("inactive") + .text("Metadata"), + ), + ) + .append( + $(document.createElement("li")).append( + $(document.createElement("a")) + .attr( + "href", + MetacatUI.root + "/quality/" + encodeURIComponent(this.pid), + ) + .addClass("inactive") + .text("Assessment Report"), + ), + ); + + this.$(this.breadcrumbContainer).html(breadcrumbs); + }, }, - }); + ); return MdqRunView; });
diff --git a/docs/docs/src_js_views_MetadataView.js.html b/docs/docs/src_js_views_MetadataView.js.html index 20a52a896..2754f44e8 100644 --- a/docs/docs/src_js_views_MetadataView.js.html +++ b/docs/docs/src_js_views_MetadataView.js.html @@ -44,113 +44,147 @@

Source: src/js/views/MetadataView.js

-
/*global define */
-define(['jquery',
-  'jqueryui',
-  'underscore',
-  'backbone',
-  'gmaps',
-  'fancybox',
-  'clipboard',
-  'collections/DataPackage',
-  'models/DataONEObject',
-  'models/PackageModel',
-  'models/SolrResult',
-  'models/metadata/ScienceMetadata',
-  'models/MetricsModel',
-  'common/Utilities',
-  'views/DataPackageView',
-  'views/DownloadButtonView',
-  'views/ProvChartView',
-  'views/MetadataIndexView',
-  'views/ExpandCollapseListView',
-  'views/ProvStatementView',
-  'views/CitationHeaderView',
-  'views/citations/CitationModalView',
-  'views/AnnotationView',
-  'views/MarkdownView',
-  'text!templates/metadata/metadata.html',
-  'text!templates/dataSource.html',
-  'text!templates/publishDOI.html',
-  'text!templates/newerVersion.html',
-  'text!templates/loading.html',
-  'text!templates/metadataControls.html',
-  'text!templates/metadataInfoIcons.html',
-  'text!templates/alert.html',
-  'text!templates/editMetadata.html',
-  'text!templates/dataDisplay.html',
-  'text!templates/map.html',
-  'text!templates/annotation.html',
-  'text!templates/metaTagsHighwirePress.html',
-  'uuid',
-  'views/MetricView',
-],
-  function ($, $ui, _, Backbone, gmaps, fancybox, Clipboard, DataPackage, DataONEObject, Package, SolrResult, ScienceMetadata,
-    MetricsModel, Utilities, DataPackageView, DownloadButtonView, ProvChart, MetadataIndex, ExpandCollapseList, ProvStatement,
-    CitationHeaderView, CitationModalView, AnnotationView, MarkdownView, MetadataTemplate, DataSourceTemplate, PublishDoiTemplate,
-    VersionTemplate, LoadingTemplate, ControlsTemplate, MetadataInfoIconsTemplate, AlertTemplate, EditMetadataTemplate, DataDisplayTemplate,
-    MapTemplate, AnnotationTemplate, metaTagsHighwirePressTemplate, uuid, MetricView) {
-    'use strict';
-
-    /**
-    * @class MetadataView
-    * @classdesc A human-readable view of a science metadata file
-    * @classcategory Views
-    * @extends Backbone.View
-    * @constructor
-    * @screenshot views/MetadataView.png
-    */
-    var MetadataView = Backbone.View.extend(
-    /** @lends MetadataView.prototype */{
-
-        subviews: [],
-
-        pid: null,
-        seriesId: null,
-        saveProvPending: false,
-
-        model: new SolrResult(),
-        packageModels: new Array(),
-        entities: new Array(),
-        dataPackage: null,
-        dataPackageSynced: false,
-        el: '#Content',
-        metadataContainer: "#metadata-container",
-        citationContainer: "#citation-container",
-        tableContainer: "#table-container",
-        controlsContainer: "#metadata-controls-container",
-        metricsContainer: "#metrics-controls-container",
-        editorControlsContainer: "#editor-controls-container",
-        breadcrumbContainer: "#breadcrumb-container",
-        parentLinkContainer: "#parent-link-container",
-        dataSourceContainer: "#data-source-container",
-        articleContainer: "#article-container",
-
-        type: "Metadata",
-
-        //Templates
-        template: _.template(MetadataTemplate),
-        alertTemplate: _.template(AlertTemplate),
-        doiTemplate: _.template(PublishDoiTemplate),
-        versionTemplate: _.template(VersionTemplate),
-        loadingTemplate: _.template(LoadingTemplate),
-        controlsTemplate: _.template(ControlsTemplate),
-        infoIconsTemplate: _.template(MetadataInfoIconsTemplate),
-        dataSourceTemplate: _.template(DataSourceTemplate),
-        editMetadataTemplate: _.template(EditMetadataTemplate),
-        dataDisplayTemplate: _.template(DataDisplayTemplate),
-        mapTemplate: _.template(MapTemplate),
-        metaTagsHighwirePressTemplate: _.template(metaTagsHighwirePressTemplate),
-
-        objectIds: [],
-
-        /**
-         * Text to display in the help tooltip for the alternative identifier field,
-         * if the field is present.
-         * @type {string}
-         * @since 2.26.0
-         */
-        alternativeIdentifierHelpText: `
+            
define([
+  "jquery",
+  "jqueryui",
+  "underscore",
+  "backbone",
+  "gmaps",
+  "fancybox",
+  "clipboard",
+  "collections/DataPackage",
+  "models/DataONEObject",
+  "models/PackageModel",
+  "models/SolrResult",
+  "models/metadata/ScienceMetadata",
+  "models/MetricsModel",
+  "common/Utilities",
+  "views/DataPackageView",
+  "views/DownloadButtonView",
+  "views/ProvChartView",
+  "views/MetadataIndexView",
+  "views/ExpandCollapseListView",
+  "views/ProvStatementView",
+  "views/CitationHeaderView",
+  "views/citations/CitationModalView",
+  "views/AnnotationView",
+  "views/MarkdownView",
+  "text!templates/metadata/metadata.html",
+  "text!templates/dataSource.html",
+  "text!templates/publishDOI.html",
+  "text!templates/newerVersion.html",
+  "text!templates/loading.html",
+  "text!templates/metadataControls.html",
+  "text!templates/metadataInfoIcons.html",
+  "text!templates/alert.html",
+  "text!templates/editMetadata.html",
+  "text!templates/dataDisplay.html",
+  "text!templates/map.html",
+  "text!templates/annotation.html",
+  "text!templates/metaTagsHighwirePress.html",
+  "uuid",
+  "views/MetricView",
+], function (
+  $,
+  $ui,
+  _,
+  Backbone,
+  gmaps,
+  fancybox,
+  Clipboard,
+  DataPackage,
+  DataONEObject,
+  Package,
+  SolrResult,
+  ScienceMetadata,
+  MetricsModel,
+  Utilities,
+  DataPackageView,
+  DownloadButtonView,
+  ProvChart,
+  MetadataIndex,
+  ExpandCollapseList,
+  ProvStatement,
+  CitationHeaderView,
+  CitationModalView,
+  AnnotationView,
+  MarkdownView,
+  MetadataTemplate,
+  DataSourceTemplate,
+  PublishDoiTemplate,
+  VersionTemplate,
+  LoadingTemplate,
+  ControlsTemplate,
+  MetadataInfoIconsTemplate,
+  AlertTemplate,
+  EditMetadataTemplate,
+  DataDisplayTemplate,
+  MapTemplate,
+  AnnotationTemplate,
+  metaTagsHighwirePressTemplate,
+  uuid,
+  MetricView,
+) {
+  "use strict";
+
+  /**
+   * @class MetadataView
+   * @classdesc A human-readable view of a science metadata file
+   * @classcategory Views
+   * @extends Backbone.View
+   * @constructor
+   * @screenshot views/MetadataView.png
+   */
+  var MetadataView = Backbone.View.extend(
+    /** @lends MetadataView.prototype */ {
+      subviews: [],
+
+      pid: null,
+      seriesId: null,
+      saveProvPending: false,
+
+      model: new SolrResult(),
+      packageModels: new Array(),
+      entities: new Array(),
+      dataPackage: null,
+      dataPackageSynced: false,
+      el: "#Content",
+      metadataContainer: "#metadata-container",
+      citationContainer: "#citation-container",
+      tableContainer: "#table-container",
+      controlsContainer: "#metadata-controls-container",
+      metricsContainer: "#metrics-controls-container",
+      editorControlsContainer: "#editor-controls-container",
+      breadcrumbContainer: "#breadcrumb-container",
+      parentLinkContainer: "#parent-link-container",
+      dataSourceContainer: "#data-source-container",
+      articleContainer: "#article-container",
+
+      type: "Metadata",
+
+      //Templates
+      template: _.template(MetadataTemplate),
+      alertTemplate: _.template(AlertTemplate),
+      doiTemplate: _.template(PublishDoiTemplate),
+      versionTemplate: _.template(VersionTemplate),
+      loadingTemplate: _.template(LoadingTemplate),
+      controlsTemplate: _.template(ControlsTemplate),
+      infoIconsTemplate: _.template(MetadataInfoIconsTemplate),
+      dataSourceTemplate: _.template(DataSourceTemplate),
+      editMetadataTemplate: _.template(EditMetadataTemplate),
+      dataDisplayTemplate: _.template(DataDisplayTemplate),
+      mapTemplate: _.template(MapTemplate),
+      metaTagsHighwirePressTemplate: _.template(metaTagsHighwirePressTemplate),
+
+      objectIds: [],
+
+      /**
+       * Text to display in the help tooltip for the alternative identifier field,
+       * if the field is present.
+       * @type {string}
+       * @since 2.26.0
+       */
+      alternativeIdentifierHelpText: `
          An identifier used to reference this dataset in the past or in another
          system. This could be a link to the original dataset or an old
          identifier that was replaced. The referenced dataset may be the same
@@ -159,209 +193,214 @@ 

Source: src/js/views/MetadataView.js

history and evolution of the dataset. `, - // Delegated events for creating new items, and clearing completed ones. - events: { - "click #publish": "publish", - "mouseover .highlight-node": "highlightNode", - "mouseout .highlight-node": "highlightNode", - "click .preview": "previewData", - "click #save-metadata-prov": "saveProv" - }, - - - initialize: function (options) { - if ((options === undefined) || (!options)) var options = {}; - - this.pid = options.pid || options.id || MetacatUI.appModel.get("pid") || null; - - this.dataPackage = null; - - if (typeof options.el !== "undefined") - this.setElement(options.el); - - }, - - // Render the main metadata view - render: function () { - - this.stopListening(); - - MetacatUI.appModel.set('headerType', 'default'); - // this.showLoading("Loading..."); - - //Reset various properties of this view first - this.classMap = new Array(); - this.subviews = new Array(); - this.model.set(this.model.defaults); - this.packageModels = new Array(); - - // get the pid to render - if (!this.pid) - this.pid = MetacatUI.appModel.get("pid"); - - this.listenTo(MetacatUI.appUserModel, "change:loggedIn", this.render); - - //Listen to when the metadata has been rendered - this.once("metadataLoaded", function () { - this.createAnnotationViews(); - this.insertMarkdownViews(); - }); - - //Listen to when the package table has been rendered - this.once("dataPackageRendered", function () { - var packageTableContainer = this.$("#data-package-container"); - $(packageTableContainer).children(".loading").remove(); - - - //Scroll to the element on the page that is in the hash fragment (if there is one) - this.scrollToFragment(); - }); - - this.getModel(); - - return this; - }, - - /** - * Retrieve the resource map given its PID, and when it's fetched, - * check for write permissions, then check for private members in the package - * table view, if there is one. - * @param {string} pid - The PID of the resource map - */ - getDataPackage: function (pid) { - - //Create a DataONEObject model to use in the DataPackage collection. - var dataOneObject = new ScienceMetadata({ id: this.model.get("id") }); - - var view = this; - - // Create a new data package with this id - this.dataPackage = new DataPackage([dataOneObject], { id: pid }); - - this.dataPackage.mergeModels([this.model]); - - // If there is no resource map - if (!pid) { - // mark the data package as synced, - // since there are no other models to fetch - this.dataPackageSynced = true; - this.trigger("changed:dataPackageSynced"); - this.checkWritePermissions(); - return - } - - this.listenToOnce(this.dataPackage, "complete", function () { - this.dataPackageSynced = true; - this.trigger("changed:dataPackageSynced"); - var dataPackageView = _.findWhere(this.subviews, { type: "DataPackage" }); - if (dataPackageView) { - dataPackageView.dataPackageCollection = this.dataPackage; - dataPackageView.checkForPrivateMembers(); - } - - }); - - this.listenToOnce(this.dataPackage, "fetchFailed", function () { - view.dataPackageSynced = false; - - // stop listening to the fetch complete - view.stopListening(view.dataPackage, "complete"); - - //Remove the loading elements - view.$(view.tableContainer).find(".loading").remove(); + // Delegated events for creating new items, and clearing completed ones. + events: { + "click #publish": "publish", + "mouseover .highlight-node": "highlightNode", + "mouseout .highlight-node": "highlightNode", + "click .preview": "previewData", + "click #save-metadata-prov": "saveProv", + }, + + initialize: function (options) { + if (options === undefined || !options) var options = {}; + + this.pid = + options.pid || options.id || MetacatUI.appModel.get("pid") || null; + + this.dataPackage = null; + + if (typeof options.el !== "undefined") this.setElement(options.el); + }, + + // Render the main metadata view + render: function () { + this.stopListening(); + + MetacatUI.appModel.set("headerType", "default"); + // this.showLoading("Loading..."); + + //Reset various properties of this view first + this.classMap = new Array(); + this.subviews = new Array(); + this.model.set(this.model.defaults); + this.packageModels = new Array(); + + // get the pid to render + if (!this.pid) this.pid = MetacatUI.appModel.get("pid"); + + this.listenTo(MetacatUI.appUserModel, "change:loggedIn", this.render); + + //Listen to when the metadata has been rendered + this.once("metadataLoaded", function () { + this.createAnnotationViews(); + this.insertMarkdownViews(); + }); + + //Listen to when the package table has been rendered + this.once("dataPackageRendered", function () { + var packageTableContainer = this.$("#data-package-container"); + $(packageTableContainer).children(".loading").remove(); + + //Scroll to the element on the page that is in the hash fragment (if there is one) + this.scrollToFragment(); + }); + + this.getModel(); + + return this; + }, + + /** + * Retrieve the resource map given its PID, and when it's fetched, + * check for write permissions, then check for private members in the package + * table view, if there is one. + * @param {string} pid - The PID of the resource map + */ + getDataPackage: function (pid) { + //Create a DataONEObject model to use in the DataPackage collection. + var dataOneObject = new ScienceMetadata({ id: this.model.get("id") }); + + var view = this; + + // Create a new data package with this id + this.dataPackage = new DataPackage([dataOneObject], { id: pid }); + + this.dataPackage.mergeModels([this.model]); + + // If there is no resource map + if (!pid) { + // mark the data package as synced, + // since there are no other models to fetch + this.dataPackageSynced = true; + this.trigger("changed:dataPackageSynced"); + this.checkWritePermissions(); + return; + } - //Show an error message - MetacatUI.appView.showAlert( - "Error retrieving files for this data package.", - "alert-error", - view.$(view.tableContainer)); + this.listenToOnce(this.dataPackage, "complete", function () { + this.dataPackageSynced = true; + this.trigger("changed:dataPackageSynced"); + var dataPackageView = _.findWhere(this.subviews, { + type: "DataPackage", }); - - if (this.dataPackage.packageModel && this.dataPackage.packageModel.get("synced") === true) { + if (dataPackageView) { + dataPackageView.dataPackageCollection = this.dataPackage; + dataPackageView.checkForPrivateMembers(); + } + }); + + this.listenToOnce(this.dataPackage, "fetchFailed", function () { + view.dataPackageSynced = false; + + // stop listening to the fetch complete + view.stopListening(view.dataPackage, "complete"); + + //Remove the loading elements + view.$(view.tableContainer).find(".loading").remove(); + + //Show an error message + MetacatUI.appView.showAlert( + "Error retrieving files for this data package.", + "alert-error", + view.$(view.tableContainer), + ); + }); + + if ( + this.dataPackage.packageModel && + this.dataPackage.packageModel.get("synced") === true + ) { + this.checkWritePermissions(); + } else { + this.listenToOnce(this.dataPackage.packageModel, "sync", function () { this.checkWritePermissions(); - } else { - this.listenToOnce(this.dataPackage.packageModel, "sync", function () { - this.checkWritePermissions(); - }); - } - // Fetch the data package. DataPackage.parse() triggers 'complete' - this.dataPackage.fetch({ - fetchModels: false }); + } + // Fetch the data package. DataPackage.parse() triggers 'complete' + this.dataPackage.fetch({ + fetchModels: false, + }); + }, + + /* + * Retrieves information from the index about this object, given the id (passed from the URL) + * When the object info is retrieved from the index, we set up models depending on the type of object this is + */ + getModel: function (pid) { + //Get the pid and sid + if (typeof pid === "undefined" || !pid) var pid = this.pid; + if (typeof this.seriesId !== "undefined" && this.seriesId) + var sid = this.seriesId; + + //Get the package ID + this.model.set({ id: pid, seriesId: sid }); + var model = this.model; + + this.listenToOnce(model, "sync", function () { + if ( + this.model.get("formatType") == "METADATA" || + !this.model.get("formatType") + ) { + this.model = model; + this.renderMetadata(); + } else if (this.model.get("formatType") == "DATA") { + //Get the metadata pids that document this data object + var isDocBy = this.model.get("isDocumentedBy"); - }, - - /* - * Retrieves information from the index about this object, given the id (passed from the URL) - * When the object info is retrieved from the index, we set up models depending on the type of object this is - */ - getModel: function (pid) { - //Get the pid and sid - if ((typeof pid === "undefined") || !pid) var pid = this.pid; - if ((typeof this.seriesId !== "undefined") && this.seriesId) var sid = this.seriesId; - - //Get the package ID - this.model.set({ id: pid, seriesId: sid }); - var model = this.model; - - this.listenToOnce(model, "sync", function () { + //If there is only one metadata pid that documents this data object, then + // get that metadata model for this view. + if (isDocBy && isDocBy.length == 1) { + this.navigateWithFragment(_.first(isDocBy), this.pid); - if (this.model.get("formatType") == "METADATA" || !this.model.get("formatType")) { - this.model = model; - this.renderMetadata(); + return; } - else if (this.model.get("formatType") == "DATA") { - - //Get the metadata pids that document this data object - var isDocBy = this.model.get("isDocumentedBy"); - - //If there is only one metadata pid that documents this data object, then - // get that metadata model for this view. - if (isDocBy && isDocBy.length == 1) { - this.navigateWithFragment(_.first(isDocBy), this.pid); - - return; - } - //If more than one metadata doc documents this data object, it is most likely - // multiple versions of the same metadata. So we need to find the latest version. - else if (isDocBy && isDocBy.length > 1) { - - var view = this; - - require(["collections/Filters", "collections/SolrResults"], function (Filters, SolrResults) { - //Create a search for the metadata docs that document this data object - var searchFilters = new Filters([{ - values: isDocBy, - fields: ["id", "seriesId"], - operator: "OR", - fieldsOperator: "OR", - matchSubstring: false - }]), - //Create a list of search results - searchResults = new SolrResults([], { - rows: isDocBy.length, - query: searchFilters.getQuery(), - fields: "obsoletes,obsoletedBy,id" - }); - - //When the search results are returned, process those results - view.listenToOnce(searchResults, "sync", function (searchResults) { + //If more than one metadata doc documents this data object, it is most likely + // multiple versions of the same metadata. So we need to find the latest version. + else if (isDocBy && isDocBy.length > 1) { + var view = this; + + require([ + "collections/Filters", + "collections/SolrResults", + ], function (Filters, SolrResults) { + //Create a search for the metadata docs that document this data object + var searchFilters = new Filters([ + { + values: isDocBy, + fields: ["id", "seriesId"], + operator: "OR", + fieldsOperator: "OR", + matchSubstring: false, + }, + ]), + //Create a list of search results + searchResults = new SolrResults([], { + rows: isDocBy.length, + query: searchFilters.getQuery(), + fields: "obsoletes,obsoletedBy,id", + }); + //When the search results are returned, process those results + view.listenToOnce( + searchResults, + "sync", + function (searchResults) { //Keep track of the latest version of the metadata doc(s) var latestVersions = []; //Iterate over each search result and find the latest version of each metadata version chain searchResults.each(function (searchResult) { - //If this metadata isn't obsoleted by another object, it is the latest version if (!searchResult.get("obsoletedBy")) { latestVersions.push(searchResult.get("id")); } //If it is obsoleted by another object but that newer object does not document this data, then this is the latest version - else if (!_.contains(isDocBy, searchResult.get("obsoletedBy"))) { + else if ( + !_.contains(isDocBy, searchResult.get("obsoletedBy")) + ) { latestVersions.push(searchResult.get("id")); } - }, view); //If at least one latest version was found (should always be the case), @@ -375,389 +414,468 @@

Source: src/js/views/MetadataView.js

//If a latest version wasn't found, which should never happen, but just in case, default to the // last metadata pid in the isDocumentedBy field (most liekly to be the most recent since it was indexed last). else { - view.navigateWithFragment(_.last(isDocBy), view.pid) + view.navigateWithFragment(_.last(isDocBy), view.pid); } + }, + ); - }); - - //Send the query to the Solr search service - searchResults.query(); - }); + //Send the query to the Solr search service + searchResults.query(); + }); - return; - } - else { - this.noMetadata(this.model); - } + return; + } else { + this.noMetadata(this.model); } - else if (this.model.get("formatType") == "RESOURCE") { - var packageModel = new Package({ id: this.model.get("id") }); - packageModel.on("complete", function () { + } else if (this.model.get("formatType") == "RESOURCE") { + var packageModel = new Package({ id: this.model.get("id") }); + packageModel.on( + "complete", + function () { var metadata = packageModel.getMetadata(); if (!metadata) { this.noMetadata(packageModel); - } - else { + } else { this.model = metadata; this.pid = this.model.get("id"); this.renderMetadata(); if (this.model.get("resourceMap")) this.getPackageDetails(this.model.get("resourceMap")); } - }, this); - packageModel.getMembers(); - return; - } - - //Get the package information - this.getPackageDetails(model.get("resourceMap")); - - }); - - //Listen to 404 and 401 errors when we get the metadata object - this.listenToOnce(model, "404", this.showNotFound); - this.listenToOnce(model, "401", this.showIsPrivate); - - //Fetch the model - model.getInfo(); - - }, - - renderMetadata: function () { - var pid = this.model.get("id"); - - this.hideLoading(); - //Load the template which holds the basic structure of the view - this.$el.html(this.template()); - this.$(this.tableContainer).html(this.loadingTemplate({ - msg: "Retrieving data set details..." - })); - - //Insert the breadcrumbs - this.insertBreadcrumbs(); - //Insert the citation - this.insertCitation(); - //Insert the data source logo - this.insertDataSource(); - // is this the latest version? (includes DOI link when needed) - this.showLatestVersion(); - - // Insert various metadata controls in the page - this.insertControls(); - - // If we're displaying the metrics well then display copy citation and edit button - // inside the well - if (MetacatUI.appModel.get("displayDatasetMetrics")) { - //Insert Metrics Stats into the dataset landing pages - this.insertMetricsControls(); + }, + this, + ); + packageModel.getMembers(); + return; } - //Show loading icon in metadata section - this.$(this.metadataContainer).html(this.loadingTemplate({ msg: "Retrieving metadata ..." })); + //Get the package information + this.getPackageDetails(model.get("resourceMap")); + }); + + //Listen to 404 and 401 errors when we get the metadata object + this.listenToOnce(model, "404", this.showNotFound); + this.listenToOnce(model, "401", this.showIsPrivate); + + //Fetch the model + model.getInfo(); + }, + + renderMetadata: function () { + var pid = this.model.get("id"); + + this.hideLoading(); + //Load the template which holds the basic structure of the view + this.$el.html(this.template()); + this.$(this.tableContainer).html( + this.loadingTemplate({ + msg: "Retrieving data set details...", + }), + ); + + //Insert the breadcrumbs + this.insertBreadcrumbs(); + //Insert the citation + this.insertCitation(); + //Insert the data source logo + this.insertDataSource(); + // is this the latest version? (includes DOI link when needed) + this.showLatestVersion(); + + // Insert various metadata controls in the page + this.insertControls(); + + // If we're displaying the metrics well then display copy citation and edit button + // inside the well + if (MetacatUI.appModel.get("displayDatasetMetrics")) { + //Insert Metrics Stats into the dataset landing pages + this.insertMetricsControls(); + } - // Check for a view service in this MetacatUI.appModel - if ((MetacatUI.appModel.get('viewServiceUrl') !== undefined) && (MetacatUI.appModel.get('viewServiceUrl'))) - var endpoint = MetacatUI.appModel.get('viewServiceUrl') + encodeURIComponent(pid); + //Show loading icon in metadata section + this.$(this.metadataContainer).html( + this.loadingTemplate({ msg: "Retrieving metadata ..." }), + ); - if (endpoint && (typeof endpoint !== "undefined")) { - var viewRef = this; - var loadSettings = { - url: endpoint, - success: function (response, status, xhr) { - try { + // Check for a view service in this MetacatUI.appModel + if ( + MetacatUI.appModel.get("viewServiceUrl") !== undefined && + MetacatUI.appModel.get("viewServiceUrl") + ) + var endpoint = + MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(pid); - //If the user has navigated away from the MetadataView, then don't render anything further - if (MetacatUI.appView.currentView != viewRef) + if (endpoint && typeof endpoint !== "undefined") { + var viewRef = this; + var loadSettings = { + url: endpoint, + success: function (response, status, xhr) { + try { + //If the user has navigated away from the MetadataView, then don't render anything further + if (MetacatUI.appView.currentView != viewRef) return; + + //Our fallback is to show the metadata details from the Solr index + if ( + status == "error" || + !response || + typeof response !== "string" + ) + viewRef.renderMetadataFromIndex(); + else { + //Check for a response that is a 200 OK status, but is an error msg + if ( + response.length < 250 && + response.indexOf("Error transforming document") > -1 && + viewRef.model.get("indexed") + ) { + viewRef.renderMetadataFromIndex(); return; + } + //Mark this as a metadata doc with no stylesheet, or one that is at least different than usual EML and FGDC + else if (response.indexOf('id="Metadata"') == -1) { + viewRef.$el.addClass("container no-stylesheet"); - //Our fallback is to show the metadata details from the Solr index - if (status == "error" || !response || typeof response !== "string") - viewRef.renderMetadataFromIndex(); - else { - //Check for a response that is a 200 OK status, but is an error msg - if ((response.length < 250) && (response.indexOf("Error transforming document") > -1) && viewRef.model.get("indexed")) { + if (viewRef.model.get("indexed")) { viewRef.renderMetadataFromIndex(); return; } - //Mark this as a metadata doc with no stylesheet, or one that is at least different than usual EML and FGDC - else if ((response.indexOf('id="Metadata"') == -1)) { - viewRef.$el.addClass("container no-stylesheet"); - - if (viewRef.model.get("indexed")) { - viewRef.renderMetadataFromIndex(); - return; - } - } - - //Now show the response from the view service - viewRef.$(viewRef.metadataContainer).html(response); + } - viewRef.storeEntityPIDs(response); + //Now show the response from the view service + viewRef.$(viewRef.metadataContainer).html(response); - //If there is no info from the index and there is no metadata doc rendered either, then display a message - if (viewRef.$el.is(".no-stylesheet") && viewRef.model.get("archived") && !viewRef.model.get("indexed")) - viewRef.$(viewRef.metadataContainer).prepend(viewRef.alertTemplate({ msg: "There is limited metadata about this dataset since it has been archived." })); + viewRef.storeEntityPIDs(response); - viewRef.alterMarkup(); + //If there is no info from the index and there is no metadata doc rendered either, then display a message + if ( + viewRef.$el.is(".no-stylesheet") && + viewRef.model.get("archived") && + !viewRef.model.get("indexed") + ) + viewRef.$(viewRef.metadataContainer).prepend( + viewRef.alertTemplate({ + msg: "There is limited metadata about this dataset since it has been archived.", + }), + ); - viewRef.trigger("metadataLoaded"); + viewRef.alterMarkup(); - //Add a map of the spatial coverage - if (gmaps) viewRef.insertSpatialCoverageMap(); + viewRef.trigger("metadataLoaded"); - // Injects Clipboard objects into DOM elements returned from the View Service - viewRef.insertCopiables(); + //Add a map of the spatial coverage + if (gmaps) viewRef.insertSpatialCoverageMap(); - } - } catch (e) { - console.log("Error rendering metadata from the view service", e); - console.log("Response from the view service: ", response); - viewRef.renderMetadataFromIndex(); + // Injects Clipboard objects into DOM elements returned from the View Service + viewRef.insertCopiables(); } - }, - error: function (xhr, textStatus, errorThrown) { + } catch (e) { + console.log( + "Error rendering metadata from the view service", + e, + ); + console.log("Response from the view service: ", response); viewRef.renderMetadataFromIndex(); } - } - - $.ajax(_.extend(loadSettings, MetacatUI.appUserModel.createAjaxSettings())); - } - else this.renderMetadataFromIndex(); - - // Insert the Linked Data into the header of the page. - if (MetacatUI.appModel.get("isJSONLDEnabled")) { - var json = this.generateJSONLD(); - this.insertJSONLD(json); - } - - this.insertCitationMetaTags(); - }, - - /* If there is no view service available, then display the metadata fields from the index */ - renderMetadataFromIndex: function () { - var metadataFromIndex = new MetadataIndex({ - pid: this.pid, - parentView: this - }); - this.subviews.push(metadataFromIndex); - - //Add the metadata HTML - this.$(this.metadataContainer).html(metadataFromIndex.render().el); - - var view = this; - - this.listenTo(metadataFromIndex, "complete", function () { - //Add the package contents - view.insertPackageDetails(); + }, + error: function (xhr, textStatus, errorThrown) { + viewRef.renderMetadataFromIndex(); + }, + }; - //Add a map of the spatial coverage - if (gmaps) view.insertSpatialCoverageMap(); + $.ajax( + _.extend(loadSettings, MetacatUI.appUserModel.createAjaxSettings()), + ); + } else this.renderMetadataFromIndex(); - }); - }, + // Insert the Linked Data into the header of the page. + if (MetacatUI.appModel.get("isJSONLDEnabled")) { + var json = this.generateJSONLD(); + this.insertJSONLD(json); + } - removeCitation: function () { - var citation = "", - citationEl = null; + this.insertCitationMetaTags(); + }, - //Find the citation element - if (this.$(".citation").length > 0) { - //Get the text for the citation - citation = this.$(".citation").text(); + /* If there is no view service available, then display the metadata fields from the index */ + renderMetadataFromIndex: function () { + var metadataFromIndex = new MetadataIndex({ + pid: this.pid, + parentView: this, + }); + this.subviews.push(metadataFromIndex); - //Save this element in the view - citationEl = this.$(".citation"); - } - //Older versions of Metacat (v2.4.3 and older) will not have the citation class in the XSLT. Find the citation another way - else { - //Find the DOM element with the citation - var wells = this.$('.well'), - viewRef = this; + //Add the metadata HTML + this.$(this.metadataContainer).html(metadataFromIndex.render().el); - //Find the div.well with the citation. If we never find it, we don't insert the list of contents - _.each(wells, function (well) { - if (!citationEl && ($(well).find('#viewMetadataCitationLink').length > 0) || ($(well).children(".row-fluid > .span10 > a"))) { + var view = this; - //Save this element in the view - citationEl = well; + this.listenTo(metadataFromIndex, "complete", function () { + //Add the package contents + view.insertPackageDetails(); - //Mark this in the DOM for CSS styling - $(well).addClass('citation'); + //Add a map of the spatial coverage + if (gmaps) view.insertSpatialCoverageMap(); + }); + }, - //Save the text of the citation - citation = $(well).text(); - } - }); + removeCitation: function () { + var citation = "", + citationEl = null; - //Remove the unnecessary classes that are used in older versions of Metacat (2.4.3 and older) - var citationText = $(citationEl).find(".span10"); - $(citationText).removeClass("span10").addClass("span12"); - } + //Find the citation element + if (this.$(".citation").length > 0) { + //Get the text for the citation + citation = this.$(".citation").text(); - //Set the document title to the citation - MetacatUI.appModel.set("title", citation); + //Save this element in the view + citationEl = this.$(".citation"); + } + //Older versions of Metacat (v2.4.3 and older) will not have the citation class in the XSLT. Find the citation another way + else { + //Find the DOM element with the citation + var wells = this.$(".well"), + viewRef = this; + + //Find the div.well with the citation. If we never find it, we don't insert the list of contents + _.each(wells, function (well) { + if ( + (!citationEl && + $(well).find("#viewMetadataCitationLink").length > 0) || + $(well).children(".row-fluid > .span10 > a") + ) { + //Save this element in the view + citationEl = well; + + //Mark this in the DOM for CSS styling + $(well).addClass("citation"); + + //Save the text of the citation + citation = $(well).text(); + } + }); - citationEl.remove(); + //Remove the unnecessary classes that are used in older versions of Metacat (2.4.3 and older) + var citationText = $(citationEl).find(".span10"); + $(citationText).removeClass("span10").addClass("span12"); + } - }, + //Set the document title to the citation + MetacatUI.appModel.set("title", citation); - insertBreadcrumbs: function () { + citationEl.remove(); + }, - var breadcrumbs = $(document.createElement("ol")) - .addClass("breadcrumb") - .append($(document.createElement("li")) + insertBreadcrumbs: function () { + var breadcrumbs = $(document.createElement("ol")) + .addClass("breadcrumb") + .append( + $(document.createElement("li")) .addClass("home") - .append($(document.createElement("a")) - .attr("href", MetacatUI.root || "/") - .addClass("home") - .text("Home"))) - .append($(document.createElement("li")) + .append( + $(document.createElement("a")) + .attr("href", MetacatUI.root || "/") + .addClass("home") + .text("Home"), + ), + ) + .append( + $(document.createElement("li")) .addClass("search") - .append($(document.createElement("a")) - .attr("href", MetacatUI.root + "/data" + ((MetacatUI.appModel.get("page") > 0) ? ("/page/" + (parseInt(MetacatUI.appModel.get("page")) + 1)) : "")) - .addClass("search") - .text("Search"))) - .append($(document.createElement("li")) - .append($(document.createElement("a")) - .attr("href", MetacatUI.root + "/view/" + encodeURIComponent(this.pid)) + .append( + $(document.createElement("a")) + .attr( + "href", + MetacatUI.root + + "/data" + + (MetacatUI.appModel.get("page") > 0 + ? "/page/" + + (parseInt(MetacatUI.appModel.get("page")) + 1) + : ""), + ) + .addClass("search") + .text("Search"), + ), + ) + .append( + $(document.createElement("li")).append( + $(document.createElement("a")) + .attr( + "href", + MetacatUI.root + "/view/" + encodeURIComponent(this.pid), + ) .addClass("inactive") - .text("Metadata"))); - - if (MetacatUI.uiRouter.lastRoute() == "data") { - $(breadcrumbs).prepend($(document.createElement("a")) - .attr("href", MetacatUI.root + "/data/page/" + ((MetacatUI.appModel.get("page") > 0) ? (parseInt(MetacatUI.appModel.get("page")) + 1) : "")) + .text("Metadata"), + ), + ); + + if (MetacatUI.uiRouter.lastRoute() == "data") { + $(breadcrumbs).prepend( + $(document.createElement("a")) + .attr( + "href", + MetacatUI.root + + "/data/page/" + + (MetacatUI.appModel.get("page") > 0 + ? parseInt(MetacatUI.appModel.get("page")) + 1 + : ""), + ) .attr("title", "Back") .addClass("back") .text(" Back to search") - .prepend($(document.createElement("i")) - .addClass("icon-angle-left"))); - $(breadcrumbs).find("a.search").addClass("inactive"); - } - - this.$(this.breadcrumbContainer).html(breadcrumbs); - }, - - /* - * When the metadata object doesn't exist, display a message to the user - */ - showNotFound: function () { - - //If the model was found, exit this function - if (!this.model.get("notFound")) { - return; - } - - try { - //Check if a query string was in the URL and if so, try removing it in the identifier - if (this.model.get("id").match(/\?\S+\=\S+/g) && !this.findTries) { - let newID = this.model.get("id").replace(/\?\S+\=\S+/g, ""); - this.onClose(); - this.model.set("id", newID); - this.pid = newID; - this.findTries = 1; - this.render(); - return; - } - } - catch (e) { - console.warn("Caught error while determining query string", e); - } - - //Construct a message that shows this object doesn't exist - var msg = "<h4>Nothing was found.</h4>" + - "<p id='metadata-view-not-found-message'>The dataset identifier '" + Utilities.encodeHTML(this.model.get("id")) + "' " + - "does not exist or it may have been removed. <a>Search for " + - "datasets that mention " + Utilities.encodeHTML(this.model.get("id")) + "</a></p>"; - - //Remove the loading message - this.hideLoading(); - - //Show the not found error message - this.showError(msg); + .prepend( + $(document.createElement("i")).addClass("icon-angle-left"), + ), + ); + $(breadcrumbs).find("a.search").addClass("inactive"); + } - //Add the pid to the link href. Add via JS so it is Attribute-encoded to prevent XSS attacks - this.$("#metadata-view-not-found-message a").attr("href", MetacatUI.root + "/data/query=" + encodeURIComponent(this.model.get("id"))); - }, + this.$(this.breadcrumbContainer).html(breadcrumbs); + }, - /* - * When the metadata object is private, display a message to the user - */ - showIsPrivate: function () { + /* + * When the metadata object doesn't exist, display a message to the user + */ + showNotFound: function () { + //If the model was found, exit this function + if (!this.model.get("notFound")) { + return; + } - //If we haven't checked the logged-in status of the user yet, wait a bit - //until we show a 401 msg, in case this content is their private content - if (!MetacatUI.appUserModel.get("checked")) { - this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.showIsPrivate); + try { + //Check if a query string was in the URL and if so, try removing it in the identifier + if (this.model.get("id").match(/\?\S+\=\S+/g) && !this.findTries) { + let newID = this.model.get("id").replace(/\?\S+\=\S+/g, ""); + this.onClose(); + this.model.set("id", newID); + this.pid = newID; + this.findTries = 1; + this.render(); return; } + } catch (e) { + console.warn("Caught error while determining query string", e); + } - //If the user is logged in, the message will display that this dataset is private. - if (MetacatUI.appUserModel.get("loggedIn")) { - var msg = '<span class="icon-stack private tooltip-this" data-toggle="tooltip"' + - 'data-placement="top" data-container="#metadata-controls-container"' + - 'title="" data-original-title="This is a private dataset.">' + - '<i class="icon icon-circle icon-stack-base private"></i>' + - '<i class="icon icon-lock icon-stack-top"></i>' + - '</span> This is a private dataset.'; - } - //If the user isn't logged in, display a log in link. - else { - var msg = '<span class="icon-stack private tooltip-this" data-toggle="tooltip"' + - 'data-placement="top" data-container="#metadata-controls-container"' + - 'title="" data-original-title="This is a private dataset.">' + - '<i class="icon icon-circle icon-stack-base private"></i>' + - '<i class="icon icon-lock icon-stack-top"></i>' + - '</span> This is a private dataset. If you believe you have permission ' + - 'to access this dataset, then <a href="' + MetacatUI.root + - '/signin">sign in</a>.'; - } - - //Remove the loading message - this.hideLoading(); - - //Show the not found error message - this.showError(msg); + //Construct a message that shows this object doesn't exist + var msg = + "<h4>Nothing was found.</h4>" + + "<p id='metadata-view-not-found-message'>The dataset identifier '" + + Utilities.encodeHTML(this.model.get("id")) + + "' " + + "does not exist or it may have been removed. <a>Search for " + + "datasets that mention " + + Utilities.encodeHTML(this.model.get("id")) + + "</a></p>"; + + //Remove the loading message + this.hideLoading(); + + //Show the not found error message + this.showError(msg); + + //Add the pid to the link href. Add via JS so it is Attribute-encoded to prevent XSS attacks + this.$("#metadata-view-not-found-message a").attr( + "href", + MetacatUI.root + + "/data/query=" + + encodeURIComponent(this.model.get("id")), + ); + }, + + /* + * When the metadata object is private, display a message to the user + */ + showIsPrivate: function () { + //If we haven't checked the logged-in status of the user yet, wait a bit + //until we show a 401 msg, in case this content is their private content + if (!MetacatUI.appUserModel.get("checked")) { + this.listenToOnce( + MetacatUI.appUserModel, + "change:checked", + this.showIsPrivate, + ); + return; + } - }, + //If the user is logged in, the message will display that this dataset is private. + if (MetacatUI.appUserModel.get("loggedIn")) { + var msg = + '<span class="icon-stack private tooltip-this" data-toggle="tooltip"' + + 'data-placement="top" data-container="#metadata-controls-container"' + + 'title="" data-original-title="This is a private dataset.">' + + '<i class="icon icon-circle icon-stack-base private"></i>' + + '<i class="icon icon-lock icon-stack-top"></i>' + + "</span> This is a private dataset."; + } + //If the user isn't logged in, display a log in link. + else { + var msg = + '<span class="icon-stack private tooltip-this" data-toggle="tooltip"' + + 'data-placement="top" data-container="#metadata-controls-container"' + + 'title="" data-original-title="This is a private dataset.">' + + '<i class="icon icon-circle icon-stack-base private"></i>' + + '<i class="icon icon-lock icon-stack-top"></i>' + + "</span> This is a private dataset. If you believe you have permission " + + 'to access this dataset, then <a href="' + + MetacatUI.root + + '/signin">sign in</a>.'; + } - getPackageDetails: function (packageIDs) { + //Remove the loading message + this.hideLoading(); - var completePackages = 0; + //Show the not found error message + this.showError(msg); + }, - //This isn't a package, but just a lonely metadata doc... - if (!packageIDs || !packageIDs.length) { - var thisPackage = new Package({ id: null, members: [this.model] }); - thisPackage.flagComplete(); - this.packageModels = [thisPackage]; - this.insertPackageDetails(thisPackage, {disablePackageDownloads: true}); - } - else { - _.each(packageIDs, function (thisPackageID, i) { + getPackageDetails: function (packageIDs) { + var completePackages = 0; + //This isn't a package, but just a lonely metadata doc... + if (!packageIDs || !packageIDs.length) { + var thisPackage = new Package({ id: null, members: [this.model] }); + thisPackage.flagComplete(); + this.packageModels = [thisPackage]; + this.insertPackageDetails(thisPackage, { + disablePackageDownloads: true, + }); + } else { + _.each( + packageIDs, + function (thisPackageID, i) { //Create a model representing the data package var thisPackage = new Package({ id: thisPackageID }); //Listen for any parent packages - this.listenToOnce(thisPackage, "change:parentPackageMetadata", this.insertParentLink); + this.listenToOnce( + thisPackage, + "change:parentPackageMetadata", + this.insertParentLink, + ); //When the package info is fully retrieved - this.listenToOnce(thisPackage, 'complete', function (thisPackage) { - - //When all packages are fully retrieved - completePackages++; - if (completePackages >= packageIDs.length) { - - var latestPackages = _.filter(this.packageModels, function (m) { - return !_.contains(packageIDs, m.get("obsoletedBy")); - }); - - //Set those packages as the most recent package - this.packageModels = latestPackages; - - this.insertPackageDetails(latestPackages); - } - }); + this.listenToOnce( + thisPackage, + "complete", + function (thisPackage) { + //When all packages are fully retrieved + completePackages++; + if (completePackages >= packageIDs.length) { + var latestPackages = _.filter( + this.packageModels, + function (m) { + return !_.contains(packageIDs, m.get("obsoletedBy")); + }, + ); + + //Set those packages as the most recent package + this.packageModels = latestPackages; + + this.insertPackageDetails(latestPackages); + } + }, + ); //Save the package in the view this.packageModels.push(thisPackage); @@ -767,102 +885,110 @@

Source: src/js/views/MetadataView.js

//Get the members thisPackage.getMembers({ getParentMetadata: true }); - }, this); - } - }, - - alterMarkup: function () { - //Find the taxonomic range and give it a class for styling - for older versions of Metacat only (v2.4.3 and older) - if (!this.$(".taxonomicCoverage").length) - this.$('h4:contains("Taxonomic Range")').parent().addClass('taxonomicCoverage'); - - //Remove ecogrid links and replace them with workable links - this.replaceEcoGridLinks(); - - //Find the tab links for attribute names - this.$(".attributeListTable tr a").on('shown', function (e) { - //When the attribute link is clicked on, highlight the tab as active - $(e.target).parents(".attributeListTable").find(".active").removeClass("active"); - $(e.target).parents("tr").first().addClass("active"); + }, + this, + ); + } + }, + + alterMarkup: function () { + //Find the taxonomic range and give it a class for styling - for older versions of Metacat only (v2.4.3 and older) + if (!this.$(".taxonomicCoverage").length) + this.$('h4:contains("Taxonomic Range")') + .parent() + .addClass("taxonomicCoverage"); + + //Remove ecogrid links and replace them with workable links + this.replaceEcoGridLinks(); + + //Find the tab links for attribute names + this.$(".attributeListTable tr a").on("shown", function (e) { + //When the attribute link is clicked on, highlight the tab as active + $(e.target) + .parents(".attributeListTable") + .find(".active") + .removeClass("active"); + $(e.target).parents("tr").first().addClass("active"); + }); + + //Mark the first row in each attribute list table as active since the first attribute is displayed at first + this.$(".attributeListTable tr:first-child()").addClass("active"); + + // Add explanation text to the alternate identifier + this.renderAltIdentifierHelpText(); + }, + + /** + * Inserts an info icon next to the alternate identifier field, if it + * exists. The icon will display a tooltip with the help text for the + * field. + * @returns {jQuery} The jQuery object for the icon element. + * @since 2.26.0 + */ + renderAltIdentifierHelpText: function () { + try { + // Find the HTML element that contains the alternate identifier. + const altIdentifierLabel = this.$( + ".control-label:contains('Alternate Identifier')", + ); + + // It may not exist for all datasets. + if (!altIdentifierLabel.length) return; + + const text = this.alternativeIdentifierHelpText; + + if (!text) return; + + // Create the tooltip + const icon = $(document.createElement("i")) + .addClass("tooltip-this icon icon-info-sign") + .css("margin-left", "4px"); + + // Activate the jQuery tooltip plugin + icon.tooltip({ + title: text, + placement: "top", + container: "body", }); - //Mark the first row in each attribute list table as active since the first attribute is displayed at first - this.$(".attributeListTable tr:first-child()").addClass("active"); - - // Add explanation text to the alternate identifier - this.renderAltIdentifierHelpText(); - }, - - /** - * Inserts an info icon next to the alternate identifier field, if it - * exists. The icon will display a tooltip with the help text for the - * field. - * @returns {jQuery} The jQuery object for the icon element. - * @since 2.26.0 - */ - renderAltIdentifierHelpText: function () { - try { - // Find the HTML element that contains the alternate identifier. - const altIdentifierLabel = this - .$(".control-label:contains('Alternate Identifier')"); - - // It may not exist for all datasets. - if (!altIdentifierLabel.length) return; - - const text = this.alternativeIdentifierHelpText; - - if(!text) return; - - // Create the tooltip - const icon = $(document.createElement("i")) - .addClass("tooltip-this icon icon-info-sign") - .css("margin-left", "4px"); - - // Activate the jQuery tooltip plugin - icon.tooltip({ - title: text, - placement: "top", - container: "body" - }); - - // Add the icon to the label. - altIdentifierLabel.append(icon); - - return icon; - } catch (e) { - console.log("Error adding help text to alternate identifier", e); - } - - }, - - /* - * Inserts a table with all the data package member information and sends the call to display annotations - */ - insertPackageDetails: function (packages, options) { - if (typeof options === 'undefined') { - var options = {} - } - //Don't insert the package details twice - var view = this; - var tableEls = this.$(view.tableContainer).children().not(".loading"); - if (tableEls.length > 0) return; - - //wait for the metadata to load - var metadataEls = this.$(view.metadataContainer).children(); - if (!metadataEls.length || metadataEls.first().is(".loading")) { - this.once("metadataLoaded", function(){ - view.insertPackageDetails(this.packageModels, options); - }); - return; - } + // Add the icon to the label. + altIdentifierLabel.append(icon); - if (!packages) var packages = this.packageModels; + return icon; + } catch (e) { + console.log("Error adding help text to alternate identifier", e); + } + }, + + /* + * Inserts a table with all the data package member information and sends the call to display annotations + */ + insertPackageDetails: function (packages, options) { + if (typeof options === "undefined") { + var options = {}; + } + //Don't insert the package details twice + var view = this; + var tableEls = this.$(view.tableContainer).children().not(".loading"); + if (tableEls.length > 0) return; + + //wait for the metadata to load + var metadataEls = this.$(view.metadataContainer).children(); + if (!metadataEls.length || metadataEls.first().is(".loading")) { + this.once("metadataLoaded", function () { + view.insertPackageDetails(this.packageModels, options); + }); + return; + } - //Get the entity names from this page/metadata - this.getEntityNames(packages); + if (!packages) var packages = this.packageModels; - _.each(packages, function (packageModel) { + //Get the entity names from this page/metadata + this.getEntityNames(packages); + _.each( + packages, + function (packageModel) { //If the package model is not complete, don't do anything if (!packageModel.complete) return; @@ -872,31 +998,43 @@

Source: src/js/views/MetadataView.js

//If this metadata is not archived, filter out archived packages if (!this.model.get("archived")) { - nestedPckgsToDisplay = _.reject(nestedPckgs, function (pkg) { - return (pkg.get("archived")) + return pkg.get("archived"); }); - - } - else { + } else { //Display all packages is this metadata is archived nestedPckgsToDisplay = nestedPckgs; } if (nestedPckgsToDisplay.length > 0) { - - if (!(!this.model.get("archived") && packageModel.get("archived") == true)) { - var title = packageModel.get("id") ? '<span class="subtle">Package: ' + packageModel.get("id") + '</span>' : ""; + if ( + !( + !this.model.get("archived") && + packageModel.get("archived") == true + ) + ) { + var title = packageModel.get("id") + ? '<span class="subtle">Package: ' + + packageModel.get("id") + + "</span>" + : ""; options.title = "Files in this dataset " + title; options.nested = true; this.insertPackageTable(packageModel, options); } - } - else { - + } else { //If this metadata is not archived, then don't display archived packages - if (!(!this.model.get("archived") && packageModel.get("archived") == true)) { - var title = packageModel.get("id") ? '<span class="subtle">Package: ' + packageModel.get("id") + '</span>' : ""; + if ( + !( + !this.model.get("archived") && + packageModel.get("archived") == true + ) + ) { + var title = packageModel.get("id") + ? '<span class="subtle">Package: ' + + packageModel.get("id") + + "</span>" + : ""; options.title = "Files in this dataset " + title; this.insertPackageTable(packageModel, options); } @@ -904,396 +1042,504 @@

Source: src/js/views/MetadataView.js

//Remove the extra download button returned from the XSLT since the package table will have all the download links $("#downloadPackage").remove(); + }, + this, + ); + + //If this metadata doc is not in a package, but is just a lonely metadata doc... + if (!packages.length) { + var packageModel = new Package({ + members: [this.model], + }); + packageModel.complete = true; + options.title = "Files in this dataset"; + options.disablePackageDownloads = true; + this.insertPackageTable(packageModel, options); + } - }, this); - - //If this metadata doc is not in a package, but is just a lonely metadata doc... - if (!packages.length) { - var packageModel = new Package({ - members: [this.model], - }); - packageModel.complete = true; - options.title = "Files in this dataset"; - options.disablePackageDownloads = true; - this.insertPackageTable(packageModel, options); - } + //Insert the data details sections + this.insertDataDetails(); - //Insert the data details sections - this.insertDataDetails(); + // Get data package, if there is one, before checking write permissions + if (packages.length) { + this.getDataPackage(packages[0].get("id")); + } else { + // Otherwise go ahead and check write permissions on metadata only + this.checkWritePermissions(); + } - // Get data package, if there is one, before checking write permissions + try { + // Get the most recent package to display the provenance graphs if (packages.length) { - this.getDataPackage(packages[0].get("id")); - } else { - // Otherwise go ahead and check write permissions on metadata only - this.checkWritePermissions(); - } - - try { - // Get the most recent package to display the provenance graphs - if (packages.length) { - //Find the most recent Package model and fetch it - let mostRecentPackage = _.find(packages, p => !p.get("obsoletedBy")); - - //If all of the packages are obsoleted, then use the last package in the array, - // which is most likely the most recent. - /** @todo Use the DataONE version API to find the most recent package in the version chain */ - if (!mostRecentPackage) { - mostRecentPackage = packages[packages.length - 1]; - } - - //Get the data package only if it is not the same as the previously fetched package - if (mostRecentPackage.get("id") != packages[0].get("id")) - this.getDataPackage(mostRecentPackage.get("id")); + //Find the most recent Package model and fetch it + let mostRecentPackage = _.find( + packages, + (p) => !p.get("obsoletedBy"), + ); + + //If all of the packages are obsoleted, then use the last package in the array, + // which is most likely the most recent. + /** @todo Use the DataONE version API to find the most recent package in the version chain */ + if (!mostRecentPackage) { + mostRecentPackage = packages[packages.length - 1]; } - } - catch (e) { - console.error("Could not get the data package (prov will not be displayed, possibly other info as well).", e); - } - - //Initialize tooltips in the package table(s) - this.$(".tooltip-this").tooltip(); - - return this; - }, - insertPackageTable: function (packageModel, options) { - var view = this; - if (this.dataPackage == null || !this.dataPackageSynced) { - this.listenToOnce(this, "changed:dataPackageSynced", function(){ - view.insertPackageTable(packageModel, options); - }); - return; + //Get the data package only if it is not the same as the previously fetched package + if (mostRecentPackage.get("id") != packages[0].get("id")) + this.getDataPackage(mostRecentPackage.get("id")); } + } catch (e) { + console.error( + "Could not get the data package (prov will not be displayed, possibly other info as well).", + e, + ); + } - // Merge already fetched SolrResults into the dataPackage - if (typeof packageModel !== "undefined" && typeof packageModel.get("members") !== "undefined") { - this.dataPackage.mergeModels(packageModel.get("members")); - } + //Initialize tooltips in the package table(s) + this.$(".tooltip-this").tooltip(); - if (options) { - var title = options.title || ""; - var disablePackageDownloads = options.disablePackageDownloads || false; - var nested = (typeof options.nested === "undefined") ? false : options.nested; - } - else - var title = "", nested = false, disablePackageDownloads = false; - - //** Draw the package table **// - var tableView = new DataPackageView({ - edit: false, - dataPackage: this.dataPackage, - currentlyViewing: this.pid, - dataEntities: this.entities, - disablePackageDownloads: disablePackageDownloads, - parentView: this, - title: title, - packageTitle: this.model.get("title"), - nested: nested, - metricsModel: this.metricsModel + return this; + }, + + insertPackageTable: function (packageModel, options) { + var view = this; + if (this.dataPackage == null || !this.dataPackageSynced) { + this.listenToOnce(this, "changed:dataPackageSynced", function () { + view.insertPackageTable(packageModel, options); }); + return; + } - //Get the package table container - var tablesContainer = this.$(this.tableContainer); + // Merge already fetched SolrResults into the dataPackage + if ( + typeof packageModel !== "undefined" && + typeof packageModel.get("members") !== "undefined" + ) { + this.dataPackage.mergeModels(packageModel.get("members")); + } - //After the first table, start collapsing them - var numTables = $(tablesContainer).find("table.download-contents").length; - if (numTables == 1) { - var tableContainer = $(document.createElement("div")).attr("id", "additional-tables-for-" + this.cid); - tableContainer.hide(); - $(tablesContainer).append(tableContainer); - } - else if (numTables > 1) - var tableContainer = this.$("#additional-tables-for-" + this.cid); - else - var tableContainer = tablesContainer; + if (options) { + var title = options.title || ""; + var disablePackageDownloads = + options.disablePackageDownloads || false; + var nested = + typeof options.nested === "undefined" ? false : options.nested; + } else + var title = "", + nested = false, + disablePackageDownloads = false; + + //** Draw the package table **// + var tableView = new DataPackageView({ + edit: false, + dataPackage: this.dataPackage, + currentlyViewing: this.pid, + dataEntities: this.entities, + disablePackageDownloads: disablePackageDownloads, + parentView: this, + title: title, + packageTitle: this.model.get("title"), + nested: nested, + metricsModel: this.metricsModel, + }); + + //Get the package table container + var tablesContainer = this.$(this.tableContainer); + + //After the first table, start collapsing them + var numTables = $(tablesContainer).find( + "table.download-contents", + ).length; + if (numTables == 1) { + var tableContainer = $(document.createElement("div")).attr( + "id", + "additional-tables-for-" + this.cid, + ); + tableContainer.hide(); + $(tablesContainer).append(tableContainer); + } else if (numTables > 1) + var tableContainer = this.$("#additional-tables-for-" + this.cid); + else var tableContainer = tablesContainer; + + //Insert the package table HTML + $(tableContainer).empty(); + $(tableContainer).append(tableView.render().el); + + // Add Package Download + // create an instance of DownloadButtonView to handle package downloads + this.downloadButtonView = new DownloadButtonView({ + id: packageModel.get("id"), + model: packageModel, + view: "actionsView", + }); + + // render + this.downloadButtonView.render(); + + // add the downloadButtonView el to the span + $(this.tableContainer) + .find(".file-header .file-actions .downloadAction") + .html(this.downloadButtonView.el); + + $(this.tableContainer).find(".loading").remove(); + + $(tableContainer).find(".tooltip-this").tooltip(); + + this.subviews.push(tableView); + + //Trigger a custom event in this view that indicates the package table has been rendered + this.trigger("dataPackageRendered"); + }, + + insertParentLink: function (packageModel) { + var parentPackageMetadata = packageModel.get("parentPackageMetadata"), + view = this; + + _.each(parentPackageMetadata, function (m, i) { + var title = m.get("title"), + icon = $(document.createElement("i")).addClass( + "icon icon-on-left icon-level-up", + ), + link = $(document.createElement("a")) + .attr( + "href", + MetacatUI.root + "/view/" + encodeURIComponent(m.get("id")), + ) + .addClass("parent-link") + .text("Parent dataset: " + title) + .prepend(icon); + + view.$(view.parentLinkContainer).append(link); + }); + }, + + insertSpatialCoverageMap: function (customCoordinates) { + //Find the geographic region container. Older versions of Metacat (v2.4.3 and less) will not have it classified so look for the header text + if (!this.$(".geographicCoverage").length) { + //For EML + var title = this.$('h4:contains("Geographic Region")'); + + //For FGDC + if (title.length == 0) { + title = this.$('label:contains("Bounding Coordinates")'); + } + + var georegionEls = $(title).parent(); + var parseText = true; + var directions = new Array("North", "South", "East", "West"); + } else { + var georegionEls = this.$(".geographicCoverage"); + var directions = new Array("north", "south", "east", "west"); + } - //Insert the package table HTML - $(tableContainer).empty(); - $(tableContainer).append(tableView.render().el); + for (var i = 0; i < georegionEls.length; i++) { + var georegion = georegionEls[i]; - // Add Package Download - // create an instance of DownloadButtonView to handle package downloads - this.downloadButtonView = new DownloadButtonView({id: packageModel.get("id"), model: packageModel, view: "actionsView"}); + if (typeof customCoordinates !== "undefined") { + //Extract the coordinates + var n = customCoordinates[0]; + var s = customCoordinates[1]; + var e = customCoordinates[2]; + var w = customCoordinates[3]; + } else { + var coordinates = new Array(); + + _.each(directions, function (direction) { + //Parse text for older versions of Metacat (v2.4.3 and earlier) + if (parseText) { + var labelEl = $(georegion).find( + 'label:contains("' + direction + '")', + ); + if (labelEl.length) { + var coordinate = $(labelEl).next().html(); + if ( + typeof coordinate != "undefined" && + coordinate.indexOf("&nbsp;") > -1 + ) + coordinate = coordinate.substring( + 0, + coordinate.indexOf("&nbsp;"), + ); + } + } else { + var coordinate = $(georegion) + .find("." + direction + "BoundingCoordinate") + .attr("data-value"); + } - // render - this.downloadButtonView.render(); + //Save our coordinate value + coordinates.push(coordinate); + }); - // add the downloadButtonView el to the span - $(this.tableContainer).find('.file-header .file-actions .downloadAction').html(this.downloadButtonView.el); + //Extract the coordinates + var n = coordinates[0]; + var s = coordinates[1]; + var e = coordinates[2]; + var w = coordinates[3]; + } - $(this.tableContainer).find(".loading").remove(); + //Create Google Map LatLng objects out of our coordinates + var latLngSW = new gmaps.LatLng(s, w); + var latLngNE = new gmaps.LatLng(n, e); + var latLngNW = new gmaps.LatLng(n, w); + var latLngSE = new gmaps.LatLng(s, e); - $(tableContainer).find(".tooltip-this").tooltip(); + //Get the centertroid location of this data item + var bounds = new gmaps.LatLngBounds(latLngSW, latLngNE); + var latLngCEN = bounds.getCenter(); - this.subviews.push(tableView); + //If there isn't a center point found, don't draw the map. + if (typeof latLngCEN == "undefined") { + return; + } - //Trigger a custom event in this view that indicates the package table has been rendered - this.trigger("dataPackageRendered"); - }, + //Get the map path color + var pathColor = MetacatUI.appModel.get("datasetMapPathColor"); + if (pathColor) { + pathColor = "color:" + pathColor + "|"; + } else { + pathColor = ""; + } - insertParentLink: function (packageModel) { - var parentPackageMetadata = packageModel.get("parentPackageMetadata"), - view = this; + //Get the map path fill color + var fillColor = MetacatUI.appModel.get("datasetMapFillColor"); + if (fillColor) { + fillColor = "fillcolor:" + fillColor + "|"; + } else { + fillColor = ""; + } + + //Create a google map image + var mapHTML = + "<img class='georegion-map' " + + "src='https://maps.googleapis.com/maps/api/staticmap?" + + "center=" + + latLngCEN.lat() + + "," + + latLngCEN.lng() + + "&size=800x350" + + "&maptype=terrain" + + "&markers=size:mid|color:0xDA4D3Aff|" + + latLngCEN.lat() + + "," + + latLngCEN.lng() + + "&path=" + + fillColor + + pathColor + + "weight:3|" + + latLngSW.lat() + + "," + + latLngSW.lng() + + "|" + + latLngNW.lat() + + "," + + latLngNW.lng() + + "|" + + latLngNE.lat() + + "," + + latLngNE.lng() + + "|" + + latLngSE.lat() + + "," + + latLngSE.lng() + + "|" + + latLngSW.lat() + + "," + + latLngSW.lng() + + "&visible=" + + latLngSW.lat() + + "," + + latLngSW.lng() + + "|" + + latLngNW.lat() + + "," + + latLngNW.lng() + + "|" + + latLngNE.lat() + + "," + + latLngNE.lng() + + "|" + + latLngSE.lat() + + "," + + latLngSE.lng() + + "|" + + latLngSW.lat() + + "," + + latLngSW.lng() + + "&sensor=false" + + "&key=" + + MetacatUI.mapKey + + "'/>"; + + //Find the spot in the DOM to insert our map image + if (parseText) + var insertAfter = $(georegion) + .find('label:contains("West")') + .parent() + .parent().length + ? $(georegion).find('label:contains("West")').parent().parent() + : georegion; + //The last coordinate listed + else var insertAfter = georegion; + + // Get the URL to the interactive Google Maps instance + const url = this.getGoogleMapsUrl(latLngCEN, bounds); + + // Insert the map image + $(insertAfter).append( + this.mapTemplate({ + map: mapHTML, + url: url, + }), + ); + + $(".fancybox-media").fancybox({ + openEffect: "elastic", + closeEffect: "elastic", + helpers: { + media: {}, + }, + }); + } - _.each(parentPackageMetadata, function (m, i) { - var title = m.get("title"), - icon = $(document.createElement("i")).addClass("icon icon-on-left icon-level-up"), - link = $(document.createElement("a")).attr("href", MetacatUI.root + "/view/" + encodeURIComponent(m.get("id"))) - .addClass("parent-link") - .text("Parent dataset: " + title) - .prepend(icon); + return true; + }, + + /** + * Returns a URL to a Google Maps instance that is centered on the given + * coordinates and zoomed to the appropriate level to display the given + * bounding box. + * @param {LatLng} latLngCEN - The center point of the map. + * @param {LatLngBounds} bounds - The bounding box to display. + * @returns {string} The URL to the Google Maps instance. + * @since 2.27.0 + */ + getGoogleMapsUrl: function (latLngCEN, bounds) { + // Use the window width and height as a proxy for the map dimensions + const mapDim = { + height: $(window).height(), + width: $(window).width(), + }; + const z = this.getBoundsZoomLevel(bounds, mapDim); + const mapLat = latLngCEN.lat(); + const mapLng = latLngCEN.lng(); + + return `https://maps.google.com/?ll=${mapLat},${mapLng}&z=${z}`; + }, + + /** + * Returns the zoom level that will display the given bounding box at + * the given dimensions. + * @param {LatLngBounds} bounds - The bounding box to display. + * @param {Object} mapDim - The dimensions of the map. + * @param {number} mapDim.height - The height of the map. + * @param {number} mapDim.width - The width of the map. + * @returns {number} The zoom level. + * @since 2.27.0 + */ + getBoundsZoomLevel: function (bounds, mapDim) { + var WORLD_DIM = { height: 256, width: 256 }; + var ZOOM_MAX = 15; + // 21 is actual max, but any closer and the map is too zoomed in to be + // useful + + function latRad(lat) { + var sin = Math.sin((lat * Math.PI) / 180); + var radX2 = Math.log((1 + sin) / (1 - sin)) / 2; + return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2; + } - view.$(view.parentLinkContainer).append(link); - }); + function zoom(mapPx, worldPx, fraction) { + return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2); + } - }, + var ne = bounds.getNorthEast(); + var sw = bounds.getSouthWest(); - insertSpatialCoverageMap: function (customCoordinates) { + var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI; - //Find the geographic region container. Older versions of Metacat (v2.4.3 and less) will not have it classified so look for the header text - if (!this.$(".geographicCoverage").length) { - //For EML - var title = this.$('h4:contains("Geographic Region")'); + var lngDiff = ne.lng() - sw.lng(); + var lngFraction = (lngDiff < 0 ? lngDiff + 360 : lngDiff) / 360; - //For FGDC - if (title.length == 0) { - title = this.$('label:contains("Bounding Coordinates")'); - } + var latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction); + var lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction); - var georegionEls = $(title).parent(); - var parseText = true; - var directions = new Array('North', 'South', 'East', 'West'); - } - else { - var georegionEls = this.$(".geographicCoverage"); - var directions = new Array('north', 'south', 'east', 'west'); - } + return Math.min(latZoom, lngZoom, ZOOM_MAX); + }, - for (var i = 0; i < georegionEls.length; i++) { - var georegion = georegionEls[i]; + insertCitation: function () { + if (!this.model) return false; + //Create a citation header element from the model attributes + var header = new CitationHeaderView({ model: this.model }); + this.$(this.citationContainer).html(header.render().el); + }, - if (typeof customCoordinates !== "undefined") { - //Extract the coordinates - var n = customCoordinates[0]; - var s = customCoordinates[1]; - var e = customCoordinates[2]; - var w = customCoordinates[3]; - } - else { - var coordinates = new Array(); - - _.each(directions, function (direction) { - //Parse text for older versions of Metacat (v2.4.3 and earlier) - if (parseText) { - var labelEl = $(georegion).find('label:contains("' + direction + '")'); - if (labelEl.length) { - var coordinate = $(labelEl).next().html(); - if (typeof coordinate != "undefined" && coordinate.indexOf("&nbsp;") > -1) - coordinate = coordinate.substring(0, coordinate.indexOf("&nbsp;")); - } - } - else { - var coordinate = $(georegion).find("." + direction + "BoundingCoordinate").attr("data-value"); - } + insertDataSource: function () { + if ( + !this.model || + !MetacatUI.nodeModel || + !MetacatUI.nodeModel.get("members").length || + !this.$(this.dataSourceContainer).length + ) + return; - //Save our coordinate value - coordinates.push(coordinate); - }); + var dataSource = MetacatUI.nodeModel.getMember(this.model), + replicaMNs = MetacatUI.nodeModel.getMembers( + this.model.get("replicaMN"), + ); - //Extract the coordinates - var n = coordinates[0]; - var s = coordinates[1]; - var e = coordinates[2]; - var w = coordinates[3]; - } + //Filter out the data source from the replica nodes + if (Array.isArray(replicaMNs) && replicaMNs.length) { + replicaMNs = _.without(replicaMNs, dataSource); + } - //Create Google Map LatLng objects out of our coordinates - var latLngSW = new gmaps.LatLng(s, w); - var latLngNE = new gmaps.LatLng(n, e); - var latLngNW = new gmaps.LatLng(n, w); - var latLngSE = new gmaps.LatLng(s, e); + if (dataSource && dataSource.logo) { + this.$("img.data-source").remove(); + + //Construct a URL to the profile of this repository + var profileURL = + dataSource.identifier == MetacatUI.appModel.get("nodeId") + ? MetacatUI.root + "/profile" + : MetacatUI.appModel.get("dataoneSearchUrl") + + "/portals/" + + dataSource.shortIdentifier; + + //Insert the data source template + this.$(this.dataSourceContainer) + .html( + this.dataSourceTemplate({ + node: dataSource, + profileURL: profileURL, + }), + ) + .addClass("has-data-source"); + + this.$(this.citationContainer).addClass("has-data-source"); + this.$(".tooltip-this").tooltip(); - //Get the centertroid location of this data item - var bounds = new gmaps.LatLngBounds(latLngSW, latLngNE); - var latLngCEN = bounds.getCenter(); + $(".popover-this.data-source.logo") + .popover({ + trigger: "manual", + html: true, + title: "From the " + dataSource.name + " repository", + content: function () { + var content = "<p>" + dataSource.description + "</p>"; - //If there isn't a center point found, don't draw the map. - if (typeof latLngCEN == "undefined") { - return; - } - - //Get the map path color - var pathColor = MetacatUI.appModel.get("datasetMapPathColor"); - if (pathColor) { - pathColor = "color:" + pathColor + "|"; - } - else { - pathColor = ""; - } - - //Get the map path fill color - var fillColor = MetacatUI.appModel.get("datasetMapFillColor"); - if (fillColor) { - fillColor = "fillcolor:" + fillColor + "|"; - } - else { - fillColor = ""; - } - - //Create a google map image - var mapHTML = "<img class='georegion-map' " + - "src='https://maps.googleapis.com/maps/api/staticmap?" + - "center=" + latLngCEN.lat() + "," + latLngCEN.lng() + - "&size=800x350" + - "&maptype=terrain" + - "&markers=size:mid|color:0xDA4D3Aff|" + latLngCEN.lat() + "," + latLngCEN.lng() + - "&path=" + fillColor + pathColor + "weight:3|" + latLngSW.lat() + "," + latLngSW.lng() + "|" + latLngNW.lat() + "," + latLngNW.lng() + "|" + latLngNE.lat() + "," + latLngNE.lng() + "|" + latLngSE.lat() + "," + latLngSE.lng() + "|" + latLngSW.lat() + "," + latLngSW.lng() + - "&visible=" + latLngSW.lat() + "," + latLngSW.lng() + "|" + latLngNW.lat() + "," + latLngNW.lng() + "|" + latLngNE.lat() + "," + latLngNE.lng() + "|" + latLngSE.lat() + "," + latLngSE.lng() + "|" + latLngSW.lat() + "," + latLngSW.lng() + - "&sensor=false" + - "&key=" + MetacatUI.mapKey + "'/>"; - - //Find the spot in the DOM to insert our map image - if (parseText) var insertAfter = ($(georegion).find('label:contains("West")').parent().parent().length) ? $(georegion).find('label:contains("West")').parent().parent() : georegion; //The last coordinate listed - else var insertAfter = georegion; - - // Get the URL to the interactive Google Maps instance - const url = this.getGoogleMapsUrl(latLngCEN, bounds); - - // Insert the map image - $(insertAfter).append(this.mapTemplate({ - map: mapHTML, - url: url - })); - - $('.fancybox-media').fancybox({ - openEffect: 'elastic', - closeEffect: 'elastic', - helpers: { - media: {} - } - }) - - } - - return true; - - }, - - /** - * Returns a URL to a Google Maps instance that is centered on the given - * coordinates and zoomed to the appropriate level to display the given - * bounding box. - * @param {LatLng} latLngCEN - The center point of the map. - * @param {LatLngBounds} bounds - The bounding box to display. - * @returns {string} The URL to the Google Maps instance. - * @since 2.27.0 - */ - getGoogleMapsUrl: function (latLngCEN, bounds) { - // Use the window width and height as a proxy for the map dimensions - const mapDim = { - height: $(window).height(), - width: $(window).width() - }; - const z = this.getBoundsZoomLevel(bounds, mapDim); - const mapLat = latLngCEN.lat(); - const mapLng = latLngCEN.lng(); - - return `https://maps.google.com/?ll=${mapLat},${mapLng}&z=${z}`; - }, - - /** - * Returns the zoom level that will display the given bounding box at - * the given dimensions. - * @param {LatLngBounds} bounds - The bounding box to display. - * @param {Object} mapDim - The dimensions of the map. - * @param {number} mapDim.height - The height of the map. - * @param {number} mapDim.width - The width of the map. - * @returns {number} The zoom level. - * @since 2.27.0 - */ - getBoundsZoomLevel: function(bounds, mapDim) { - var WORLD_DIM = { height: 256, width: 256 }; - var ZOOM_MAX = 15; - // 21 is actual max, but any closer and the map is too zoomed in to be - // useful - - function latRad(lat) { - var sin = Math.sin(lat * Math.PI / 180); - var radX2 = Math.log((1 + sin) / (1 - sin)) / 2; - return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2; - } - - function zoom(mapPx, worldPx, fraction) { - return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2); - } - - var ne = bounds.getNorthEast(); - var sw = bounds.getSouthWest(); - - var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI; - - var lngDiff = ne.lng() - sw.lng(); - var lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360; - - var latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction); - var lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction); - - return Math.min(latZoom, lngZoom, ZOOM_MAX); - }, - - - insertCitation: function () { - if (!this.model) return false; - //Create a citation header element from the model attributes - var header = new CitationHeaderView({ model: this.model }); - this.$(this.citationContainer).html(header.render().el); - }, - - insertDataSource: function () { - if (!this.model || !MetacatUI.nodeModel || !MetacatUI.nodeModel.get("members").length || !this.$(this.dataSourceContainer).length) return; - - var dataSource = MetacatUI.nodeModel.getMember(this.model), - replicaMNs = MetacatUI.nodeModel.getMembers(this.model.get("replicaMN")); - - //Filter out the data source from the replica nodes - if (Array.isArray(replicaMNs) && replicaMNs.length) { - replicaMNs = _.without(replicaMNs, dataSource); - } - - if (dataSource && dataSource.logo) { - this.$("img.data-source").remove(); - - //Construct a URL to the profile of this repository - var profileURL = (dataSource.identifier == MetacatUI.appModel.get("nodeId")) ? - MetacatUI.root + "/profile" : - MetacatUI.appModel.get("dataoneSearchUrl") + "/portals/" + dataSource.shortIdentifier; - - //Insert the data source template - this.$(this.dataSourceContainer).html(this.dataSourceTemplate({ - node: dataSource, - profileURL: profileURL - })).addClass("has-data-source"); - - this.$(this.citationContainer).addClass("has-data-source"); - this.$(".tooltip-this").tooltip(); - - $(".popover-this.data-source.logo").popover({ - trigger: "manual", - html: true, - title: "From the " + dataSource.name + " repository", - content: function () { - var content = "<p>" + dataSource.description + "</p>"; - - if (replicaMNs.length) { - content += '<h5>Exact copies hosted by ' + replicaMNs.length + ' repositories: </h5><ul class="unstyled">'; + if (replicaMNs.length) { + content += + "<h5>Exact copies hosted by " + + replicaMNs.length + + ' repositories: </h5><ul class="unstyled">'; _.each(replicaMNs, function (node) { - content += '<li><a href="' + MetacatUI.appModel.get("dataoneSearchUrl") + '/portals/' + + content += + '<li><a href="' + + MetacatUI.appModel.get("dataoneSearchUrl") + + "/portals/" + node.shortIdentifier + '" class="pointer">' + node.name + - '</a></li>'; + "</a></li>"; }); content += "</ul>"; @@ -1301,1744 +1547,1926 @@

Source: src/js/views/MetadataView.js

return content; }, - animation: false + animation: false, }) - .on("mouseenter", function () { - var _this = this; - $(this).popover("show"); - $(".popover").on("mouseleave", function () { - $(_this).popover('hide'); - }); - }).on("mouseleave", function () { - var _this = this; - setTimeout(function () { - if (!$(".popover:hover").length) { - $(_this).popover("hide"); - } - }, 300); - }); - - } - }, - - /** - * Check whether the user has write permissions on the resource map and the EML. - * Once the permission checks have finished, continue with the functions that - * depend on them. - */ - checkWritePermissions: function () { - - var view = this, - authorization = [], - resourceMap = this.dataPackage ? this.dataPackage.packageModel : null, - modelsToCheck = [this.model, resourceMap]; - - modelsToCheck.forEach(function (model, index) { - // If there is no resource map or no EML, - // then the user does not need permission to edit it. - if (!model || model.get("notFound") == true) { - authorization[index] = true - // If we already checked, and the user is authorized, - // record that information in the authorzation array. - } else if (model.get("isAuthorized_write") === true) { - authorization[index] = true - // If we already checked, and the user is not authorized, - // record that information in the authorzation array. - } else if (model.get("isAuthorized_write") === false) { - authorization[index] = false - // If we haven't checked for authorization yet, do that now. - // Return to this function once we've finished checking. - } else { - view.stopListening(model, "change:isAuthorized_write"); - view.listenToOnce(model, "change:isAuthorized_write", function () { - view.checkWritePermissions(); - }); - view.stopListening(model, "change:notFound"); - view.listenToOnce(model, "change:notFound", function () { - view.checkWritePermissions(); + .on("mouseenter", function () { + var _this = this; + $(this).popover("show"); + $(".popover").on("mouseleave", function () { + $(_this).popover("hide"); }); - model.checkAuthority("write"); - return - } - }); - - // Check that all the models were tested for authorization - - // Every value in the auth array must be true for the user to have full permissions - var allTrue = _.every(authorization, function (test) { return test }), - // When we have completed checking each of the models that we need to check for - // permissions, every value in the authorization array should be "true" or "false", - // and the array should have the same length as the modelsToCheck array. - allBoolean = _.every(authorization, function (test) { return typeof test === "boolean" }), - allChecked = allBoolean && authorization.length === modelsToCheck.length; - - // Check for and render prov diagrams now that we know whether or not the user has editor permissions - // (There is a different version of the chart for users who can edit the resource map and users who cannot) - if (allChecked) { - this.checkForProv(); + }) + .on("mouseleave", function () { + var _this = this; + setTimeout(function () { + if (!$(".popover:hover").length) { + $(_this).popover("hide"); + } + }, 300); + }); + } + }, + + /** + * Check whether the user has write permissions on the resource map and the EML. + * Once the permission checks have finished, continue with the functions that + * depend on them. + */ + checkWritePermissions: function () { + var view = this, + authorization = [], + resourceMap = this.dataPackage ? this.dataPackage.packageModel : null, + modelsToCheck = [this.model, resourceMap]; + + modelsToCheck.forEach(function (model, index) { + // If there is no resource map or no EML, + // then the user does not need permission to edit it. + if (!model || model.get("notFound") == true) { + authorization[index] = true; + // If we already checked, and the user is authorized, + // record that information in the authorzation array. + } else if (model.get("isAuthorized_write") === true) { + authorization[index] = true; + // If we already checked, and the user is not authorized, + // record that information in the authorzation array. + } else if (model.get("isAuthorized_write") === false) { + authorization[index] = false; + // If we haven't checked for authorization yet, do that now. + // Return to this function once we've finished checking. } else { - return - } - // Only render the editor controls if we have completed the checks AND the user has full editor permissions - if (allTrue) { - this.insertEditorControls(); + view.stopListening(model, "change:isAuthorized_write"); + view.listenToOnce(model, "change:isAuthorized_write", function () { + view.checkWritePermissions(); + }); + view.stopListening(model, "change:notFound"); + view.listenToOnce(model, "change:notFound", function () { + view.checkWritePermissions(); + }); + model.checkAuthority("write"); + return; } + }); + + // Check that all the models were tested for authorization + + // Every value in the auth array must be true for the user to have full permissions + var allTrue = _.every(authorization, function (test) { + return test; + }), + // When we have completed checking each of the models that we need to check for + // permissions, every value in the authorization array should be "true" or "false", + // and the array should have the same length as the modelsToCheck array. + allBoolean = _.every(authorization, function (test) { + return typeof test === "boolean"; + }), + allChecked = + allBoolean && authorization.length === modelsToCheck.length; + + // Check for and render prov diagrams now that we know whether or not the user has editor permissions + // (There is a different version of the chart for users who can edit the resource map and users who cannot) + if (allChecked) { + this.checkForProv(); + } else { + return; + } + // Only render the editor controls if we have completed the checks AND the user has full editor permissions + if (allTrue) { + this.insertEditorControls(); + } + }, + + /* + * Inserts control elements onto the page for the user to interact with the dataset - edit, publish, etc. + * Editor permissions should already have been checked before running this function. + */ + insertEditorControls: function () { + var view = this, + resourceMap = this.dataPackage ? this.dataPackage.packageModel : null, + modelsToCheck = [this.model, resourceMap], + authorized = _.every(modelsToCheck, function (model) { + // If there is no EML or no resource map, the user doesn't need permission to edit it. + return !model || model.get("notFound") == true + ? true + : model.get("isAuthorized_write") === true; + }); - }, + // Only run this function when the user has full editor permissions + // (i.e. write permission on the EML, and write permission on the resource map if there is one.) + if (!authorized) { + return; + } - /* - * Inserts control elements onto the page for the user to interact with the dataset - edit, publish, etc. - * Editor permissions should already have been checked before running this function. - */ - insertEditorControls: function () { + if ( + (this.model.get("obsoletedBy") && + this.model.get("obsoletedBy").length > 0) || + this.model.get("archived") + ) { + return false; + } - var view = this, - resourceMap = this.dataPackage ? this.dataPackage.packageModel : null, - modelsToCheck = [this.model, resourceMap], - authorized = _.every(modelsToCheck, function (model) { - // If there is no EML or no resource map, the user doesn't need permission to edit it. - return (!model || model.get("notFound") == true) ? true : model.get("isAuthorized_write") === true; - }); + // Save the element that will contain the owner control buttons + var container = this.$(this.editorControlsContainer); + // Do not insert the editor controls twice + container.empty(); - // Only run this function when the user has full editor permissions - // (i.e. write permission on the EML, and write permission on the resource map if there is one.) - if (!authorized) { - return - } + // The PID for the EML model + var pid = this.model.get("id") || this.pid; + //Insert an Edit button if the Edit button is enabled + if (MetacatUI.appModel.get("displayDatasetEditButton")) { + //Check that this is an editable metadata format if ( - (this.model.get("obsoletedBy") && (this.model.get("obsoletedBy").length > 0)) || - this.model.get("archived") + _.contains( + MetacatUI.appModel.get("editableFormats"), + this.model.get("formatId"), + ) ) { - return false; - } - - // Save the element that will contain the owner control buttons - var container = this.$(this.editorControlsContainer); - // Do not insert the editor controls twice - container.empty(); - - // The PID for the EML model - var pid = this.model.get("id") || this.pid; - - //Insert an Edit button if the Edit button is enabled - if (MetacatUI.appModel.get("displayDatasetEditButton")) { - //Check that this is an editable metadata format - if (_.contains(MetacatUI.appModel.get("editableFormats"), this.model.get("formatId"))) { - //Insert the Edit Metadata template - container.append( - this.editMetadataTemplate({ - identifier: pid, - supported: true - })); - } - //If this format is not editable, insert an unspported Edit Metadata template - else { - container.append(this.editMetadataTemplate({ - supported: false - })); - } + //Insert the Edit Metadata template + container.append( + this.editMetadataTemplate({ + identifier: pid, + supported: true, + }), + ); + } + //If this format is not editable, insert an unspported Edit Metadata template + else { + container.append( + this.editMetadataTemplate({ + supported: false, + }), + ); } + } - try { - //Determine if this metadata can be published. - // The Publish feature has to be enabled in the app. - // The model cannot already have a DOI - var canBePublished = MetacatUI.appModel.get("enablePublishDOI") && !view.model.isDOI(); - - //If publishing is enabled, check if only certain users and groups can publish metadata - if (canBePublished) { - //Get the list of authorized publishers from the AppModel - var authorizedPublishers = MetacatUI.appModel.get("enablePublishDOIForSubjects"); - //If the logged-in user is one of the subjects in the list or is in a group that is - // in the list, then this metadata can be published. Otherwise, it cannot. - if (Array.isArray(authorizedPublishers) && authorizedPublishers.length) { - if (MetacatUI.appUserModel.hasIdentityOverlap(authorizedPublishers)) { - canBePublished = true; - } - else { - canBePublished = false; - } + try { + //Determine if this metadata can be published. + // The Publish feature has to be enabled in the app. + // The model cannot already have a DOI + var canBePublished = + MetacatUI.appModel.get("enablePublishDOI") && !view.model.isDOI(); + + //If publishing is enabled, check if only certain users and groups can publish metadata + if (canBePublished) { + //Get the list of authorized publishers from the AppModel + var authorizedPublishers = MetacatUI.appModel.get( + "enablePublishDOIForSubjects", + ); + //If the logged-in user is one of the subjects in the list or is in a group that is + // in the list, then this metadata can be published. Otherwise, it cannot. + if ( + Array.isArray(authorizedPublishers) && + authorizedPublishers.length + ) { + if ( + MetacatUI.appUserModel.hasIdentityOverlap(authorizedPublishers) + ) { + canBePublished = true; + } else { + canBePublished = false; } } - - //If this metadata can be published, then insert the Publish button template - if (canBePublished) { - //Insert a Publish button template - container.append( - view.doiTemplate({ - isAuthorized: true, - identifier: pid - })); - } } - catch (e) { - console.error("Cannot display the publish button: ", e); - } - - }, - /* - * Injects Clipboard objects onto DOM elements returned from the Metacat - * View Service. This code depends on the implementation of the Metacat - * View Service in that it depends on elements with the class "copy" being - * contained in the HTML returned from the View Service. - * - * To add more copiable buttons (or other elements) to a View Service XSLT, - * you should be able to just add something like: - * - * <button class="btn copy" data-clipboard-text="your-text-to-copy"> - * Copy - * </button> - * - * to your XSLT and this should pick it up automatically. - */ - insertCopiables: function () { - var copiables = $("#Metadata .copy"); - - _.each(copiables, function (copiable) { - var clipboard = new Clipboard(copiable); - - clipboard.on("success", function (e) { - var el = $(e.trigger); - - $(el).html($(document.createElement("span")).addClass("icon icon-ok success")); - - // Use setTimeout instead of jQuery's built-in Events system because - // it didn't look flexible enough to allow me update innerHTML in - // a chain - setTimeout(function () { - $(el).html("Copy"); - }, 500) - }); + //If this metadata can be published, then insert the Publish button template + if (canBePublished) { + //Insert a Publish button template + container.append( + view.doiTemplate({ + isAuthorized: true, + identifier: pid, + }), + ); + } + } catch (e) { + console.error("Cannot display the publish button: ", e); + } + }, + + /* + * Injects Clipboard objects onto DOM elements returned from the Metacat + * View Service. This code depends on the implementation of the Metacat + * View Service in that it depends on elements with the class "copy" being + * contained in the HTML returned from the View Service. + * + * To add more copiable buttons (or other elements) to a View Service XSLT, + * you should be able to just add something like: + * + * <button class="btn copy" data-clipboard-text="your-text-to-copy"> + * Copy + * </button> + * + * to your XSLT and this should pick it up automatically. + */ + insertCopiables: function () { + var copiables = $("#Metadata .copy"); + + _.each(copiables, function (copiable) { + var clipboard = new Clipboard(copiable); + + clipboard.on("success", function (e) { + var el = $(e.trigger); + + $(el).html( + $(document.createElement("span")).addClass( + "icon icon-ok success", + ), + ); + + // Use setTimeout instead of jQuery's built-in Events system because + // it didn't look flexible enough to allow me update innerHTML in + // a chain + setTimeout(function () { + $(el).html("Copy"); + }, 500); }); - }, - - /* - * Inserts elements users can use to interact with this dataset: - * - A "Copy Citation" button to copy the citation text - */ - insertControls: function () { - - // Convert the support mdq formatId list to a version - // that JS regex likes (with special characters double - RegExp.escape = function (s) { - return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\\\$&'); - }; - var mdqFormatIds = MetacatUI.appModel.get("mdqFormatIds"); - - // Check of the current formatId is supported by the current - // metadata quality suite. If not, the 'Assessment Report' button - // will not be displacyed in the metadata controls panel. - var thisFormatId = this.model.get("formatId"); - var mdqFormatSupported = false; - var formatFound = false; - if (mdqFormatIds !== null) { - for (var ifmt = 0; ifmt < mdqFormatIds.length; ++ifmt) { - var currentFormatId = RegExp.escape(mdqFormatIds[ifmt]); - var re = new RegExp(currentFormatId); - formatFound = re.test(thisFormatId); - if (formatFound) { - break; - } + }); + }, + + /* + * Inserts elements users can use to interact with this dataset: + * - A "Copy Citation" button to copy the citation text + */ + insertControls: function () { + // Convert the support mdq formatId list to a version + // that JS regex likes (with special characters double + RegExp.escape = function (s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\\\$&"); + }; + var mdqFormatIds = MetacatUI.appModel.get("mdqFormatIds"); + + // Check of the current formatId is supported by the current + // metadata quality suite. If not, the 'Assessment Report' button + // will not be displacyed in the metadata controls panel. + var thisFormatId = this.model.get("formatId"); + var mdqFormatSupported = false; + var formatFound = false; + if (mdqFormatIds !== null) { + for (var ifmt = 0; ifmt < mdqFormatIds.length; ++ifmt) { + var currentFormatId = RegExp.escape(mdqFormatIds[ifmt]); + var re = new RegExp(currentFormatId); + formatFound = re.test(thisFormatId); + if (formatFound) { + break; } } + } - //Get template - var controlsContainer = this.controlsTemplate({ - citationTarget: this.citationContainer, - url: window.location, - displayQualtyReport: MetacatUI.appModel.get("mdqBaseUrl") && formatFound && MetacatUI.appModel.get("displayDatasetQualityMetric"), - showWholetale: MetacatUI.appModel.get("showWholeTaleFeatures"), - model: this.model.toJSON() - }); - - $(this.controlsContainer).html(controlsContainer); - - - //Insert the info icons - var metricsWell = this.$(".metrics-container"); - metricsWell.append(this.infoIconsTemplate({ - model: this.model.toJSON() - })); - - if (MetacatUI.appModel.get("showWholeTaleFeatures")) { - this.createWholeTaleButton(); - } + //Get template + var controlsContainer = this.controlsTemplate({ + citationTarget: this.citationContainer, + url: window.location, + displayQualtyReport: + MetacatUI.appModel.get("mdqBaseUrl") && + formatFound && + MetacatUI.appModel.get("displayDatasetQualityMetric"), + showWholetale: MetacatUI.appModel.get("showWholeTaleFeatures"), + model: this.model.toJSON(), + }); + + $(this.controlsContainer).html(controlsContainer); + + //Insert the info icons + var metricsWell = this.$(".metrics-container"); + metricsWell.append( + this.infoIconsTemplate({ + model: this.model.toJSON(), + }), + ); + + if (MetacatUI.appModel.get("showWholeTaleFeatures")) { + this.createWholeTaleButton(); + } - // Show the citation modal with the ability to copy the citation text - // when the "Copy Citation" button is clicked - const citeButton = this.el.querySelector('#cite-this-dataset-btn'); - if (citeButton) { - citeButton.removeEventListener('click', this.citationModal); - citeButton.addEventListener('click', () => { + // Show the citation modal with the ability to copy the citation text + // when the "Copy Citation" button is clicked + const citeButton = this.el.querySelector("#cite-this-dataset-btn"); + if (citeButton) { + citeButton.removeEventListener("click", this.citationModal); + citeButton.addEventListener( + "click", + () => { this.citationModal = new CitationModalView({ model: this.model, - createLink: true - }) + createLink: true, + }); this.subviews.push(this.citationModal); this.citationModal.render(); - }, false); - } - - }, - - /** - *Creates a button which the user can click to launch the package in Whole Tale - */ - createWholeTaleButton: function () { - let self = this; - MetacatUI.appModel.get('taleEnvironments').forEach(function (environment) { + }, + false, + ); + } + }, + + /** + *Creates a button which the user can click to launch the package in Whole Tale + */ + createWholeTaleButton: function () { + let self = this; + MetacatUI.appModel + .get("taleEnvironments") + .forEach(function (environment) { var queryParams = - '?uri=' + window.location.href + - '&title=' + encodeURIComponent(self.model.get("title")) + - '&environment=' + environment + - '&api=' + MetacatUI.appModel.get("d1CNBaseUrl") + MetacatUI.appModel.get("d1CNService"); - var composeUrl = MetacatUI.appModel.get('dashboardUrl') + queryParams; - var anchor = $('<a>'); - anchor.attr('href', composeUrl).append( - $('<span>').attr('class', 'tab').append(environment)); - anchor.attr('target', '_blank'); - $('.analyze.dropdown-menu').append($('<li>').append(anchor)); + "?uri=" + + window.location.href + + "&title=" + + encodeURIComponent(self.model.get("title")) + + "&environment=" + + environment + + "&api=" + + MetacatUI.appModel.get("d1CNBaseUrl") + + MetacatUI.appModel.get("d1CNService"); + var composeUrl = + MetacatUI.appModel.get("dashboardUrl") + queryParams; + var anchor = $("<a>"); + anchor + .attr("href", composeUrl) + .append($("<span>").attr("class", "tab").append(environment)); + anchor.attr("target", "_blank"); + $(".analyze.dropdown-menu").append($("<li>").append(anchor)); }); - }, + }, - // Inserting the Metric Stats - insertMetricsControls: function () { + // Inserting the Metric Stats + insertMetricsControls: function () { + //Exit if metrics shouldn't be shown for this dataset + if (this.model.hideMetrics()) { + return; + } - //Exit if metrics shouldn't be shown for this dataset - if (this.model.hideMetrics()) { - return; + var pid_list = []; + pid_list.push(this.pid); + var metricsModel = new MetricsModel({ + pid_list: pid_list, + type: "dataset", + }); + metricsModel.fetch(); + this.metricsModel = metricsModel; + + // Retreive the model from the server for the given PID + // TODO: Create a Metric Request Object + + if (MetacatUI.appModel.get("displayDatasetMetrics")) { + var buttonToolbar = this.$(".metrics-container"); + + if (MetacatUI.appModel.get("displayDatasetDownloadMetric")) { + var dwnldsMetricView = new MetricView({ + metricName: "Downloads", + model: metricsModel, + pid: this.pid, + }); + buttonToolbar.append(dwnldsMetricView.render().el); + this.subviews.push(dwnldsMetricView); } - - var pid_list = []; - pid_list.push(this.pid); - var metricsModel = new MetricsModel({ pid_list: pid_list, type: "dataset" }); - metricsModel.fetch(); - this.metricsModel = metricsModel; - - // Retreive the model from the server for the given PID - // TODO: Create a Metric Request Object - - if (MetacatUI.appModel.get("displayDatasetMetrics")) { - var buttonToolbar = this.$(".metrics-container"); - - if (MetacatUI.appModel.get("displayDatasetDownloadMetric")) { - var dwnldsMetricView = new MetricView({ metricName: 'Downloads', model: metricsModel, pid: this.pid }); - buttonToolbar.append(dwnldsMetricView.render().el); - this.subviews.push(dwnldsMetricView); - } - - if (MetacatUI.appModel.get("displayDatasetCitationMetric")) { - var citationsMetricView = new MetricView({ metricName: 'Citations', model: metricsModel, pid: this.pid }); - buttonToolbar.append(citationsMetricView.render().el); - this.subviews.push(citationsMetricView); - - try { - //Check if the registerCitation=true query string is set - if (window.location.search) { - if (window.location.search.indexOf("registerCitation=true") > -1) { - - //Open the modal for the citations - citationsMetricView.showMetricModal(); - - //Show the register citation form - if (citationsMetricView.modalView) { - citationsMetricView.modalView.on("renderComplete", citationsMetricView.modalView.showCitationForm); - } + if (MetacatUI.appModel.get("displayDatasetCitationMetric")) { + var citationsMetricView = new MetricView({ + metricName: "Citations", + model: metricsModel, + pid: this.pid, + }); + buttonToolbar.append(citationsMetricView.render().el); + this.subviews.push(citationsMetricView); + + try { + //Check if the registerCitation=true query string is set + if (window.location.search) { + if ( + window.location.search.indexOf("registerCitation=true") > -1 + ) { + //Open the modal for the citations + citationsMetricView.showMetricModal(); + + //Show the register citation form + if (citationsMetricView.modalView) { + citationsMetricView.modalView.on( + "renderComplete", + citationsMetricView.modalView.showCitationForm, + ); } } } - catch (e) { - console.warn("Not able to show the register citation form ", e); - } + } catch (e) { + console.warn("Not able to show the register citation form ", e); } - - if (MetacatUI.appModel.get("displayDatasetViewMetric")) { - var viewsMetricView = new MetricView({ metricName: 'Views', model: metricsModel, pid: this.pid }); - buttonToolbar.append(viewsMetricView.render().el); - this.subviews.push(viewsMetricView); - } - } - }, - - /** - * Check if the DataPackage provenance parsing has completed. If it has, - * draw provenance charts. If it hasn't start the parseProv function. - * The view must have the DataPackage collection set as view.dataPackage - * for this function to run. - */ - checkForProv: function () { - - if (!this.dataPackage) { - return - } - // Render the provenance trace using the redrawProvCharts function instead of the drawProvCharts function - // just in case the prov charts have already been inserted. Redraw will make sure they are removed - // before being re-inserted. - var model = this.model; - if (this.dataPackage.provenanceFlag == "complete") { - this.redrawProvCharts(this.dataPackage); - } else { - this.listenToOnce(this.dataPackage, "queryComplete", function () { - this.redrawProvCharts(this.dataPackage); + if (MetacatUI.appModel.get("displayDatasetViewMetric")) { + var viewsMetricView = new MetricView({ + metricName: "Views", + model: metricsModel, + pid: this.pid, }); - // parseProv triggers "queryComplete" - this.dataPackage.parseProv(); + buttonToolbar.append(viewsMetricView.render().el); + this.subviews.push(viewsMetricView); } - }, - - /* - * Renders ProvChartViews on the page to display provenance on a package level and on an individual object level. - * This function looks at four sources for the provenance - the package sources, the package derivations, member sources, and member derivations - */ - drawProvCharts: function (dataPackage) { - - // Set a listener to re-draw the prov charts when needed - this.stopListening(this.dataPackage, "redrawProvCharts"); - this.listenToOnce(this.dataPackage, "redrawProvCharts", this.redrawProvCharts); - - // Provenance has to be retrieved from the Package Model (getProvTrace()) before the charts can be drawn - if (dataPackage.provenanceFlag != "complete") return false; - - // If the user is authorized to edit the provenance for this package - // then turn on editing, so that edit icons are displayed. - var editModeOn = this.dataPackage.packageModel.get("isAuthorized_write"); - - //If this content is archived, then turn edit mode off - if (this.model.get("archived")) { - editModeOn = false; - } - - //If none of the models in this package have the formatId attributes, - // we should fetch the DataPackage since it likely has only had a shallow fetch so far - var formats = _.compact(dataPackage.pluck("formatId")); - - //If the number of formatIds is less than the number of models in this collection, - // then we need to get them. - if (formats.length < dataPackage.length) { - - var modelsToMerge = []; - - //Get the PackageModel associated with this view - if (this.packageModels.length) { - //Get the PackageModel for this DataPackage - var packageModel = _.find(this.packageModels, function (packageModel) { return packageModel.get("id") == dataPackage.id }); - - //Merge the SolrResult models into the DataONEObject models - if (packageModel && packageModel.get("members").length) { - modelsToMerge = packageModel.get("members"); - } - } - - //If there is at least one model to merge into this data package, do so - if (modelsToMerge.length) { - dataPackage.mergeModels(modelsToMerge); - } - //If there are no models to merge in, get them from the index - else { - - //Listen to the DataPackage fetch to complete and re-execute this function - this.listenToOnce(dataPackage, "complete", function () { - this.drawProvCharts(dataPackage); - }); - - //Create a query that searches for all the members of this DataPackage in Solr - dataPackage.solrResults.currentquery = dataPackage.filterModel.getQuery() + - "%20AND%20-formatType:METADATA"; - dataPackage.solrResults.fields = "id,seriesId,formatId,fileName"; - dataPackage.solrResults.rows = dataPackage.length; - dataPackage.solrResults.sort = null; - dataPackage.solrResults.start = 0; - dataPackage.solrResults.facet = []; - dataPackage.solrResults.stats = null; - - //Fetch the data package with the "fromIndex" option - dataPackage.fetch({ fromIndex: true }); + } + }, + + /** + * Check if the DataPackage provenance parsing has completed. If it has, + * draw provenance charts. If it hasn't start the parseProv function. + * The view must have the DataPackage collection set as view.dataPackage + * for this function to run. + */ + checkForProv: function () { + if (!this.dataPackage) { + return; + } + // Render the provenance trace using the redrawProvCharts function instead of the drawProvCharts function + // just in case the prov charts have already been inserted. Redraw will make sure they are removed + // before being re-inserted. + var model = this.model; + if (this.dataPackage.provenanceFlag == "complete") { + this.redrawProvCharts(this.dataPackage); + } else { + this.listenToOnce(this.dataPackage, "queryComplete", function () { + this.redrawProvCharts(this.dataPackage); + }); + // parseProv triggers "queryComplete" + this.dataPackage.parseProv(); + } + }, + + /* + * Renders ProvChartViews on the page to display provenance on a package level and on an individual object level. + * This function looks at four sources for the provenance - the package sources, the package derivations, member sources, and member derivations + */ + drawProvCharts: function (dataPackage) { + // Set a listener to re-draw the prov charts when needed + this.stopListening(this.dataPackage, "redrawProvCharts"); + this.listenToOnce( + this.dataPackage, + "redrawProvCharts", + this.redrawProvCharts, + ); + + // Provenance has to be retrieved from the Package Model (getProvTrace()) before the charts can be drawn + if (dataPackage.provenanceFlag != "complete") return false; + + // If the user is authorized to edit the provenance for this package + // then turn on editing, so that edit icons are displayed. + var editModeOn = + this.dataPackage.packageModel.get("isAuthorized_write"); + + //If this content is archived, then turn edit mode off + if (this.model.get("archived")) { + editModeOn = false; + } - //Exit this function since it will be executed again when the fetch is complete - return; + //If none of the models in this package have the formatId attributes, + // we should fetch the DataPackage since it likely has only had a shallow fetch so far + var formats = _.compact(dataPackage.pluck("formatId")); + + //If the number of formatIds is less than the number of models in this collection, + // then we need to get them. + if (formats.length < dataPackage.length) { + var modelsToMerge = []; + + //Get the PackageModel associated with this view + if (this.packageModels.length) { + //Get the PackageModel for this DataPackage + var packageModel = _.find( + this.packageModels, + function (packageModel) { + return packageModel.get("id") == dataPackage.id; + }, + ); + //Merge the SolrResult models into the DataONEObject models + if (packageModel && packageModel.get("members").length) { + modelsToMerge = packageModel.get("members"); } - - } - - var view = this; - //Draw two flow charts to represent the sources and derivations at a package level - var packageSources = dataPackage.sourcePackages; - var packageDerivations = dataPackage.derivationPackages; - - if (Object.keys(packageSources).length) { - var sourceProvChart = new ProvChart({ - sources: packageSources, - context: dataPackage, - contextEl: this.$(this.articleContainer), - dataPackage: dataPackage, - parentView: view - }); - this.subviews.push(sourceProvChart); - this.$(this.articleContainer).before(sourceProvChart.render().el); } - if (Object.keys(packageDerivations).length) { - var derivationProvChart = new ProvChart({ - derivations: packageDerivations, - context: dataPackage, - contextEl: this.$(this.articleContainer), - dataPackage: dataPackage, - parentView: view - }); - this.subviews.push(derivationProvChart); - this.$(this.articleContainer).after(derivationProvChart.render().el); - } - - if (dataPackage.sources.length || dataPackage.derivations.length || editModeOn) { - //Draw the provenance charts for each member of this package at an object level - _.each(dataPackage.toArray(), function (member, i) { - // Don't draw prov charts for metadata objects. - if (member.get("type").toLowerCase() == "metadata" || member.get("formatType").toLowerCase() == "metadata") { - return; - } - var entityDetailsSection = view.findEntityDetailsContainer(member); - - if (!entityDetailsSection) { - return; - } - - //Retrieve the sources and derivations for this member - var memberSources = member.get("provSources") || new Array(), - memberDerivations = member.get("provDerivations") || new Array(); - - //Make the source chart for this member. - // If edit is on, then either a 'blank' sources ProvChart will be displayed if there - // are no sources for this member, or edit icons will be displayed with prov icons. - if (memberSources.length || editModeOn) { - var memberSourcesProvChart = new ProvChart({ - sources: memberSources, - context: member, - contextEl: entityDetailsSection, - dataPackage: dataPackage, - parentView: view, - editModeOn: editModeOn, - editorType: "sources" - }); - view.subviews.push(memberSourcesProvChart); - $(entityDetailsSection).before(memberSourcesProvChart.render().el); - view.$(view.articleContainer).addClass("gutters"); - } - //Make the derivation chart for this member - // If edit is on, then either a 'blank' derivations ProvChart will be displayed if there, - // are no derivations for this member or edit icons will be displayed with prov icons. - if (memberDerivations.length || editModeOn) { - var memberDerivationsProvChart = new ProvChart({ - derivations: memberDerivations, - context: member, - contextEl: entityDetailsSection, - dataPackage: dataPackage, - parentView: view, - editModeOn: editModeOn, - editorType: "derivations" - }); - view.subviews.push(memberDerivationsProvChart); - $(entityDetailsSection).after(memberDerivationsProvChart.render().el); - view.$(view.articleContainer).addClass("gutters"); - } - }); + //If there is at least one model to merge into this data package, do so + if (modelsToMerge.length) { + dataPackage.mergeModels(modelsToMerge); } - - //Make all of the prov chart nodes look different based on id - if (this.$(".prov-chart").length > 10000) { - var allNodes = this.$(".prov-chart .node"), - ids = [], - view = this, - i = 1; - - $(allNodes).each(function () { ids.push($(this).attr("data-id")) }); - ids = _.uniq(ids); - - _.each(ids, function (id) { - var matchingNodes = view.$(".prov-chart .node[data-id='" + id + "']").not(".editorNode"); - //var matchingEntityDetails = view.findEntityDetailsContainer(id); - - //Don't use the unique class on images since they will look a lot different anyway by their image - if (!$(matchingNodes).first().hasClass("image")) { - var className = "uniqueNode" + i; - - //Add the unique class and up the iterator - if (matchingNodes.prop("tagName") != "polygon") - $(matchingNodes).addClass(className); - else - $(matchingNodes).attr("class", $(matchingNodes).attr("class") + " " + className); - - /* if(matchingEntityDetails) - $(matchingEntityDetails).addClass(className);*/ - - //Save this id->class mapping in this view - view.classMap.push({ - id: id, - className: className - }); - i++; - } + //If there are no models to merge in, get them from the index + else { + //Listen to the DataPackage fetch to complete and re-execute this function + this.listenToOnce(dataPackage, "complete", function () { + this.drawProvCharts(dataPackage); }); - } - }, - - /* Step through all prov charts and re-render each one that has been - marked for re-rendering. - */ - redrawProvCharts: function () { - var view = this; - - // Check if prov edits are active and turn on the prov save bar if so. - // Alternatively, turn off save bar if there are no prov edits, which - // could occur if a user undoes a previous which could result in - // an empty edit list. - if (this.dataPackage.provEditsPending()) { - this.showEditorControls(); - } else { - this.hideEditorControls(); - // Reset the edited flag for each package member - _.each(this.dataPackage.toArray(), function (item) { - item.selectedInEditor == false; - }); + //Create a query that searches for all the members of this DataPackage in Solr + dataPackage.solrResults.currentquery = + dataPackage.filterModel.getQuery() + + "%20AND%20-formatType:METADATA"; + dataPackage.solrResults.fields = "id,seriesId,formatId,fileName"; + dataPackage.solrResults.rows = dataPackage.length; + dataPackage.solrResults.sort = null; + dataPackage.solrResults.start = 0; + dataPackage.solrResults.facet = []; + dataPackage.solrResults.stats = null; + + //Fetch the data package with the "fromIndex" option + dataPackage.fetch({ fromIndex: true }); + + //Exit this function since it will be executed again when the fetch is complete + return; } - _.each(this.subviews, function (thisView, i) { + } - // Check if this is a ProvChartView - if (thisView.className && thisView.className.indexOf("prov-chart") !== -1) { - // Check if this ProvChartView is marked for re-rendering - // Erase the current ProvChartView - thisView.onClose(); - } + var view = this; + //Draw two flow charts to represent the sources and derivations at a package level + var packageSources = dataPackage.sourcePackages; + var packageDerivations = dataPackage.derivationPackages; + + if (Object.keys(packageSources).length) { + var sourceProvChart = new ProvChart({ + sources: packageSources, + context: dataPackage, + contextEl: this.$(this.articleContainer), + dataPackage: dataPackage, + parentView: view, }); - - // Remove prov charts from the array of subviews. - this.subviews = _.filter(this.subviews, function (item) { - return (item.className && (item.className.indexOf("prov-chart") == -1)); + this.subviews.push(sourceProvChart); + this.$(this.articleContainer).before(sourceProvChart.render().el); + } + if (Object.keys(packageDerivations).length) { + var derivationProvChart = new ProvChart({ + derivations: packageDerivations, + context: dataPackage, + contextEl: this.$(this.articleContainer), + dataPackage: dataPackage, + parentView: view, }); + this.subviews.push(derivationProvChart); + this.$(this.articleContainer).after(derivationProvChart.render().el); + } - view.drawProvCharts(this.dataPackage); - - }, - - /* - * When the data package collection saves successfully, tell the user - */ - saveSuccess: function (savedObject) { - //We only want to perform these actions after the package saves - if (savedObject.type != "DataPackage") return; - - //Change the URL to the new id - MetacatUI.uiRouter.navigate("view/" + this.dataPackage.packageModel.get("id"), { trigger: false, replace: true }); - - var message = $(document.createElement("div")).append($(document.createElement("span")).text("Your changes have been saved. ")); - - MetacatUI.appView.showAlert(message, "alert-success", "body", 4000, { remove: false }); - - // Reset the state to clean - this.dataPackage.packageModel.set("changed", false); - - // If provenance relationships were updated, then reset the edit list now. - if (this.dataPackage.provEdits.length) this.dataPackage.provEdits = []; - - this.saveProvPending = false; - this.hideSaving(); - this.stopListening(this.dataPackage, "errorSaving", this.saveError); - - // Turn off "save" footer - this.hideEditorControls(); - - // Update the metadata table header with the new resource map id. - // First find the DataPackageView for the top level package, and - // then re-render it with the update resmap id. - var view = this; - var metadataId = this.packageModels[0].getMetadata().get("id") - _.each(this.subviews, function (thisView, i) { - // Check if this is a ProvChartView - if (thisView.type && thisView.type.indexOf("DataPackage") !== -1) { - if (thisView.currentlyViewing == metadataId) { - var packageId = view.dataPackage.packageModel.get("id"); - var title = packageId ? '<span class="subtle">Package: ' + packageId + '</span>' : ""; - thisView.title = "Files in this dataset " + title; - thisView.render(); - } + if ( + dataPackage.sources.length || + dataPackage.derivations.length || + editModeOn + ) { + //Draw the provenance charts for each member of this package at an object level + _.each(dataPackage.toArray(), function (member, i) { + // Don't draw prov charts for metadata objects. + if ( + member.get("type").toLowerCase() == "metadata" || + member.get("formatType").toLowerCase() == "metadata" + ) { + return; } - }); - }, - - /* - * When the data package collection fails to save, tell the user - */ - saveError: function (errorMsg) { - var errorId = "error" + Math.round(Math.random() * 100), - message = $(document.createElement("div")).append("<p>Your changes could not be saved.</p>"); - - message.append($(document.createElement("a")) - .text("See details") - .attr("data-toggle", "collapse") - .attr("data-target", "#" + errorId) - .addClass("pointer"), - $(document.createElement("div")) - .addClass("collapse") - .attr("id", errorId) - .append($(document.createElement("pre")).text(errorMsg))); - - MetacatUI.appView.showAlert(message, "alert-error", "body", null, { - emailBody: "Error message: Data Package save error: " + errorMsg, - remove: true - }); - - this.saveProvPending = false; - this.hideSaving(); - this.stopListening(this.dataPackage, "successSaving", this.saveSuccess); - - // Turn off "save" footer - this.hideEditorControls(); - }, - - /* If provenance relationships have been modified by the provenance editor (in ProvChartView), then - update the ORE Resource Map and save it to the server. - */ - saveProv: function () { - // Only call this function once per save operation. - if (this.saveProvPending) return; - - var view = this; - if (this.dataPackage.provEditsPending()) { - this.saveProvPending = true; - // If the Data Package failed saving, display an error message - this.listenToOnce(this.dataPackage, "errorSaving", this.saveError); - // Listen for when the package has been successfully saved - this.listenToOnce(this.dataPackage, "successSaving", this.saveSuccess); - this.showSaving(); - this.dataPackage.saveProv(); - } else { - //TODO: should a dialog be displayed saying that no prov edits were made? - } - }, - - showSaving: function () { - - //Change the style of the save button - this.$("#save-metadata-prov") - .html('<i class="icon icon-spinner icon-spin"></i> Saving...') - .addClass("btn-disabled"); - - this.$("input, textarea, select, button").prop("disabled", true); - }, - - hideSaving: function () { - this.$("input, textarea, select, button").prop("disabled", false); - - //When prov is saved, revert the Save button back to normal - this.$("#save-metadata-prov").html("Save").removeClass("btn-disabled"); - - }, - - showEditorControls: function () { - this.$("#editor-footer").slideDown(); - }, - - hideEditorControls: function () { - this.$("#editor-footer").slideUp(); - }, - - getEntityNames: function (packageModels) { - var viewRef = this; - - _.each(packageModels, function (packageModel) { - - //Don't get entity names for larger packages - users must put the names in the system metadata - if (packageModel.get("members").length > 100) return; - - //If this package has a different metadata doc than the one we are currently viewing - var metadataModel = packageModel.getMetadata(); - if (!metadataModel) return; - - if (metadataModel.get("id") != viewRef.pid) { - var requestSettings = { - url: MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(metadataModel.get("id")), - success: function (parsedMetadata, response, xhr) { - _.each(packageModel.get("members"), function (solrResult, i) { - var entityName = ""; - - if (solrResult.get("formatType") == "METADATA") - entityName = solrResult.get("title"); - - var container = viewRef.findEntityDetailsContainer(solrResult, parsedMetadata); - if (container) entityName = viewRef.getEntityName(container); - - //Set the entity name - if (entityName) { - solrResult.set("fileName", entityName); - //Update the UI with the new name - viewRef.$(".entity-name-placeholder[data-id='" + solrResult.get("id") + "']").text(entityName); - } - }); - } - } - - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); + var entityDetailsSection = view.findEntityDetailsContainer(member); + if (!entityDetailsSection) { return; } - _.each(packageModel.get("members"), function (solrResult, i) { - - var entityName = ""; - - if (solrResult.get("fileName")) - entityName = solrResult.get("fileName"); - else if (solrResult.get("formatType") == "METADATA") - entityName = solrResult.get("title"); - else if (solrResult.get("formatType") == "RESOURCE") - return; - else { - var container = viewRef.findEntityDetailsContainer(solrResult); - - if (container && container.length > 0) - entityName = viewRef.getEntityName(container); - else - entityName = null; - - } - - //Set the entityName, even if it's null - solrResult.set("fileName", entityName); - }); - }); - }, - - getEntityName: function (containerEl) { - if (!containerEl) return false; - - var entityName = $(containerEl).find(".entityName").attr("data-entity-name"); - if ((typeof entityName === "undefined") || (!entityName)) { - entityName = $(containerEl).find(".control-label:contains('Entity Name') + .controls-well").text(); - if ((typeof entityName === "undefined") || (!entityName)) - entityName = null; - } - - return entityName; - }, - - //Checks if the metadata has entity details sections - hasEntityDetails: function () { - return (this.$(".entitydetails").length > 0); - }, - - - /** - * Finds the element in the rendered metadata that describes the given data entity. - * - * @param {(DataONEObject|SolrResult|string)} model - Either a model that represents the data object or the identifier of the data object - * @param {Element} [el] - The DOM element to exclusivly search inside. - * @return {Element} - The DOM element that describbbes the given data entity. - */ - findEntityDetailsContainer: function (model, el) { - if (!el) var el = this.el; - - //Get the id and file name for this data object - var id = "", - fileName = ""; - - //If a model is given, get the id and file name from the object - if (model && (DataONEObject.prototype.isPrototypeOf(model) || SolrResult.prototype.isPrototypeOf(model))) { - id = model.get("id"); - fileName = model.get("fileName"); - } - //If a string is given instead, it must be the id of the data object - else if (typeof model == "string") { - id = model; - } - //Otherwise, there isn't enough info to find the element, so exit - else { - return; - } - - //If we already found it earlier, return it now - var container = this.$(".entitydetails[data-id='" + id + "'], " + - ".entitydetails[data-id='" + DataONEObject.prototype.getXMLSafeID(id) + "']"); - if (container.length) - return container; - - //Are we looking for the main object that this MetadataView is displaying? - if (id == this.pid) { - if (this.$("#Metadata").length > 0) - return this.$("#Metadata"); - else - return this.el; - } - - //Metacat 2.4.2 and up will have the Online Distribution Link marked - var link = this.$(".entitydetails a[data-pid='" + id + "']"); - - //Otherwise, try looking for an anchor with the id matching this object's id - if (!link.length) - link = $(el).find("a#" + id.replace(/[^A-Za-z0-9]/g, "\\$&")); - - //Get metadata index view - var metadataFromIndex = _.findWhere(this.subviews, { type: "MetadataIndex" }); - if (typeof metadataFromIndex === "undefined") metadataFromIndex = null; - - //Otherwise, find the Online Distribution Link the hard way - if ((link.length < 1) && (!metadataFromIndex)) - link = $(el).find(".control-label:contains('Online Distribution Info') + .controls-well > a[href*='" + id.replace(/[^A-Za-z0-9]/g, "\\$&") + "']"); - - if (link.length > 0) { - //Get the container element - container = $(link).parents(".entitydetails"); - - if (container.length < 1) { - //backup - find the parent of this link that is a direct child of the form element - var firstLevelContainer = _.intersection($(link).parents("form").children(), $(link).parents()); - //Find the controls-well inside of that first level container, which is the well that contains info about this data object - if (firstLevelContainer.length > 0) - container = $(firstLevelContainer).children(".controls-well"); - - if ((container.length < 1) && (firstLevelContainer.length > 0)) - container = firstLevelContainer; - - $(container).addClass("entitydetails"); + //Retrieve the sources and derivations for this member + var memberSources = member.get("provSources") || new Array(), + memberDerivations = member.get("provDerivations") || new Array(); + + //Make the source chart for this member. + // If edit is on, then either a 'blank' sources ProvChart will be displayed if there + // are no sources for this member, or edit icons will be displayed with prov icons. + if (memberSources.length || editModeOn) { + var memberSourcesProvChart = new ProvChart({ + sources: memberSources, + context: member, + contextEl: entityDetailsSection, + dataPackage: dataPackage, + parentView: view, + editModeOn: editModeOn, + editorType: "sources", + }); + view.subviews.push(memberSourcesProvChart); + $(entityDetailsSection).before( + memberSourcesProvChart.render().el, + ); + view.$(view.articleContainer).addClass("gutters"); } - //Add the id so we can easily find it later - container.attr("data-id", id); + //Make the derivation chart for this member + // If edit is on, then either a 'blank' derivations ProvChart will be displayed if there, + // are no derivations for this member or edit icons will be displayed with prov icons. + if (memberDerivations.length || editModeOn) { + var memberDerivationsProvChart = new ProvChart({ + derivations: memberDerivations, + context: member, + contextEl: entityDetailsSection, + dataPackage: dataPackage, + parentView: view, + editModeOn: editModeOn, + editorType: "derivations", + }); + view.subviews.push(memberDerivationsProvChart); + $(entityDetailsSection).after( + memberDerivationsProvChart.render().el, + ); + view.$(view.articleContainer).addClass("gutters"); + } + }); + } - return container; - } + //Make all of the prov chart nodes look different based on id + if (this.$(".prov-chart").length > 10000) { + var allNodes = this.$(".prov-chart .node"), + ids = [], + view = this, + i = 1; - //----Find by file name rather than id----- - if (!fileName) { - //Get the name of the object first - for (var i = 0; i < this.packageModels.length; i++) { - var model = _.findWhere(this.packageModels[i].get("members"), { id: id }); - if (model) { - fileName = model.get("fileName"); - break; - } - } - } + $(allNodes).each(function () { + ids.push($(this).attr("data-id")); + }); + ids = _.uniq(ids); + + _.each(ids, function (id) { + var matchingNodes = view + .$(".prov-chart .node[data-id='" + id + "']") + .not(".editorNode"); + //var matchingEntityDetails = view.findEntityDetailsContainer(id); + + //Don't use the unique class on images since they will look a lot different anyway by their image + if (!$(matchingNodes).first().hasClass("image")) { + var className = "uniqueNode" + i; + + //Add the unique class and up the iterator + if (matchingNodes.prop("tagName") != "polygon") + $(matchingNodes).addClass(className); + else + $(matchingNodes).attr( + "class", + $(matchingNodes).attr("class") + " " + className, + ); + + /* if(matchingEntityDetails) + $(matchingEntityDetails).addClass(className);*/ - if (fileName) { - var possibleLocations = [".entitydetails [data-object-name='" + fileName + "']", - ".entitydetails .control-label:contains('Object Name') + .controls-well:contains('" + fileName + "')", - ".entitydetails .control-label:contains('Entity Name') + .controls-well:contains('" + fileName + "')"]; - - //Search through each possible location in the DOM where the file name might be - for (var i = 0; i < possibleLocations.length; i++) { - //Get the elements in this view that match the possible location - var matches = this.$(possibleLocations[i]); - - //If exactly one match is found - if (matches.length == 1) { - //Get the entity details parent element - container = $(matches).parents(".entitydetails").first(); - //Set the object ID on the element for easier locating later - container.attr("data-id", id); - if (container.length) - break; - } + //Save this id->class mapping in this view + view.classMap.push({ + id: id, + className: className, + }); + i++; } + }); + } + }, - if (container.length) - return container; - - } + /* Step through all prov charts and re-render each one that has been + marked for re-rendering. + */ + redrawProvCharts: function () { + var view = this; + + // Check if prov edits are active and turn on the prov save bar if so. + // Alternatively, turn off save bar if there are no prov edits, which + // could occur if a user undoes a previous which could result in + // an empty edit list. + if (this.dataPackage.provEditsPending()) { + this.showEditorControls(); + } else { + this.hideEditorControls(); - //--- The last option:---- - //If this package has only one item, we can assume the only entity details are about that item - var members = this.packageModels[0].get("members"), - dataMembers = _.filter(members, function (m) { return (m.get("formatType") == "DATA"); }); - if (dataMembers.length == 1) { - if (this.$(".entitydetails").length == 1) { - this.$(".entitydetails").attr("data-id", id); - return this.$(".entitydetails"); + // Reset the edited flag for each package member + _.each(this.dataPackage.toArray(), function (item) { + item.selectedInEditor == false; + }); + } + _.each(this.subviews, function (thisView, i) { + // Check if this is a ProvChartView + if ( + thisView.className && + thisView.className.indexOf("prov-chart") !== -1 + ) { + // Check if this ProvChartView is marked for re-rendering + // Erase the current ProvChartView + thisView.onClose(); + } + }); + + // Remove prov charts from the array of subviews. + this.subviews = _.filter(this.subviews, function (item) { + return item.className && item.className.indexOf("prov-chart") == -1; + }); + + view.drawProvCharts(this.dataPackage); + }, + + /* + * When the data package collection saves successfully, tell the user + */ + saveSuccess: function (savedObject) { + //We only want to perform these actions after the package saves + if (savedObject.type != "DataPackage") return; + + //Change the URL to the new id + MetacatUI.uiRouter.navigate( + "view/" + this.dataPackage.packageModel.get("id"), + { trigger: false, replace: true }, + ); + + var message = $(document.createElement("div")).append( + $(document.createElement("span")).text( + "Your changes have been saved. ", + ), + ); + + MetacatUI.appView.showAlert(message, "alert-success", "body", 4000, { + remove: false, + }); + + // Reset the state to clean + this.dataPackage.packageModel.set("changed", false); + + // If provenance relationships were updated, then reset the edit list now. + if (this.dataPackage.provEdits.length) this.dataPackage.provEdits = []; + + this.saveProvPending = false; + this.hideSaving(); + this.stopListening(this.dataPackage, "errorSaving", this.saveError); + + // Turn off "save" footer + this.hideEditorControls(); + + // Update the metadata table header with the new resource map id. + // First find the DataPackageView for the top level package, and + // then re-render it with the update resmap id. + var view = this; + var metadataId = this.packageModels[0].getMetadata().get("id"); + _.each(this.subviews, function (thisView, i) { + // Check if this is a ProvChartView + if (thisView.type && thisView.type.indexOf("DataPackage") !== -1) { + if (thisView.currentlyViewing == metadataId) { + var packageId = view.dataPackage.packageModel.get("id"); + var title = packageId + ? '<span class="subtle">Package: ' + packageId + "</span>" + : ""; + thisView.title = "Files in this dataset " + title; + thisView.render(); } } + }); + }, - return false; - }, - - /* - * Inserts new image elements into the DOM via the image template. Use for displaying images that are part of this metadata's resource map. - */ - insertDataDetails: function () { - - //If there is a metadataIndex subview, render from there. - var metadataFromIndex = _.findWhere(this.subviews, { type: "MetadataIndex" }); - if (typeof metadataFromIndex !== "undefined") { - _.each(this.packageModels, function (packageModel) { - metadataFromIndex.insertDataDetails(packageModel); - }); - return; - } + /* + * When the data package collection fails to save, tell the user + */ + saveError: function (errorMsg) { + var errorId = "error" + Math.round(Math.random() * 100), + message = $(document.createElement("div")).append( + "<p>Your changes could not be saved.</p>", + ); - var viewRef = this; + message.append( + $(document.createElement("a")) + .text("See details") + .attr("data-toggle", "collapse") + .attr("data-target", "#" + errorId) + .addClass("pointer"), + $(document.createElement("div")) + .addClass("collapse") + .attr("id", errorId) + .append($(document.createElement("pre")).text(errorMsg)), + ); + + MetacatUI.appView.showAlert(message, "alert-error", "body", null, { + emailBody: "Error message: Data Package save error: " + errorMsg, + remove: true, + }); + + this.saveProvPending = false; + this.hideSaving(); + this.stopListening(this.dataPackage, "successSaving", this.saveSuccess); + + // Turn off "save" footer + this.hideEditorControls(); + }, + + /* If provenance relationships have been modified by the provenance editor (in ProvChartView), then + update the ORE Resource Map and save it to the server. + */ + saveProv: function () { + // Only call this function once per save operation. + if (this.saveProvPending) return; + + var view = this; + if (this.dataPackage.provEditsPending()) { + this.saveProvPending = true; + // If the Data Package failed saving, display an error message + this.listenToOnce(this.dataPackage, "errorSaving", this.saveError); + // Listen for when the package has been successfully saved + this.listenToOnce( + this.dataPackage, + "successSaving", + this.saveSuccess, + ); + this.showSaving(); + this.dataPackage.saveProv(); + } else { + //TODO: should a dialog be displayed saying that no prov edits were made? + } + }, - _.each(this.packageModels, function (packageModel) { + showSaving: function () { + //Change the style of the save button + this.$("#save-metadata-prov") + .html('<i class="icon icon-spinner icon-spin"></i> Saving...') + .addClass("btn-disabled"); - var dataDisplay = "", - images = [], - other = [], - packageMembers = packageModel.get("members"); + this.$("input, textarea, select, button").prop("disabled", true); + }, - //Don't do this for large packages - if (packageMembers.length > 150) return; + hideSaving: function () { + this.$("input, textarea, select, button").prop("disabled", false); - //==== Loop over each visual object and create a dataDisplay template for it to attach to the DOM ==== - _.each(packageMembers, function (solrResult, i) { - //Don't display any info about nested packages - if (solrResult.type == "Package") return; + //When prov is saved, revert the Save button back to normal + this.$("#save-metadata-prov").html("Save").removeClass("btn-disabled"); + }, - var objID = solrResult.get("id"); + showEditorControls: function () { + this.$("#editor-footer").slideDown(); + }, - if (objID == viewRef.pid) - return; + hideEditorControls: function () { + this.$("#editor-footer").slideUp(); + }, - //Is this a visual object (image)? - var type = solrResult.type == "SolrResult" ? solrResult.getType() : "Data set"; - if (type == "image") - images.push(solrResult); + getEntityNames: function (packageModels) { + var viewRef = this; - //Find the part of the HTML Metadata view that describes this data object - var anchor = $(document.createElement("a")).attr("id", objID.replace(/[^A-Za-z0-9]/g, "-")), - container = viewRef.findEntityDetailsContainer(objID); + _.each(packageModels, function (packageModel) { + //Don't get entity names for larger packages - users must put the names in the system metadata + if (packageModel.get("members").length > 100) return; - var downloadButton = new DownloadButtonView({ model: solrResult }); - downloadButton.render(); + //If this package has a different metadata doc than the one we are currently viewing + var metadataModel = packageModel.getMetadata(); + if (!metadataModel) return; - //Insert the data display HTML and the anchor tag to mark this spot on the page - if (container) { + if (metadataModel.get("id") != viewRef.pid) { + var requestSettings = { + url: + MetacatUI.appModel.get("viewServiceUrl") + + encodeURIComponent(metadataModel.get("id")), + success: function (parsedMetadata, response, xhr) { + _.each(packageModel.get("members"), function (solrResult, i) { + var entityName = ""; + + if (solrResult.get("formatType") == "METADATA") + entityName = solrResult.get("title"); + + var container = viewRef.findEntityDetailsContainer( + solrResult, + parsedMetadata, + ); + if (container) entityName = viewRef.getEntityName(container); + + //Set the entity name + if (entityName) { + solrResult.set("fileName", entityName); + //Update the UI with the new name + viewRef + .$( + ".entity-name-placeholder[data-id='" + + solrResult.get("id") + + "']", + ) + .text(entityName); + } + }); + }, + }; - //Only show data displays for images hosted on the same origin - if (type == "image") { + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); - //Create the data display HTML - var dataDisplay = $.parseHTML(viewRef.dataDisplayTemplate({ - type: type, - src: solrResult.get("url"), - objID: objID - }).trim()); + return; + } - //Insert into the page - if ($(container).children("label").length > 0) - $(container).children("label").first().after(dataDisplay); - else - $(container).prepend(dataDisplay); + _.each(packageModel.get("members"), function (solrResult, i) { + var entityName = ""; - //If this image is private, we need to load it via an XHR request - if (!solrResult.get("isPublic")) { - //Create an XHR - var xhr = new XMLHttpRequest(); - xhr.withCredentials = true; + if (solrResult.get("fileName")) + entityName = solrResult.get("fileName"); + else if (solrResult.get("formatType") == "METADATA") + entityName = solrResult.get("title"); + else if (solrResult.get("formatType") == "RESOURCE") return; + else { + var container = viewRef.findEntityDetailsContainer(solrResult); - xhr.onload = function () { + if (container && container.length > 0) + entityName = viewRef.getEntityName(container); + else entityName = null; + } - if (xhr.response) - $(dataDisplay).find("img").attr("src", window.URL.createObjectURL(xhr.response)); - } + //Set the entityName, even if it's null + solrResult.set("fileName", entityName); + }); + }); + }, + + getEntityName: function (containerEl) { + if (!containerEl) return false; + + var entityName = $(containerEl) + .find(".entityName") + .attr("data-entity-name"); + if (typeof entityName === "undefined" || !entityName) { + entityName = $(containerEl) + .find(".control-label:contains('Entity Name') + .controls-well") + .text(); + if (typeof entityName === "undefined" || !entityName) + entityName = null; + } - //Open and send the request with the user's auth token - xhr.open('GET', solrResult.get("url")); - xhr.responseType = "blob"; - xhr.setRequestHeader("Authorization", "Bearer " + MetacatUI.appUserModel.get("token")); - xhr.send(); - } + return entityName; + }, + + //Checks if the metadata has entity details sections + hasEntityDetails: function () { + return this.$(".entitydetails").length > 0; + }, + + /** + * Finds the element in the rendered metadata that describes the given data entity. + * + * @param {(DataONEObject|SolrResult|string)} model - Either a model that represents the data object or the identifier of the data object + * @param {Element} [el] - The DOM element to exclusivly search inside. + * @return {Element} - The DOM element that describbbes the given data entity. + */ + findEntityDetailsContainer: function (model, el) { + if (!el) var el = this.el; + + //Get the id and file name for this data object + var id = "", + fileName = ""; + + //If a model is given, get the id and file name from the object + if ( + model && + (DataONEObject.prototype.isPrototypeOf(model) || + SolrResult.prototype.isPrototypeOf(model)) + ) { + id = model.get("id"); + fileName = model.get("fileName"); + } + //If a string is given instead, it must be the id of the data object + else if (typeof model == "string") { + id = model; + } + //Otherwise, there isn't enough info to find the element, so exit + else { + return; + } - } + //If we already found it earlier, return it now + var container = this.$( + ".entitydetails[data-id='" + + id + + "'], " + + ".entitydetails[data-id='" + + DataONEObject.prototype.getXMLSafeID(id) + + "']", + ); + if (container.length) return container; + + //Are we looking for the main object that this MetadataView is displaying? + if (id == this.pid) { + if (this.$("#Metadata").length > 0) return this.$("#Metadata"); + else return this.el; + } - $(container).prepend(anchor); + //Metacat 2.4.2 and up will have the Online Distribution Link marked + var link = this.$(".entitydetails a[data-pid='" + id + "']"); - var nameLabel = $(container).find("label:contains('Entity Name')"); - if (nameLabel.length) { - $(nameLabel).parent().after(downloadButton.el); - } - } + //Otherwise, try looking for an anchor with the id matching this object's id + if (!link.length) + link = $(el).find("a#" + id.replace(/[^A-Za-z0-9]/g, "\\$&")); - }); + //Get metadata index view + var metadataFromIndex = _.findWhere(this.subviews, { + type: "MetadataIndex", + }); + if (typeof metadataFromIndex === "undefined") metadataFromIndex = null; - //==== Initialize the fancybox images ===== - // We will be checking every half-second if all the HTML has been loaded into the DOM - once they are all loaded, we can initialize the lightbox functionality. - var numImages = images.length, - //The shared lightbox options for both images - lightboxOptions = { - prevEffect: 'elastic', - nextEffect: 'elastic', - closeEffect: 'elastic', - openEffect: 'elastic', - aspectRatio: true, - closeClick: true, - afterLoad: function () { - //Create a custom HTML caption based on data stored in the DOM element - viewRef.title = viewRef.title + " <a href='" + viewRef.href + "' class='btn' target='_blank'>Download</a> "; - }, - helpers: { - title: { - type: 'outside' - } - } - }; + //Otherwise, find the Online Distribution Link the hard way + if (link.length < 1 && !metadataFromIndex) + link = $(el).find( + ".control-label:contains('Online Distribution Info') + .controls-well > a[href*='" + + id.replace(/[^A-Za-z0-9]/g, "\\$&") + + "']", + ); - if (numImages > 0) { - var numImgChecks = 0, //Keep track of how many interval checks we have so we don't wait forever for images to load - lightboxImgSelector = "a[class^='fancybox'][data-fancybox-type='image']"; + if (link.length > 0) { + //Get the container element + container = $(link).parents(".entitydetails"); - //Add additional options for images - var imgLightboxOptions = lightboxOptions; - imgLightboxOptions.type = "image"; - imgLightboxOptions.perload = 1; + if (container.length < 1) { + //backup - find the parent of this link that is a direct child of the form element + var firstLevelContainer = _.intersection( + $(link).parents("form").children(), + $(link).parents(), + ); + //Find the controls-well inside of that first level container, which is the well that contains info about this data object + if (firstLevelContainer.length > 0) + container = $(firstLevelContainer).children(".controls-well"); - var initializeImgLightboxes = function () { - numImgChecks++; + if (container.length < 1 && firstLevelContainer.length > 0) + container = firstLevelContainer; - //Initialize what images have loaded so far after 5 seconds - if (numImgChecks == 10) { - $(lightboxImgSelector).fancybox(imgLightboxOptions); - } - //When 15 seconds have passed, stop checking so we don't blow up the browser - else if (numImgChecks > 30) { - $(lightboxImgSelector).fancybox(imgLightboxOptions); - window.clearInterval(imgIntervalID); - return; - } + $(container).addClass("entitydetails"); + } - //Are all of our images loaded yet? - if (viewRef.$(lightboxImgSelector).length < numImages) return; - else { - //Initialize our lightboxes - $(lightboxImgSelector).fancybox(imgLightboxOptions); + //Add the id so we can easily find it later + container.attr("data-id", id); - //We're done - clear the interval - window.clearInterval(imgIntervalID); - } - } + return container; + } - var imgIntervalID = window.setInterval(initializeImgLightboxes, 500); + //----Find by file name rather than id----- + if (!fileName) { + //Get the name of the object first + for (var i = 0; i < this.packageModels.length; i++) { + var model = _.findWhere(this.packageModels[i].get("members"), { + id: id, + }); + if (model) { + fileName = model.get("fileName"); + break; } - }); - }, - - replaceEcoGridLinks: function () { - var viewRef = this; - - //Find the element in the DOM housing the ecogrid link - $("a:contains('ecogrid://')").each(function (i, thisLink) { + } + } - //Get the link text - var linkText = $(thisLink).text(); + if (fileName) { + var possibleLocations = [ + ".entitydetails [data-object-name='" + fileName + "']", + ".entitydetails .control-label:contains('Object Name') + .controls-well:contains('" + + fileName + + "')", + ".entitydetails .control-label:contains('Entity Name') + .controls-well:contains('" + + fileName + + "')", + ]; + + //Search through each possible location in the DOM where the file name might be + for (var i = 0; i < possibleLocations.length; i++) { + //Get the elements in this view that match the possible location + var matches = this.$(possibleLocations[i]); + + //If exactly one match is found + if (matches.length == 1) { + //Get the entity details parent element + container = $(matches).parents(".entitydetails").first(); + //Set the object ID on the element for easier locating later + container.attr("data-id", id); + if (container.length) break; + } + } - //Clean up the link text - var withoutPrefix = linkText.substring(linkText.indexOf("ecogrid://") + 10), - pid = withoutPrefix.substring(withoutPrefix.indexOf("/") + 1), - baseUrl = MetacatUI.appModel.get('resolveServiceUrl') || MetacatUI.appModel.get('objectServiceUrl'); + if (container.length) return container; + } - $(thisLink).attr('href', baseUrl + encodeURIComponent(pid)).text(pid); + //--- The last option:---- + //If this package has only one item, we can assume the only entity details are about that item + var members = this.packageModels[0].get("members"), + dataMembers = _.filter(members, function (m) { + return m.get("formatType") == "DATA"; }); - }, - - publish: function (event) { - - // target may not actually prevent click events, so double check - var disabled = $(event.target).closest("a").attr("disabled"); - if (disabled) { - return false; + if (dataMembers.length == 1) { + if (this.$(".entitydetails").length == 1) { + this.$(".entitydetails").attr("data-id", id); + return this.$(".entitydetails"); } - var publishServiceUrl = MetacatUI.appModel.get('publishServiceUrl'); - var pid = $(event.target).closest("a").attr("pid"); - var ret = confirm("Are you sure you want to publish " + pid + " with a DOI?"); - - if (ret) { + } - // show the loading icon - var message = "Publishing package...this may take a few moments"; - this.showLoading(message); + return false; + }, + + /* + * Inserts new image elements into the DOM via the image template. Use for displaying images that are part of this metadata's resource map. + */ + insertDataDetails: function () { + //If there is a metadataIndex subview, render from there. + var metadataFromIndex = _.findWhere(this.subviews, { + type: "MetadataIndex", + }); + if (typeof metadataFromIndex !== "undefined") { + _.each(this.packageModels, function (packageModel) { + metadataFromIndex.insertDataDetails(packageModel); + }); + return; + } - var identifier = null; - var viewRef = this; - var requestSettings = { - url: publishServiceUrl + pid, - type: "PUT", - xhrFields: { - withCredentials: true - }, - success: function (data, textStatus, xhr) { - // the response should have new identifier in it - identifier = $(data).find("d1\\:identifier, identifier").text(); - - if (identifier) { - viewRef.hideLoading(); - var msg = "Published data package '" + identifier + "'. If you are not redirected soon, you can view your <a href='" + MetacatUI.root + "/view/" + encodeURIComponent(identifier) + "'>published data package here</a>"; - viewRef.$el.find('.container').prepend( - viewRef.alertTemplate({ - msg: msg, - classes: 'alert-success' + var viewRef = this; + + _.each(this.packageModels, function (packageModel) { + var dataDisplay = "", + images = [], + other = [], + packageMembers = packageModel.get("members"); + + //Don't do this for large packages + if (packageMembers.length > 150) return; + + //==== Loop over each visual object and create a dataDisplay template for it to attach to the DOM ==== + _.each(packageMembers, function (solrResult, i) { + //Don't display any info about nested packages + if (solrResult.type == "Package") return; + + var objID = solrResult.get("id"); + + if (objID == viewRef.pid) return; + + //Is this a visual object (image)? + var type = + solrResult.type == "SolrResult" + ? solrResult.getType() + : "Data set"; + if (type == "image") images.push(solrResult); + + //Find the part of the HTML Metadata view that describes this data object + var anchor = $(document.createElement("a")).attr( + "id", + objID.replace(/[^A-Za-z0-9]/g, "-"), + ), + container = viewRef.findEntityDetailsContainer(objID); + + var downloadButton = new DownloadButtonView({ model: solrResult }); + downloadButton.render(); + + //Insert the data display HTML and the anchor tag to mark this spot on the page + if (container) { + //Only show data displays for images hosted on the same origin + if (type == "image") { + //Create the data display HTML + var dataDisplay = $.parseHTML( + viewRef + .dataDisplayTemplate({ + type: type, + src: solrResult.get("url"), + objID: objID, }) + .trim(), + ); + + //Insert into the page + if ($(container).children("label").length > 0) + $(container).children("label").first().after(dataDisplay); + else $(container).prepend(dataDisplay); + + //If this image is private, we need to load it via an XHR request + if (!solrResult.get("isPublic")) { + //Create an XHR + var xhr = new XMLHttpRequest(); + xhr.withCredentials = true; + + xhr.onload = function () { + if (xhr.response) + $(dataDisplay) + .find("img") + .attr("src", window.URL.createObjectURL(xhr.response)); + }; + + //Open and send the request with the user's auth token + xhr.open("GET", solrResult.get("url")); + xhr.responseType = "blob"; + xhr.setRequestHeader( + "Authorization", + "Bearer " + MetacatUI.appUserModel.get("token"), ); - - // navigate to the new view after a few seconds - setTimeout( - function () { - // avoid a double fade out/in - viewRef.$el.html(''); - viewRef.showLoading(); - MetacatUI.uiRouter.navigate("view/" + identifier, { trigger: true }) - }, - 3000); + xhr.send(); } - }, - error: function (xhr, textStatus, errorThrown) { - // show the error message, but stay on the same page - var msg = "Publish failed: " + $(xhr.responseText).find("description").text(); + } - viewRef.hideLoading(); - viewRef.showError(msg); + $(container).prepend(anchor); + + var nameLabel = $(container).find( + "label:contains('Entity Name')", + ); + if (nameLabel.length) { + $(nameLabel).parent().after(downloadButton.el); } } + }); - $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); - - } - }, + //==== Initialize the fancybox images ===== + // We will be checking every half-second if all the HTML has been loaded into the DOM - once they are all loaded, we can initialize the lightbox functionality. + var numImages = images.length, + //The shared lightbox options for both images + lightboxOptions = { + prevEffect: "elastic", + nextEffect: "elastic", + closeEffect: "elastic", + openEffect: "elastic", + aspectRatio: true, + closeClick: true, + afterLoad: function () { + //Create a custom HTML caption based on data stored in the DOM element + viewRef.title = + viewRef.title + + " <a href='" + + viewRef.href + + "' class='btn' target='_blank'>Download</a> "; + }, + helpers: { + title: { + type: "outside", + }, + }, + }; - //When the given ID from the URL is a resource map that has no metadata, do the following... - noMetadata: function (solrResultModel) { + if (numImages > 0) { + var numImgChecks = 0, //Keep track of how many interval checks we have so we don't wait forever for images to load + lightboxImgSelector = + "a[class^='fancybox'][data-fancybox-type='image']"; - this.hideLoading(); - this.$el.html(this.template()); + //Add additional options for images + var imgLightboxOptions = lightboxOptions; + imgLightboxOptions.type = "image"; + imgLightboxOptions.perload = 1; - this.pid = solrResultModel.get("resourceMap") || solrResultModel.get("id"); + var initializeImgLightboxes = function () { + numImgChecks++; - //Insert breadcrumbs - this.insertBreadcrumbs(); + //Initialize what images have loaded so far after 5 seconds + if (numImgChecks == 10) { + $(lightboxImgSelector).fancybox(imgLightboxOptions); + } + //When 15 seconds have passed, stop checking so we don't blow up the browser + else if (numImgChecks > 30) { + $(lightboxImgSelector).fancybox(imgLightboxOptions); + window.clearInterval(imgIntervalID); + return; + } - this.insertDataSource(); + //Are all of our images loaded yet? + if (viewRef.$(lightboxImgSelector).length < numImages) return; + else { + //Initialize our lightboxes + $(lightboxImgSelector).fancybox(imgLightboxOptions); - //Insert a table of contents - this.insertPackageTable(solrResultModel); + //We're done - clear the interval + window.clearInterval(imgIntervalID); + } + }; - this.renderMetadataFromIndex(); + var imgIntervalID = window.setInterval( + initializeImgLightboxes, + 500, + ); + } + }); + }, + + replaceEcoGridLinks: function () { + var viewRef = this; + + //Find the element in the DOM housing the ecogrid link + $("a:contains('ecogrid://')").each(function (i, thisLink) { + //Get the link text + var linkText = $(thisLink).text(); + + //Clean up the link text + var withoutPrefix = linkText.substring( + linkText.indexOf("ecogrid://") + 10, + ), + pid = withoutPrefix.substring(withoutPrefix.indexOf("/") + 1), + baseUrl = + MetacatUI.appModel.get("resolveServiceUrl") || + MetacatUI.appModel.get("objectServiceUrl"); + + $(thisLink) + .attr("href", baseUrl + encodeURIComponent(pid)) + .text(pid); + }); + }, + + publish: function (event) { + // target may not actually prevent click events, so double check + var disabled = $(event.target).closest("a").attr("disabled"); + if (disabled) { + return false; + } + var publishServiceUrl = MetacatUI.appModel.get("publishServiceUrl"); + var pid = $(event.target).closest("a").attr("pid"); + var ret = confirm( + "Are you sure you want to publish " + pid + " with a DOI?", + ); + + if (ret) { + // show the loading icon + var message = "Publishing package...this may take a few moments"; + this.showLoading(message); + + var identifier = null; + var viewRef = this; + var requestSettings = { + url: publishServiceUrl + pid, + type: "PUT", + xhrFields: { + withCredentials: true, + }, + success: function (data, textStatus, xhr) { + // the response should have new identifier in it + identifier = $(data).find("d1\\:identifier, identifier").text(); - //Insert a message that this data is not described by metadata - MetacatUI.appView.showAlert("Additional information about this data is limited since metadata was not provided by the creator.", "alert-warning", this.$(this.metadataContainer)); - }, + if (identifier) { + viewRef.hideLoading(); + var msg = + "Published data package '" + + identifier + + "'. If you are not redirected soon, you can view your <a href='" + + MetacatUI.root + + "/view/" + + encodeURIComponent(identifier) + + "'>published data package here</a>"; + viewRef.$el.find(".container").prepend( + viewRef.alertTemplate({ + msg: msg, + classes: "alert-success", + }), + ); + + // navigate to the new view after a few seconds + setTimeout(function () { + // avoid a double fade out/in + viewRef.$el.html(""); + viewRef.showLoading(); + MetacatUI.uiRouter.navigate("view/" + identifier, { + trigger: true, + }); + }, 3000); + } + }, + error: function (xhr, textStatus, errorThrown) { + // show the error message, but stay on the same page + var msg = + "Publish failed: " + + $(xhr.responseText).find("description").text(); + + viewRef.hideLoading(); + viewRef.showError(msg); + }, + }; - // this will lookup the latest version of the PID - showLatestVersion: function () { + $.ajax( + _.extend( + requestSettings, + MetacatUI.appUserModel.createAjaxSettings(), + ), + ); + } + }, - //If this metadata doc is not obsoleted by a new version, then exit the function - if (!this.model.get("obsoletedBy")) { - return; - } + //When the given ID from the URL is a resource map that has no metadata, do the following... + noMetadata: function (solrResultModel) { + this.hideLoading(); + this.$el.html(this.template()); - var view = this; + this.pid = + solrResultModel.get("resourceMap") || solrResultModel.get("id"); - //When the latest version is found, - this.listenTo(this.model, "change:newestVersion", function () { - //Make sure it has a newer version, and if so, - if (view.model.get("newestVersion") != view.model.get("id")) { - //Put a link to the newest version in the content - view.$(".newer-version").replaceWith(view.versionTemplate({ - pid: view.model.get("newestVersion") - })); - } - else { - view.$(".newer-version").remove(); - } - }); + //Insert breadcrumbs + this.insertBreadcrumbs(); - //Insert the newest version template with a loading message - this.$el.prepend(this.versionTemplate({ - loading: true - })); - - //Find the latest version of this metadata object - this.model.findLatestVersion(); - }, - - showLoading: function (message) { - this.hideLoading(); - - MetacatUI.appView.scrollToTop(); - - var loading = this.loadingTemplate({ msg: message }); - if (!loading) return; - - this.$loading = $($.parseHTML(loading)); - this.$detached = this.$el.children().detach(); - - this.$el.html(loading); - }, - - hideLoading: function () { - if (this.$loading) this.$loading.remove(); - if (this.$detached) this.$el.html(this.$detached); - }, - - showError: function (msg) { - //Remove any existing error messages - this.$el.children(".alert-container").remove(); - - this.$el.prepend( - this.alertTemplate({ - msg: msg, - classes: 'alert-error', - containerClasses: "page", - includeEmail: true - })); - }, - - /** - * When the "Metadata" button in the table is clicked while we are on the Metadata view, - * we want to scroll to the anchor tag of this data object within the page instead of navigating - * to the metadata page again, which refreshes the page and re-renders (more loading time) - **/ - previewData: function (e) { - //Don't go anywhere yet... - e.preventDefault(); - - //Get the target and id of the click - var link = $(e.target); - if (!$(link).hasClass("preview")) - link = $(link).parents("a.preview"); - - if (link) { - var id = $(link).attr("data-id"); - if ((typeof id === "undefined") || !id) - return false; //This will make the app defualt to the child view previewData function - } - else - return false; + this.insertDataSource(); - // If we are on the Metadata view, update the URL and scroll to the - // anchor - window.location.hash = encodeURIComponent(id); - MetacatUI.appView.scrollTo(this.findEntityDetailsContainer(id)); + //Insert a table of contents + this.insertPackageTable(solrResultModel); - return true; - }, + this.renderMetadataFromIndex(); - /** - * Try to scroll to the section on a page describing the identifier in the - * fragment/hash portion of the current page. - * - * This function depends on there being an `id` dataset attribute on an - * element on the page set to an XML-safe version of the value in the - * fragment/hash. Used to provide direct links to sub-resources on a page. - */ - scrollToFragment: function () { - var hash = window.location.hash; - - if (!hash || hash.length <= 1) { - return; - } + //Insert a message that this data is not described by metadata + MetacatUI.appView.showAlert( + "Additional information about this data is limited since metadata was not provided by the creator.", + "alert-warning", + this.$(this.metadataContainer), + ); + }, - //Get the id from the URL hash and decode it - var idFragment = decodeURIComponent(hash.substring(1)); + // this will lookup the latest version of the PID + showLatestVersion: function () { + //If this metadata doc is not obsoleted by a new version, then exit the function + if (!this.model.get("obsoletedBy")) { + return; + } - //Find the corresponding entity details section for this id - var entityDetailsEl = this.findEntityDetailsContainer(idFragment); + var view = this; + + //When the latest version is found, + this.listenTo(this.model, "change:newestVersion", function () { + //Make sure it has a newer version, and if so, + if (view.model.get("newestVersion") != view.model.get("id")) { + //Put a link to the newest version in the content + view.$(".newer-version").replaceWith( + view.versionTemplate({ + pid: view.model.get("newestVersion"), + }), + ); + } else { + view.$(".newer-version").remove(); + } + }); + + //Insert the newest version template with a loading message + this.$el.prepend( + this.versionTemplate({ + loading: true, + }), + ); + + //Find the latest version of this metadata object + this.model.findLatestVersion(); + }, + + showLoading: function (message) { + this.hideLoading(); + + MetacatUI.appView.scrollToTop(); + + var loading = this.loadingTemplate({ msg: message }); + if (!loading) return; + + this.$loading = $($.parseHTML(loading)); + this.$detached = this.$el.children().detach(); + + this.$el.html(loading); + }, + + hideLoading: function () { + if (this.$loading) this.$loading.remove(); + if (this.$detached) this.$el.html(this.$detached); + }, + + showError: function (msg) { + //Remove any existing error messages + this.$el.children(".alert-container").remove(); + + this.$el.prepend( + this.alertTemplate({ + msg: msg, + classes: "alert-error", + containerClasses: "page", + includeEmail: true, + }), + ); + }, + + /** + * When the "Metadata" button in the table is clicked while we are on the Metadata view, + * we want to scroll to the anchor tag of this data object within the page instead of navigating + * to the metadata page again, which refreshes the page and re-renders (more loading time) + **/ + previewData: function (e) { + //Don't go anywhere yet... + e.preventDefault(); + + //Get the target and id of the click + var link = $(e.target); + if (!$(link).hasClass("preview")) link = $(link).parents("a.preview"); + + if (link) { + var id = $(link).attr("data-id"); + if (typeof id === "undefined" || !id) return false; //This will make the app defualt to the child view previewData function + } else return false; + + // If we are on the Metadata view, update the URL and scroll to the + // anchor + window.location.hash = encodeURIComponent(id); + MetacatUI.appView.scrollTo(this.findEntityDetailsContainer(id)); + + return true; + }, + + /** + * Try to scroll to the section on a page describing the identifier in the + * fragment/hash portion of the current page. + * + * This function depends on there being an `id` dataset attribute on an + * element on the page set to an XML-safe version of the value in the + * fragment/hash. Used to provide direct links to sub-resources on a page. + */ + scrollToFragment: function () { + var hash = window.location.hash; + + if (!hash || hash.length <= 1) { + return; + } - if (entityDetailsEl || entityDetailsEl.length) { - MetacatUI.appView.scrollTo(entityDetailsEl); - } - }, + //Get the id from the URL hash and decode it + var idFragment = decodeURIComponent(hash.substring(1)); - /** - * Navigate to a new /view URL with a fragment - * - * Used in getModel() when the pid originally passed into MetadataView - * is not a metadata PID but is, instead, a data PID. getModel() does - * the work of finding an appropriate metadata PID for the data PID and - * this method handles re-routing to the correct URL. - * - * @param {string} metadata_pid - The new metadata PID - * @param {string} data_pid - Optional. A data PID that's part of the - * package metadata_pid exists within. - */ - navigateWithFragment: function (metadata_pid, data_pid) { - var next_route = "view/" + encodeURIComponent(metadata_pid); - - if (typeof data_pid === "string" && data_pid.length > 0) { - next_route += "#" + encodeURIComponent(data_pid); - } + //Find the corresponding entity details section for this id + var entityDetailsEl = this.findEntityDetailsContainer(idFragment); - MetacatUI.uiRouter.navigate(next_route, { trigger: true }); - }, + if (entityDetailsEl || entityDetailsEl.length) { + MetacatUI.appView.scrollTo(entityDetailsEl); + } + }, + + /** + * Navigate to a new /view URL with a fragment + * + * Used in getModel() when the pid originally passed into MetadataView + * is not a metadata PID but is, instead, a data PID. getModel() does + * the work of finding an appropriate metadata PID for the data PID and + * this method handles re-routing to the correct URL. + * + * @param {string} metadata_pid - The new metadata PID + * @param {string} data_pid - Optional. A data PID that's part of the + * package metadata_pid exists within. + */ + navigateWithFragment: function (metadata_pid, data_pid) { + var next_route = "view/" + encodeURIComponent(metadata_pid); + + if (typeof data_pid === "string" && data_pid.length > 0) { + next_route += "#" + encodeURIComponent(data_pid); + } - closePopovers: function (e) { - //If this is a popover element or an element that has a popover, don't close anything. - //Check with the .classList attribute to account for SVG elements - var svg = $(e.target).parents("svg"); + MetacatUI.uiRouter.navigate(next_route, { trigger: true }); + }, + + closePopovers: function (e) { + //If this is a popover element or an element that has a popover, don't close anything. + //Check with the .classList attribute to account for SVG elements + var svg = $(e.target).parents("svg"); + + if ( + _.contains(e.target.classList, "popover-this") || + $(e.target).parents(".popover-this").length > 0 || + $(e.target).parents(".popover").length > 0 || + _.contains(e.target.classList, "popover") || + (svg.length && _.contains(svg[0].classList, "popover-this")) + ) + return; + + //Close all active popovers + this.$(".popover-this.active").popover("hide"); + }, + + highlightNode: function (e) { + //Find the id + var id = $(e.target).attr("data-id"); + + if (typeof id === "undefined" || !id) + id = $(e.target).parents("[data-id]").attr("data-id"); + + //If there is no id, return + if (typeof id === "undefined") return false; + + //Highlight its node + $(".prov-chart .node[data-id='" + id + "']").toggleClass("active"); + + //Highlight its metadata section + if (MetacatUI.appModel.get("pid") == id) + this.$("#Metadata").toggleClass("active"); + else { + var entityDetails = this.findEntityDetailsContainer(id); + if (entityDetails) entityDetails.toggleClass("active"); + } + }, - if (_.contains(e.target.classList, "popover-this") || - ($(e.target).parents(".popover-this").length > 0) || - ($(e.target).parents(".popover").length > 0) || - _.contains(e.target.classList, "popover") || - (svg.length && _.contains(svg[0].classList, "popover-this"))) return; + onClose: function () { + var viewRef = this; - //Close all active popovers - this.$(".popover-this.active").popover("hide"); - }, + this.stopListening(); - highlightNode: function (e) { - //Find the id - var id = $(e.target).attr("data-id"); + _.each(this.subviews, function (subview) { + if (subview.onClose) subview.onClose(); + }); - if ((typeof id === "undefined") || (!id)) - id = $(e.target).parents("[data-id]").attr("data-id"); + this.packageModels = new Array(); + this.model.set(this.model.defaults); + this.pid = null; + this.dataPackage = null; + this.seriesId = null; + this.$detached = null; + this.$loading = null; - //If there is no id, return - if (typeof id === "undefined") return false; + //Put the document title back to the default + MetacatUI.appModel.resetTitle(); - //Highlight its node - $(".prov-chart .node[data-id='" + id + "']").toggleClass("active"); + //Remove view-specific classes + this.$el.removeClass("container no-stylesheet"); - //Highlight its metadata section - if (MetacatUI.appModel.get("pid") == id) - this.$("#Metadata").toggleClass("active"); - else { - var entityDetails = this.findEntityDetailsContainer(id); - if (entityDetails) - entityDetails.toggleClass("active"); - } - }, + this.$el.empty(); + }, - onClose: function () { - var viewRef = this; + /** + * Generate a string appropriate to go into the author/creator portion of + * a dataset citation from the value stored in the underlying model's + * origin field. + */ + getAuthorText: function () { + var authors = this.model.get("origin"), + count = 0, + authorText = ""; - this.stopListening(); + _.each(authors, function (author) { + count++; - _.each(this.subviews, function (subview) { - if (subview.onClose) - subview.onClose(); - }); + if (count == 6) { + authorText += ", et al. "; + return; + } else if (count > 6) { + return; + } - this.packageModels = new Array(); - this.model.set(this.model.defaults); - this.pid = null; - this.dataPackage = null; - this.seriesId = null; - this.$detached = null; - this.$loading = null; - - //Put the document title back to the default - MetacatUI.appModel.resetTitle(); - - //Remove view-specific classes - this.$el.removeClass("container no-stylesheet"); - - this.$el.empty(); - }, - - /** - * Generate a string appropriate to go into the author/creator portion of - * a dataset citation from the value stored in the underlying model's - * origin field. - */ - getAuthorText: function () { - var authors = this.model.get("origin"), - count = 0, - authorText = ""; - - _.each(authors, function (author) { - count++; - - if (count == 6) { - authorText += ", et al. "; - return; - } else if (count > 6) { - return; + if (count > 1) { + if (authors.length > 2) { + authorText += ","; } - if (count > 1) { - if (authors.length > 2) { - authorText += ","; - } - - if (count == authors.length) { - authorText += " and"; - } - - if (authors.length > 1) { - authorText += " "; - } + if (count == authors.length) { + authorText += " and"; } - authorText += author; - }); - - return authorText; - }, - - /** - * Generate a string appropriate to be used in the publisher portion of a - * dataset citation. This method falls back to the node ID when the proper - * node name cannot be fetched from the app's NodeModel instance. - */ - getPublisherText: function () { - var datasource = this.model.get("datasource"), - memberNode = MetacatUI.nodeModel.getMember(datasource); - - if (memberNode) { - return memberNode.name; - } else { - return datasource; - } - }, - - /** - * Generate a string appropriate to be used as the publication date in a - * dataset citation. - */ - getDatePublishedText: function () { - // Dataset/datePublished - // Prefer pubDate, fall back to dateUploaded so we have something to show - if (this.model.get("pubDate") !== "") { - return this.model.get("pubDate") - } else { - return this.model.get("dateUploaded") - } - }, - - /** - * Generate Schema.org-compliant JSONLD for the model bound to the view into - * the head tag of the page by `insertJSONLD`. - * - * Note: `insertJSONLD` should be called to do the actual inserting into the - * DOM. - */ - generateJSONLD: function () { - var model = this.model; - - // Determine the path (either #view or view, depending on router - // configuration) for use in the 'url' property - var href = document.location.href, - route = href.replace(document.location.origin + "/", "") - .split("/")[0]; - - // First: Create a minimal Schema.org Dataset with just the fields we - // know will come back from Solr (System Metadata fields). - // Add the rest in conditional on whether they are present. - var elJSON = { - "@context": { - "@vocab": "https://schema.org/", - }, - "@type": "Dataset", - "@id": "https://dataone.org/datasets/" + - encodeURIComponent(model.get("id")), - "datePublished": this.getDatePublishedText(), - "dateModified": model.get("dateModified"), - "publisher": { - "@type": "Organization", - "name": this.getPublisherText() - }, - "identifier": this.generateSchemaOrgIdentifier(model.get("id")), - "version": model.get("version"), - "url": "https://dataone.org/datasets/" + - encodeURIComponent(model.get("id")), - "schemaVersion": model.get("formatId"), - "isAccessibleForFree": true - }; - - // Attempt to add in a sameAs property of we have high confidence the - // identifier is a DOI - if (this.model.isDOI(model.get("id"))) { - var doi = this.getCanonicalDOIIRI(model.get("id")); - - if (doi) { - elJSON["sameAs"] = doi; + if (authors.length > 1) { + authorText += " "; } } - // Second: Add in optional fields + authorText += author; + }); - // Name - if (model.get("title")) { - elJSON["name"] = model.get("title") - } + return authorText; + }, - // Creator - if (model.get("origin")) { - elJSON["creator"] = model.get("origin").map(function (creator) { - return { - "@type": "Person", - "name": creator - }; - }); + /** + * Generate a string appropriate to be used in the publisher portion of a + * dataset citation. This method falls back to the node ID when the proper + * node name cannot be fetched from the app's NodeModel instance. + */ + getPublisherText: function () { + var datasource = this.model.get("datasource"), + memberNode = MetacatUI.nodeModel.getMember(datasource); + + if (memberNode) { + return memberNode.name; + } else { + return datasource; + } + }, + + /** + * Generate a string appropriate to be used as the publication date in a + * dataset citation. + */ + getDatePublishedText: function () { + // Dataset/datePublished + // Prefer pubDate, fall back to dateUploaded so we have something to show + if (this.model.get("pubDate") !== "") { + return this.model.get("pubDate"); + } else { + return this.model.get("dateUploaded"); + } + }, + + /** + * Generate Schema.org-compliant JSONLD for the model bound to the view into + * the head tag of the page by `insertJSONLD`. + * + * Note: `insertJSONLD` should be called to do the actual inserting into the + * DOM. + */ + generateJSONLD: function () { + var model = this.model; + + // Determine the path (either #view or view, depending on router + // configuration) for use in the 'url' property + var href = document.location.href, + route = href + .replace(document.location.origin + "/", "") + .split("/")[0]; + + // First: Create a minimal Schema.org Dataset with just the fields we + // know will come back from Solr (System Metadata fields). + // Add the rest in conditional on whether they are present. + var elJSON = { + "@context": { + "@vocab": "https://schema.org/", + }, + "@type": "Dataset", + "@id": + "https://dataone.org/datasets/" + + encodeURIComponent(model.get("id")), + datePublished: this.getDatePublishedText(), + dateModified: model.get("dateModified"), + publisher: { + "@type": "Organization", + name: this.getPublisherText(), + }, + identifier: this.generateSchemaOrgIdentifier(model.get("id")), + version: model.get("version"), + url: + "https://dataone.org/datasets/" + + encodeURIComponent(model.get("id")), + schemaVersion: model.get("formatId"), + isAccessibleForFree: true, + }; + + // Attempt to add in a sameAs property of we have high confidence the + // identifier is a DOI + if (this.model.isDOI(model.get("id"))) { + var doi = this.getCanonicalDOIIRI(model.get("id")); + + if (doi) { + elJSON["sameAs"] = doi; } + } - // Dataset/spatialCoverage - if (model.get("northBoundCoord") && - model.get("eastBoundCoord") && - model.get("southBoundCoord") && - model.get("westBoundCoord")) { - - var spatialCoverage = { - "@type": "Place", - "additionalProperty": [ - { - "@type": "PropertyValue", - "additionalType": "http://dbpedia.org/resource/Coordinate_reference_system", - "name": "Coordinate Reference System", - "value": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" - } - ], - "geo": this.generateSchemaOrgGeo(model.get("northBoundCoord"), - model.get("eastBoundCoord"), - model.get("southBoundCoord"), - model.get("westBoundCoord")), - "subjectOf": { - "@type": "CreativeWork", - "fileFormat": "application/vnd.geo+json", - "text": this.generateGeoJSONString(model.get("northBoundCoord"), - model.get("eastBoundCoord"), - model.get("southBoundCoord"), - model.get("westBoundCoord")) - } + // Second: Add in optional fields + // Name + if (model.get("title")) { + elJSON["name"] = model.get("title"); + } + // Creator + if (model.get("origin")) { + elJSON["creator"] = model.get("origin").map(function (creator) { + return { + "@type": "Person", + name: creator, }; + }); + } - elJSON.spatialCoverage = spatialCoverage; - } - - // Dataset/temporalCoverage - if (model.get("beginDate") && !model.get("endDate")) { - elJSON.temporalCoverage = model.get("beginDate"); - } else if (model.get("beginDate") && model.get("endDate")) { - elJSON.temporalCoverage = model.get("beginDate") + "/" + model.get("endDate"); - } + // Dataset/spatialCoverage + if ( + model.get("northBoundCoord") && + model.get("eastBoundCoord") && + model.get("southBoundCoord") && + model.get("westBoundCoord") + ) { + var spatialCoverage = { + "@type": "Place", + additionalProperty: [ + { + "@type": "PropertyValue", + additionalType: + "http://dbpedia.org/resource/Coordinate_reference_system", + name: "Coordinate Reference System", + value: "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + }, + ], + geo: this.generateSchemaOrgGeo( + model.get("northBoundCoord"), + model.get("eastBoundCoord"), + model.get("southBoundCoord"), + model.get("westBoundCoord"), + ), + subjectOf: { + "@type": "CreativeWork", + fileFormat: "application/vnd.geo+json", + text: this.generateGeoJSONString( + model.get("northBoundCoord"), + model.get("eastBoundCoord"), + model.get("southBoundCoord"), + model.get("westBoundCoord"), + ), + }, + }; - // Dataset/variableMeasured - if (model.get("attributeName")) { - elJSON.variableMeasured = model.get("attributeName"); - } + elJSON.spatialCoverage = spatialCoverage; + } - // Dataset/description - if (model.get("abstract")) { - elJSON.description = model.get("abstract"); - } else { - var datasets_url = "https://dataone.org/datasets/" + encodeURIComponent(model.get("id")); - elJSON.description = 'No description is available. Visit ' + datasets_url + ' for complete metadata about this dataset.'; - } + // Dataset/temporalCoverage + if (model.get("beginDate") && !model.get("endDate")) { + elJSON.temporalCoverage = model.get("beginDate"); + } else if (model.get("beginDate") && model.get("endDate")) { + elJSON.temporalCoverage = + model.get("beginDate") + "/" + model.get("endDate"); + } - // Dataset/keywords - if (model.get("keywords")) { - elJSON.keywords = model.get("keywords").join(", "); - } + // Dataset/variableMeasured + if (model.get("attributeName")) { + elJSON.variableMeasured = model.get("attributeName"); + } - return elJSON; - }, + // Dataset/description + if (model.get("abstract")) { + elJSON.description = model.get("abstract"); + } else { + var datasets_url = + "https://dataone.org/datasets/" + + encodeURIComponent(model.get("id")); + elJSON.description = + "No description is available. Visit " + + datasets_url + + " for complete metadata about this dataset."; + } - /** - * Insert Schema.org-compliant JSONLD for the model bound to the view into - * the head tag of the page (at the end). - * - * @param {object} json - JSON-LD to insert into the page - * - * Some notes: - * - * - Checks if the JSONLD already exists from the previous data view - * - If not create a new script tag and append otherwise replace the text - * for the script - */ - insertJSONLD: function (json) { - if (!document.getElementById('jsonld')) { - var el = document.createElement('script'); - el.type = 'application/ld+json'; - el.id = 'jsonld'; - el.text = JSON.stringify(json); - document.querySelector('head').appendChild(el); - } else { - var script = document.getElementById('jsonld'); - script.text = JSON.stringify(json); - } - }, + // Dataset/keywords + if (model.get("keywords")) { + elJSON.keywords = model.get("keywords").join(", "); + } - /** - * Generate a Schema.org/identifier from the model's id - * - * Tries to use the PropertyValue pattern when the identifier is a DOI - * and falls back to a Text value otherwise - * - * @param {string} identifier - The raw identifier - */ - generateSchemaOrgIdentifier: function (identifier) { - if (!this.model.isDOI()) { - return identifier; - } + return elJSON; + }, + + /** + * Insert Schema.org-compliant JSONLD for the model bound to the view into + * the head tag of the page (at the end). + * + * @param {object} json - JSON-LD to insert into the page + * + * Some notes: + * + * - Checks if the JSONLD already exists from the previous data view + * - If not create a new script tag and append otherwise replace the text + * for the script + */ + insertJSONLD: function (json) { + if (!document.getElementById("jsonld")) { + var el = document.createElement("script"); + el.type = "application/ld+json"; + el.id = "jsonld"; + el.text = JSON.stringify(json); + document.querySelector("head").appendChild(el); + } else { + var script = document.getElementById("jsonld"); + script.text = JSON.stringify(json); + } + }, + + /** + * Generate a Schema.org/identifier from the model's id + * + * Tries to use the PropertyValue pattern when the identifier is a DOI + * and falls back to a Text value otherwise + * + * @param {string} identifier - The raw identifier + */ + generateSchemaOrgIdentifier: function (identifier) { + if (!this.model.isDOI()) { + return identifier; + } - var doi = this.getCanonicalDOIIRI(identifier); + var doi = this.getCanonicalDOIIRI(identifier); - if (!doi) { - return identifier; - } + if (!doi) { + return identifier; + } + return { + "@type": "PropertyValue", + propertyID: "https://registry.identifiers.org/registry/doi", + value: doi.replace("https://doi.org/", "doi:"), + url: doi, + }; + }, + + /** + * Generate a Schema.org/Place/geo from bounding coordinates + * + * Either generates a GeoCoordinates (when the north and east coords are + * the same) or a GeoShape otherwise. + */ + generateSchemaOrgGeo: function (north, east, south, west) { + if (north === south) { return { - "@type": "PropertyValue", - "propertyID": "https://registry.identifiers.org/registry/doi", - "value": doi.replace("https://doi.org/", "doi:"), - "url": doi - } - }, - - /** - * Generate a Schema.org/Place/geo from bounding coordinates - * - * Either generates a GeoCoordinates (when the north and east coords are - * the same) or a GeoShape otherwise. - */ - generateSchemaOrgGeo: function (north, east, south, west) { - if (north === south) { - return { - "@type": "GeoCoordinates", - "latitude": north, - "longitude": west - } - } else { - return { - "@type": "GeoShape", - "box": west + ", " + south + " " + east + ", " + north - } - } - }, - - /** - * Creates a (hopefully) valid geoJSON string from the a set of bounding - * coordinates from the Solr index (north, east, south, west). - * - * This function produces either a GeoJSON Point or Polygon depending on - * whether the north and south bounding coordinates are the same. - * - * Part of the reason for factoring this out, in addition to code - * organization issues, is that the GeoJSON spec requires us to modify - * the raw result from Solr when the coverage crosses -180W which is common - * for datasets that cross the Pacific Ocean. In this case, We need to - * convert the east bounding coordinate from degrees west to degrees east. - * - * e.g., if the east bounding coordinate is 120 W and west bounding - * coordinate is 140 E, geoJSON requires we specify 140 E as 220 - * - * @param {number} north - North bounding coordinate - * @param {number} east - East bounding coordinate - * @param {number} south - South bounding coordinate - * @param {number} west - West bounding coordinate - */ - generateGeoJSONString: function (north, east, south, west) { - if (north === south) { - return this.generateGeoJSONPoint(north, east); - } else { - return this.generateGeoJSONPolygon(north, east, south, west); - } - }, + "@type": "GeoCoordinates", + latitude: north, + longitude: west, + }; + } else { + return { + "@type": "GeoShape", + box: west + ", " + south + " " + east + ", " + north, + }; + } + }, + + /** + * Creates a (hopefully) valid geoJSON string from the a set of bounding + * coordinates from the Solr index (north, east, south, west). + * + * This function produces either a GeoJSON Point or Polygon depending on + * whether the north and south bounding coordinates are the same. + * + * Part of the reason for factoring this out, in addition to code + * organization issues, is that the GeoJSON spec requires us to modify + * the raw result from Solr when the coverage crosses -180W which is common + * for datasets that cross the Pacific Ocean. In this case, We need to + * convert the east bounding coordinate from degrees west to degrees east. + * + * e.g., if the east bounding coordinate is 120 W and west bounding + * coordinate is 140 E, geoJSON requires we specify 140 E as 220 + * + * @param {number} north - North bounding coordinate + * @param {number} east - East bounding coordinate + * @param {number} south - South bounding coordinate + * @param {number} west - West bounding coordinate + */ + generateGeoJSONString: function (north, east, south, west) { + if (north === south) { + return this.generateGeoJSONPoint(north, east); + } else { + return this.generateGeoJSONPolygon(north, east, south, west); + } + }, - /** + /** * Generate a GeoJSON Point object * * @param {number} north - North bounding coordinate @@ -3053,165 +3481,190 @@

Source: src/js/views/MetadataView.js

* ]} */ - generateGeoJSONPoint: function (north, east) { - var preamble = "{\"type\":\"Point\",\"coordinates\":", - inner = "[" + east + "," + north + "]", - postamble = "}"; - - return preamble + inner + postamble; - }, - - /** - * Generate a GeoJSON Polygon object from - * - * @param {number} north - North bounding coordinate - * @param {number} east - East bounding coordinate - * @param {number} south - South bounding coordinate - * @param {number} west - West bounding coordinate - * - * - * Example: - * - * { - * "type": "Polygon", - * "coordinates": [[ - * [ 100, 0 ], - * [ 101, 0 ], - * [ 101, 1 ], - * [ 100, 1 ], - * [ 100, 0 ] - * ]} - * - */ - generateGeoJSONPolygon: function (north, east, south, west) { - var preamble = "{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\"\:\"Polygon\",\"coordinates\":[["; - - // Handle the case when the polygon wraps across the 180W/180E boundary - if (east < west) { - east = 360 - east - } - - var inner = "[" + west + "," + south + "]," + - "[" + east + "," + south + "]," + - "[" + east + "," + north + "]," + - "[" + west + "," + north + "]," + - "[" + west + "," + south + "]"; - - var postamble = "]]}}"; - - return preamble + inner + postamble; - }, - - /** - * Create a canonical IRI for a DOI given a random DataONE identifier. - * - * @param {string} identifier: The identifier to (possibly) create the IRI - * for. - * @return {string|null} Returns null when matching the identifier to a DOI - * regex fails or a string when the match is successful - * - * Useful for describing resources identified by DOIs in linked open data - * contexts or possibly also useful for comparing two DOIs for equality. - * - * Note: Really could be generalized to more identifier schemes. - */ - getCanonicalDOIIRI: function (identifier) { - return MetacatUI.appModel.DOItoURL(identifier) || null; - }, - - /** - * Insert citation information as meta tags into the head of the page - * - * Currently supports Highwire Press style tags (citation_) which is - * supposedly what Google (Scholar), Mendeley, and Zotero support. - */ - insertCitationMetaTags: function () { - // Generate template data to use for all templates - var title = this.model.get("title"), - authors = this.model.get("origin"), - publisher = this.getPublisherText(), - date = new Date(this.getDatePublishedText()).getUTCFullYear().toString(), - isDOI = this.model.isDOI(this.model.get("id")), - id = this.model.get("id"), - abstract = this.model.get("abstract"); - - // Generate HTML strings from each template - var hwpt = this.metaTagsHighwirePressTemplate({ - title: title, - authors: authors, - publisher: publisher, - date: date, - isDOI: isDOI, - id: id, - abstract - }); + generateGeoJSONPoint: function (north, east) { + var preamble = '{"type":"Point","coordinates":', + inner = "[" + east + "," + north + "]", + postamble = "}"; + + return preamble + inner + postamble; + }, + + /** + * Generate a GeoJSON Polygon object from + * + * @param {number} north - North bounding coordinate + * @param {number} east - East bounding coordinate + * @param {number} south - South bounding coordinate + * @param {number} west - West bounding coordinate + * + * + * Example: + * + * { + * "type": "Polygon", + * "coordinates": [[ + * [ 100, 0 ], + * [ 101, 0 ], + * [ 101, 1 ], + * [ 100, 1 ], + * [ 100, 0 ] + * ]} + * + */ + generateGeoJSONPolygon: function (north, east, south, west) { + var preamble = + '{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[['; + + // Handle the case when the polygon wraps across the 180W/180E boundary + if (east < west) { + east = 360 - east; + } - // Clear any that are already in the document. - $("meta[name='citation_title']").remove(); - $("meta[name='citation_authors']").remove(); - $("meta[name='citation_author']").remove(); - $("meta[name='citation_publisher']").remove(); - $("meta[name='citation_date']").remove(); - $("meta[name='citation_doi']").remove(); - $("meta[name='citation_abstract']").remove(); - - // Insert - document.head.insertAdjacentHTML("beforeend", hwpt); - - // Update Zotero - // https://www.zotero.org/support/dev/exposing_metadata#force_zotero_to_refresh_metadata - document.dispatchEvent(new Event('ZoteroItemUpdated', { + var inner = + "[" + + west + + "," + + south + + "]," + + "[" + + east + + "," + + south + + "]," + + "[" + + east + + "," + + north + + "]," + + "[" + + west + + "," + + north + + "]," + + "[" + + west + + "," + + south + + "]"; + + var postamble = "]]}}"; + + return preamble + inner + postamble; + }, + + /** + * Create a canonical IRI for a DOI given a random DataONE identifier. + * + * @param {string} identifier: The identifier to (possibly) create the IRI + * for. + * @return {string|null} Returns null when matching the identifier to a DOI + * regex fails or a string when the match is successful + * + * Useful for describing resources identified by DOIs in linked open data + * contexts or possibly also useful for comparing two DOIs for equality. + * + * Note: Really could be generalized to more identifier schemes. + */ + getCanonicalDOIIRI: function (identifier) { + return MetacatUI.appModel.DOItoURL(identifier) || null; + }, + + /** + * Insert citation information as meta tags into the head of the page + * + * Currently supports Highwire Press style tags (citation_) which is + * supposedly what Google (Scholar), Mendeley, and Zotero support. + */ + insertCitationMetaTags: function () { + // Generate template data to use for all templates + var title = this.model.get("title"), + authors = this.model.get("origin"), + publisher = this.getPublisherText(), + date = new Date(this.getDatePublishedText()) + .getUTCFullYear() + .toString(), + isDOI = this.model.isDOI(this.model.get("id")), + id = this.model.get("id"), + abstract = this.model.get("abstract"); + + // Generate HTML strings from each template + var hwpt = this.metaTagsHighwirePressTemplate({ + title: title, + authors: authors, + publisher: publisher, + date: date, + isDOI: isDOI, + id: id, + abstract, + }); + + // Clear any that are already in the document. + $("meta[name='citation_title']").remove(); + $("meta[name='citation_authors']").remove(); + $("meta[name='citation_author']").remove(); + $("meta[name='citation_publisher']").remove(); + $("meta[name='citation_date']").remove(); + $("meta[name='citation_doi']").remove(); + $("meta[name='citation_abstract']").remove(); + + // Insert + document.head.insertAdjacentHTML("beforeend", hwpt); + + // Update Zotero + // https://www.zotero.org/support/dev/exposing_metadata#force_zotero_to_refresh_metadata + document.dispatchEvent( + new Event("ZoteroItemUpdated", { bubbles: true, - cancelable: true - })); - }, - - createAnnotationViews: function () { - - try { - var viewRef = this; - - _.each($(".annotation"), function (annoEl) { - var newView = new AnnotationView({ - el: annoEl - }); - viewRef.subviews.push(newView); - }); - } - catch (e) { - console.error(e); - } - }, + cancelable: true, + }), + ); + }, - insertMarkdownViews: function () { + createAnnotationViews: function () { + try { var viewRef = this; - _.each($(".markdown"), function (markdownEl) { - var newView = new MarkdownView({ - markdown: $(markdownEl).text().trim(), - el: $(markdownEl).parent() + _.each($(".annotation"), function (annoEl) { + var newView = new AnnotationView({ + el: annoEl, }); - viewRef.subviews.push(newView); + }); + } catch (e) { + console.error(e); + } + }, - // Clear out old content before rendering - $(markdownEl).remove(); + insertMarkdownViews: function () { + var viewRef = this; - newView.render(); + _.each($(".markdown"), function (markdownEl) { + var newView = new MarkdownView({ + markdown: $(markdownEl).text().trim(), + el: $(markdownEl).parent(), }); - }, - storeEntityPIDs: function(responseEl) { - var view = this; - _.each($(responseEl).find(".entitydetails"), function (entityEl) { - var entityId = $(entityEl).data("id"); - view.entities.push(entityId.replace('urn-uuid-', 'urn:uuid:')); - }); - } - }); + viewRef.subviews.push(newView); + + // Clear out old content before rendering + $(markdownEl).remove(); + + newView.render(); + }); + }, + + storeEntityPIDs: function (responseEl) { + var view = this; + _.each($(responseEl).find(".entitydetails"), function (entityEl) { + var entityId = $(entityEl).data("id"); + view.entities.push(entityId.replace("urn-uuid-", "urn:uuid:")); + }); + }, + }, + ); - return MetadataView; - }); + return MetadataView; +});
diff --git a/docs/docs/src_js_views_MetricModalView.js.html b/docs/docs/src_js_views_MetricModalView.js.html index 9b2e3e550..00c6e5dba 100644 --- a/docs/docs/src_js_views_MetricModalView.js.html +++ b/docs/docs/src_js_views_MetricModalView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/MetricModalView.js

-
/*global define */
-define([
+            
define([
   "jquery",
   "underscore",
   "backbone",
@@ -62,7 +61,7 @@ 

Source: src/js/views/MetricModalView.js

MetricModalTemplate, Citations, CitationList, - SignInView + SignInView, ) { "use strict"; @@ -212,7 +211,7 @@

Source: src/js/views/MetricModalView.js

// Get the current metric name and associated options const metric = this.metricName || this.metrics[0].name; const metricOpts = this.metrics.find( - (metric) => metric.name === this.metricName + (metric) => metric.name === this.metricName, ); // Get the name in the singular form in lower case. @@ -273,7 +272,7 @@

Source: src/js/views/MetricModalView.js

getMetricAtOffset: function (n) { const currentMetricName = this.metricName || this.metrics[0].name; const currentMetricIndex = this.metrics.findIndex( - (metric) => metric.name === currentMetricName + (metric) => metric.name === currentMetricName, ); let metricIndex = (currentMetricIndex + n) % this.metrics.length; if (metricIndex < 0) { @@ -358,7 +357,7 @@

Source: src/js/views/MetricModalView.js

this.teardown(); require(["views/RegisterCitationView"], function ( - RegisterCitationView + RegisterCitationView, ) { // display a register citation modal var registerCitationView = new RegisterCitationView({ @@ -375,7 +374,7 @@

Source: src/js/views/MetricModalView.js

*/ showSignIn: function () { var container = $(document.createElement("div")).addClass( - "container center" + "container center", ); this.$el.html(container); @@ -390,7 +389,7 @@

Source: src/js/views/MetricModalView.js

//Add the elements to the page $(container).append( "<h1>Sign in to register citations</h1>", - signInButtons + signInButtons, ); }, @@ -415,7 +414,7 @@

Source: src/js/views/MetricModalView.js

// Prepend to modal-body this.$el.find(".modal-body").prepend(chartContainer); var metricCount = MetacatUI.appView.currentView.metricsModel.get( - this.metricName.toLowerCase() + this.metricName.toLowerCase(), ); var metricMonths = MetacatUI.appView.currentView.metricsModel.get("months"); @@ -454,7 +453,7 @@

Source: src/js/views/MetricModalView.js

onClose: function () { this.teardown(); }, - } + }, ); return MetricModalView; diff --git a/docs/docs/src_js_views_MetricView.js.html b/docs/docs/src_js_views_MetricView.js.html index 09d5e49f8..6994d0a20 100644 --- a/docs/docs/src_js_views_MetricView.js.html +++ b/docs/docs/src_js_views_MetricView.js.html @@ -44,181 +44,229 @@

Source: src/js/views/MetricView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'views/MetricModalView'],
-    function($, _, Backbone, MetricModalView) {
-    'use strict';
-
-    /**
-    * @class MetricView
-    * @classdesc the display of the dataset citation and usage metrics on the dataset landing page
-    * @classcategory Views
-    * @screenshot views/MetricView.png
-    * @extends Backbone.View
-    */
-    var MetricView = Backbone.View.extend(
-        /** @lends MetricView.prototype */{
-
-        tagName: 'a',
-        // id: 'metrics-button',
-
-        /**
-        * Class name to be applied to the metric buttons
-        * @type {string}
-        */
-        className: 'btn metrics',
-
-        /**
-        * Attribute to indicate the type of metric
-        * @type {string}
-        */
-        metricName: null,
-
-        /**
-        * The Metric Model associated with this view
-        * @type {MetricsModel}
-        */
-        model: null,
-
-        //Templates
-        metricButtonTemplate:  _.template( "<span class='metric-icon'> <i class='icon" +
-                            " <%=metricIcon%>'></i> </span>" +
-                            "<span class='metric-name'> <%=metricName%> </span>" +
-                            "<span class='metric-value'> <i class='icon metric-icon icon-spinner icon-spin'>" +
-                            "</i> </span>"),
-
-        events: {
-            "click" : "showMetricModal",
-        },
-
-        /**
-        * @param {Object} options - A literal object with options to pass to the view
-        * @property {MetricsModel} options.model - The MetricsModel object associated with this view
-        * @property {string} options.metricName - The name of the metric view
-        * @property {string} options.pid - Associated dataset identifier with this view
-        */
-        initialize: function(options){
-            if((typeof options == "undefined")){
-                var options = {};
-            }
+            
define(["jquery", "underscore", "backbone", "views/MetricModalView"], function (
+  $,
+  _,
+  Backbone,
+  MetricModalView,
+) {
+  "use strict";
+
+  /**
+   * @class MetricView
+   * @classdesc the display of the dataset citation and usage metrics on the dataset landing page
+   * @classcategory Views
+   * @screenshot views/MetricView.png
+   * @extends Backbone.View
+   */
+  var MetricView = Backbone.View.extend(
+    /** @lends MetricView.prototype */ {
+      tagName: "a",
+      // id: 'metrics-button',
+
+      /**
+       * Class name to be applied to the metric buttons
+       * @type {string}
+       */
+      className: "btn metrics",
+
+      /**
+       * Attribute to indicate the type of metric
+       * @type {string}
+       */
+      metricName: null,
+
+      /**
+       * The Metric Model associated with this view
+       * @type {MetricsModel}
+       */
+      model: null,
+
+      //Templates
+      metricButtonTemplate: _.template(
+        "<span class='metric-icon'> <i class='icon" +
+          " <%=metricIcon%>'></i> </span>" +
+          "<span class='metric-name'> <%=metricName%> </span>" +
+          "<span class='metric-value'> <i class='icon metric-icon icon-spinner icon-spin'>" +
+          "</i> </span>",
+      ),
+
+      events: {
+        click: "showMetricModal",
+      },
+
+      /**
+       * @param {Object} options - A literal object with options to pass to the view
+       * @property {MetricsModel} options.model - The MetricsModel object associated with this view
+       * @property {string} options.metricName - The name of the metric view
+       * @property {string} options.pid - Associated dataset identifier with this view
+       */
+      initialize: function (options) {
+        if (typeof options == "undefined") {
+          var options = {};
+        }
 
-            this.metricName = options.metricName;
-            this.model = options.model;
-            this.pid = options.pid;
-        },
-
-        /**
-        * Renders the apprpriate metric view on the UI
-        */
-        render: function () {
-            // Generating the Button view for the given metric
-            if  (this.metricName == 'Citations') {
-                this.$el.html(this.metricButtonTemplate({metricValue:'', metricIcon:'icon-quote-right', metricName:this.metricName}));
-            } else if (this.metricName == 'Downloads') {
-                this.$el.html(this.metricButtonTemplate({metricValue:'', metricIcon:'icon-cloud-download', metricName:this.metricName}));
-            } else if (this.metricName == 'Views') {
-                this.$el.html(this.metricButtonTemplate({metricValue:'', metricIcon:'icon-eye-open', metricName:this.metricName}));
-            } else {
-                this.$el.html('');
-            };
-
-            // Adding tool-tip for the buttons
-            // TODO: Change to 'Show metricName', once you've the modals working.
-            if (MetacatUI.appModel.get("displayDatasetMetricsTooltip")) {
-                this.$el.addClass("tooltip-this")
-                        .attr("data-placement", "top")
-                        .attr("data-trigger", "hover")
-                        .attr("data-delay", "700")
-                        .attr("data-container", "body");
-                if  (this.metricName == 'Citations') {
-                    this.$el.attr("data-title", "For all the versions of this dataset, the number of times that all or part of this dataset was cited.");
-                } else if (this.metricName == 'Downloads') {
-                    this.$el.attr("data-title", "For all the versions of this dataset, the number of times that all or part of this dataset was downloaded.");
-                } else if (this.metricName == 'Views') {
-                    this.$el.attr("data-title", "For all the versions of this dataset, the number of times that all or part of this dataset was viewed.");
-                } else {
-                    this.$el.attr("data-title", "");
-                }
-            };
-
-            // waiting for the fetch() call to succeed.
-            this.listenTo(this.model, "sync", this.renderResults);
-
-            // in case when there is an error for the fetch call.
-            this.listenTo(this.model, "error", this.renderError);
-
-            return this;
-        },
-
-
-        /**
-        * Handles the click functions and displays appropriate modals on click events
-        */
-        showMetricModal: function(e) {
-            if (MetacatUI.appModel.get("displayMetricModals") ) {
-                var modalView = new MetricModalView({metricName: this.metricName, metricsModel: this.model, pid: this.pid});
-                modalView.render();
-                this.modalView = modalView;
-
-                if( Array.isArray(this.subviews) ){
-                  this.subviews.push(modalView);
-                }
-                else{
-                  this.subviews = [modalView];
-                }
-
-                //Track this event
-                MetacatUI.analytics?.trackEvent("metrics", "Click metric", this.metricName);
-            }
-        },
-
-        /**
-         * Displays the metrics count and badge on the landing page
-         */
-        renderResults: function() {
-            var total = this.model.get("total"+this.metricName);
-            // Check if the metric object exists in results obtained from the service
-            // If it does, get its total value else set the total count to 0
-
-            // Replacing the metric total count with the spinning icon.
-
-            this.$('.metric-value').text(MetacatUI.appView.numberAbbreviator(total, 1));
-            this.$('.metric-value').addClass("badge");
-
-        },
-
-        /**
-         * Manages error handling in case Metrics Service does not responsd
-         */
-        renderError: function() {
-            // Replacing the spinning icon with a question-mark
-            // when the metrics are not loaded
-            var iconEl = this.$('.metric-value').find('.metric-icon');
-            iconEl.removeClass('icon-spinner');
-            iconEl.removeClass('icon-spin');
-            iconEl.addClass("icon-exclamation-sign more-info");
-
-            // Setting the error tool-tip
-            this.$el.removeAttr("data-title");
-
-            this.$el.addClass("metrics-button-disabled");
-            this.$el.attr("data-title", "The number of " + this.metricName + " could not be retreived at this time.");
-        },
-
-        /**
-         * Cleans up listeners from this view
-         */
-        onClose: function(){
-          _.each(this.subviews, function(view){
-            if( view.onClose ){
-              view.onClose();
-            }
-          }, this);
+        this.metricName = options.metricName;
+        this.model = options.model;
+        this.pid = options.pid;
+      },
+
+      /**
+       * Renders the apprpriate metric view on the UI
+       */
+      render: function () {
+        // Generating the Button view for the given metric
+        if (this.metricName == "Citations") {
+          this.$el.html(
+            this.metricButtonTemplate({
+              metricValue: "",
+              metricIcon: "icon-quote-right",
+              metricName: this.metricName,
+            }),
+          );
+        } else if (this.metricName == "Downloads") {
+          this.$el.html(
+            this.metricButtonTemplate({
+              metricValue: "",
+              metricIcon: "icon-cloud-download",
+              metricName: this.metricName,
+            }),
+          );
+        } else if (this.metricName == "Views") {
+          this.$el.html(
+            this.metricButtonTemplate({
+              metricValue: "",
+              metricIcon: "icon-eye-open",
+              metricName: this.metricName,
+            }),
+          );
+        } else {
+          this.$el.html("");
         }
 
-    });
+        // Adding tool-tip for the buttons
+        // TODO: Change to 'Show metricName', once you've the modals working.
+        if (MetacatUI.appModel.get("displayDatasetMetricsTooltip")) {
+          this.$el
+            .addClass("tooltip-this")
+            .attr("data-placement", "top")
+            .attr("data-trigger", "hover")
+            .attr("data-delay", "700")
+            .attr("data-container", "body");
+          if (this.metricName == "Citations") {
+            this.$el.attr(
+              "data-title",
+              "For all the versions of this dataset, the number of times that all or part of this dataset was cited.",
+            );
+          } else if (this.metricName == "Downloads") {
+            this.$el.attr(
+              "data-title",
+              "For all the versions of this dataset, the number of times that all or part of this dataset was downloaded.",
+            );
+          } else if (this.metricName == "Views") {
+            this.$el.attr(
+              "data-title",
+              "For all the versions of this dataset, the number of times that all or part of this dataset was viewed.",
+            );
+          } else {
+            this.$el.attr("data-title", "");
+          }
+        }
 
-    return MetricView;
+        // waiting for the fetch() call to succeed.
+        this.listenTo(this.model, "sync", this.renderResults);
+
+        // in case when there is an error for the fetch call.
+        this.listenTo(this.model, "error", this.renderError);
+
+        return this;
+      },
+
+      /**
+       * Handles the click functions and displays appropriate modals on click events
+       */
+      showMetricModal: function (e) {
+        if (MetacatUI.appModel.get("displayMetricModals")) {
+          var modalView = new MetricModalView({
+            metricName: this.metricName,
+            metricsModel: this.model,
+            pid: this.pid,
+          });
+          modalView.render();
+          this.modalView = modalView;
+
+          if (Array.isArray(this.subviews)) {
+            this.subviews.push(modalView);
+          } else {
+            this.subviews = [modalView];
+          }
+
+          //Track this event
+          MetacatUI.analytics?.trackEvent(
+            "metrics",
+            "Click metric",
+            this.metricName,
+          );
+        }
+      },
+
+      /**
+       * Displays the metrics count and badge on the landing page
+       */
+      renderResults: function () {
+        var total = this.model.get("total" + this.metricName);
+        // Check if the metric object exists in results obtained from the service
+        // If it does, get its total value else set the total count to 0
+
+        // Replacing the metric total count with the spinning icon.
+
+        this.$(".metric-value").text(
+          MetacatUI.appView.numberAbbreviator(total, 1),
+        );
+        this.$(".metric-value").addClass("badge");
+      },
+
+      /**
+       * Manages error handling in case Metrics Service does not responsd
+       */
+      renderError: function () {
+        // Replacing the spinning icon with a question-mark
+        // when the metrics are not loaded
+        var iconEl = this.$(".metric-value").find(".metric-icon");
+        iconEl.removeClass("icon-spinner");
+        iconEl.removeClass("icon-spin");
+        iconEl.addClass("icon-exclamation-sign more-info");
+
+        // Setting the error tool-tip
+        this.$el.removeAttr("data-title");
+
+        this.$el.addClass("metrics-button-disabled");
+        this.$el.attr(
+          "data-title",
+          "The number of " +
+            this.metricName +
+            " could not be retreived at this time.",
+        );
+      },
+
+      /**
+       * Cleans up listeners from this view
+       */
+      onClose: function () {
+        _.each(
+          this.subviews,
+          function (view) {
+            if (view.onClose) {
+              view.onClose();
+            }
+          },
+          this,
+        );
+      },
+    },
+  );
+
+  return MetricView;
 });
 
diff --git a/docs/docs/src_js_views_MetricsChartView.js.html b/docs/docs/src_js_views_MetricsChartView.js.html index 1b8af9b7c..bbbe8e221 100644 --- a/docs/docs/src_js_views_MetricsChartView.js.html +++ b/docs/docs/src_js_views_MetricsChartView.js.html @@ -44,64 +44,70 @@

Source: src/js/views/MetricsChartView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'd3'],
-    function($, _, Backbone, d3) {
-    'use strict';
-
-    /**
-    * @class MetricsChartView
-    * @classdesc The MetricsChartView will render an SVG times-series chart using D3 that shows the number of metrics over time.
-    * @screenshot views/MetricsChartView.png
-    * @classcategory Views
-    * @extends Backbone.View
-    */
-    var MetricsChartView = Backbone.View.extend(
-      /** @lends MetricsChartView.prototype */{
-
-        initialize: function (options) {
-
-            if(!d3){ console.log('SVG is not supported'); return null; }
-
-            if(typeof options !== "undefined"){
+            
define(["jquery", "underscore", "backbone", "d3"], function (
+  $,
+  _,
+  Backbone,
+  d3,
+) {
+  "use strict";
+
+  /**
+   * @class MetricsChartView
+   * @classdesc The MetricsChartView will render an SVG times-series chart using D3 that shows the number of metrics over time.
+   * @screenshot views/MetricsChartView.png
+   * @classcategory Views
+   * @extends Backbone.View
+   */
+  var MetricsChartView = Backbone.View.extend(
+    /** @lends MetricsChartView.prototype */ {
+      initialize: function (options) {
+        if (!d3) {
+          console.log("SVG is not supported");
+          return null;
+        }
 
-            this.model        = options.model         || null;    // TODO: figure out how to set the model on this view
+        if (typeof options !== "undefined") {
+          this.model = options.model || null; // TODO: figure out how to set the model on this view
 
-            this.metricCount  = options.metricCount   || "0";     // for now, use individual arrays
-            this.metricMonths = options.metricMonths  || "0";
-            this.id           = options.id            || "metrics-chart";
-            this.viewType     = options.type          || "dataset";
-            this.width        = options.width         || 600;
-            this.height       = options.height        || 370;
-            this.metricName   = options.metricName;
+          this.metricCount = options.metricCount || "0"; // for now, use individual arrays
+          this.metricMonths = options.metricMonths || "0";
+          this.id = options.id || "metrics-chart";
+          this.viewType = options.type || "dataset";
+          this.width = options.width || 600;
+          this.height = options.height || 370;
+          this.metricName = options.metricName;
+        }
+      },
 
+      // http://stackoverflow.com/questions/9651167/svg-not-rendering-properly-as-a-backbone-view
+      // Give our el a svg namespace because Backbone gives a different one automatically
+      nameSpace: "http://www.w3.org/2000/svg",
+      _ensureElement: function () {
+        if (!this.el) {
+          var attrs = _.extend({}, _.result(this, "attributes"));
+          if (this.id) attrs.id = _.result(this, "id");
+          if (this.className) attrs["class"] = _.result(this, "className");
+          var $el = $(
+            window.document.createElementNS(
+              _.result(this, "nameSpace"),
+              _.result(this, "tagName"),
+            ),
+          ).attr(attrs);
+          this.setElement($el, false);
+        } else {
+          this.setElement(_.result(this, "el"), false);
         }
       },
 
-        // http://stackoverflow.com/questions/9651167/svg-not-rendering-properly-as-a-backbone-view
-        // Give our el a svg namespace because Backbone gives a different one automatically
-        nameSpace: "http://www.w3.org/2000/svg",
-        _ensureElement: function() {
-           if (!this.el) {
-              var attrs = _.extend({}, _.result(this, 'attributes'));
-              if (this.id) attrs.id = _.result(this, 'id');
-              if (this.className) attrs['class'] = _.result(this, 'className');
-              var $el = $(window.document.createElementNS(_.result(this, 'nameSpace'), _.result(this, 'tagName'))).attr(attrs);
-              this.setElement($el, false);
-           } else {
-              this.setElement(_.result(this, 'el'), false);
-           }
-       },
-
-       tagName: "svg",
+      tagName: "svg",
 
       /** Renders this Metric Chart view. */
-      render: function(){
-
+      render: function () {
         //Clear out any view elements in case this is a re-render
         this.$el.empty();
 
-          /*
+        /*
           * ========================================================================
           *  NAMING CONVENTIONS:
 
@@ -112,90 +118,108 @@ 

Source: src/js/views/MetricsChartView.js

* ======================================================================== */ - // check if there have been any views/citations - var sumMetricCount = 0; - for (var i = 0; i < this.metricCount.length; i++) { - sumMetricCount += this.metricCount[i] - } + // check if there have been any views/citations + var sumMetricCount = 0; + for (var i = 0; i < this.metricCount.length; i++) { + sumMetricCount += this.metricCount[i]; + } - var self = this; + var self = this; + + // when ther no data or no views/citations yet, just show some text: + if ( + this.metricCount.length == 0 || + this.metricCount == 0 || + sumMetricCount == 0 + ) { + var metricNameLemma = this.metricName + .toLowerCase() + .substring(0, this.metricName.length - 1); + var textMessage = + "This dataset hasn’t been " + metricNameLemma + "ed yet."; + if (this.viewType != "dataset") { + textMessage = + "These datasets have not been " + metricNameLemma + "ed yet."; + } - // when ther no data or no views/citations yet, just show some text: - if(this.metricCount.length == 0 || this.metricCount == 0 || sumMetricCount ==0){ + var margin = { top: 25, right: 40, bottom: 40, left: 40 }, + height = this.height - margin.top - margin.bottom; - var metricNameLemma = this.metricName.toLowerCase().substring(0, this.metricName.length - 1); - var textMessage = "This dataset hasn’t been " + metricNameLemma + "ed yet." - if (this.viewType != "dataset") { - textMessage = "These datasets have not been " + metricNameLemma + "ed yet." - } + //Set the chart width + this.$el.css("width", "100%"); + var width = + (this.$el.width() || this.width) - margin.left - margin.right; + this.width = width; - var margin = {top: 25, right: 40, bottom: 40, left: 40}, - height = this.height - margin.top - margin.bottom; - - //Set the chart width - this.$el.css("width", "100%"); - var width = (this.$el.width() || this.width) - margin.left - margin.right; - this.width = width; - - var vis = d3.select(this.el) - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .attr("class", "line-chart no-data"); - - var bkg = vis.append("svg:rect") - .attr("class", "no-data") - .attr("width", width) - .attr("height", height) - .attr("rx", 2) - .attr("ry", 2) - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - - var msg = vis.append("text") - .attr("class", "no-data") - .text(textMessage) - .attr("text-anchor", "middle") - .attr("font-size", "20px") - .attr("x", width/2) - .attr("y", height/2) - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + var vis = d3 + .select(this.el) + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .attr("class", "line-chart no-data"); + + var bkg = vis + .append("svg:rect") + .attr("class", "no-data") + .attr("width", width) + .attr("height", height) + .attr("rx", 2) + .attr("ry", 2) + .attr( + "transform", + "translate(" + margin.left + "," + margin.top + ")", + ); + + var msg = vis + .append("text") + .attr("class", "no-data") + .text(textMessage) + .attr("text-anchor", "middle") + .attr("font-size", "20px") + .attr("x", width / 2) + .attr("y", height / 2) + .attr( + "transform", + "translate(" + margin.left + "," + margin.top + ")", + ); // if there is data (even a series of zeros), draw the time-series chart: - } else { - + } else { /* - * ======================================================================== - * Global variables and options - * ======================================================================== - */ + * ======================================================================== + * Global variables and options + * ======================================================================== + */ var metricName = this.metricName; // the format of the date in the input data - var input_date_format = d3.time.format("%Y-%m"); + var input_date_format = d3.time.format("%Y-%m"); // how dates will be displayed in the chart in most cases - var display_date_format = d3.time.format("%b %Y"); + var display_date_format = d3.time.format("%b %Y"); // the length of a day/year in milliseconds var day_in_ms = 86400000, - year_in_ms = 31540000000; + year_in_ms = 31540000000; // focus chart sizing - var margin = {top: 30, right: 30, bottom: 95, left: 20}, - height = this.height - margin.top - margin.bottom; + var margin = { top: 30, right: 30, bottom: 95, left: 20 }, + height = this.height - margin.top - margin.bottom; //Set the chart width this.$el.css("width", "100%"); - var width = (this.$el.width() || this.width) - margin.left - margin.right; + var width = + (this.$el.width() || this.width) - margin.left - margin.right; this.width = width; // context chart sizing - var margin_context = {top: 315, right: 30, bottom: 20, left: 20}, - height_context = this.height - margin_context.top - margin_context.bottom; + var margin_context = { top: 315, right: 30, bottom: 20, left: 20 }, + height_context = + this.height - margin_context.top - margin_context.bottom; // zoom button sizing var button_width = 40, - button_height = 14, - button_padding = 10; + button_height = 14, + button_padding = 10; // how wide does the tooltip div need to be to accomdate text? var tooltip_width = 76; @@ -204,45 +228,59 @@

Source: src/js/views/MetricsChartView.js

var bar_width_factor = 0.8; /* - * ======================================================================== - * Prepare data - * ======================================================================== - */ + * ======================================================================== + * Prepare data + * ======================================================================== + */ // change dates to milliseconds, to enable calculating the `d3.extent` var metricMonths_parsed = []; - this.metricMonths.forEach(function(part, index, theArray) { - - try { - metricMonths_parsed[index] = input_date_format.parse(part).getTime(); - } - catch { - // replace null with current month - var today = new Date(); - var yyyy = today.getFullYear(); - var mm = today.getMonth() + 1; - var updatedPart = yyyy.toString() + "-" + (mm.toString()).padStart(2, '0'); - - metricMonths_parsed[index] = input_date_format.parse(updatedPart).getTime(); - } - + this.metricMonths.forEach(function (part, index, theArray) { + try { + metricMonths_parsed[index] = input_date_format + .parse(part) + .getTime(); + } catch { + // replace null with current month + var today = new Date(); + var yyyy = today.getFullYear(); + var mm = today.getMonth() + 1; + var updatedPart = + yyyy.toString() + "-" + mm.toString().padStart(2, "0"); + + metricMonths_parsed[index] = input_date_format + .parse(updatedPart) + .getTime(); + } }); // input data from model doesn't list months where there were 0 counts for all metrics (views/downloads/citations) // construct an array of all months between min and max dates to use as x variable - var all_months = d3.time.scale() - .domain(d3.extent(metricMonths_parsed)) - .ticks(d3.time.months, 1); + var all_months = d3.time + .scale() + .domain(d3.extent(metricMonths_parsed)) + .ticks(d3.time.months, 1); // add padding to both sides of array so that bars don't get cut off. // add more padding when there's just one bar (otherwise it's too wide) if (metricMonths_parsed.length == 1) { - var new_min_date = new Date(d3.extent(metricMonths_parsed)[0] - day_in_ms*13), - new_max_date = new Date(d3.extent(metricMonths_parsed)[1] + day_in_ms*31*bar_width_factor + day_in_ms*13); + var new_min_date = new Date( + d3.extent(metricMonths_parsed)[0] - day_in_ms * 13, + ), + new_max_date = new Date( + d3.extent(metricMonths_parsed)[1] + + day_in_ms * 31 * bar_width_factor + + day_in_ms * 13, + ); } else { - var new_min_date = new Date(d3.extent(metricMonths_parsed)[0] - day_in_ms*1), - new_max_date = new Date(d3.extent(metricMonths_parsed)[1] + day_in_ms*31*bar_width_factor); - }; + var new_min_date = new Date( + d3.extent(metricMonths_parsed)[0] - day_in_ms * 1, + ), + new_max_date = new Date( + d3.extent(metricMonths_parsed)[1] + + day_in_ms * 31 * bar_width_factor, + ); + } all_months.push(new_min_date); // also add a little padding on the left for consistency @@ -251,541 +289,671 @@

Source: src/js/views/MetricsChartView.js

// for each month, check whether there is a count available, // if so append it, otherwise append zero. var dataset = []; - for(var i=0; i<all_months.length; i++){ - var match_index = metricMonths_parsed.indexOf(all_months[i].getTime()); - if (match_index == -1) { // no match in data - dataset.push({integer: i, month: all_months[i], count:0}); - } else { // match in data - dataset.push({integer: i, month: all_months[i], count:this.metricCount[match_index]}); - } - }; + for (var i = 0; i < all_months.length; i++) { + var match_index = metricMonths_parsed.indexOf( + all_months[i].getTime(), + ); + if (match_index == -1) { + // no match in data + dataset.push({ integer: i, month: all_months[i], count: 0 }); + } else { + // match in data + dataset.push({ + integer: i, + month: all_months[i], + count: this.metricCount[match_index], + }); + } + } /* - * ======================================================================== - * x and y coordinates - * ======================================================================== - */ + * ======================================================================== + * x and y coordinates + * ======================================================================== + */ - var x_full_extent = d3.extent(dataset, function(d) { return d.month; }); - var bar_width = ((day_in_ms*30)/(x_full_extent[1]-x_full_extent[0])) * width * bar_width_factor; + var x_full_extent = d3.extent(dataset, function (d) { + return d.month; + }); + var bar_width = + ((day_in_ms * 30) / (x_full_extent[1] - x_full_extent[0])) * + width * + bar_width_factor; /* === Focus Chart === */ - var x = d3.time.scale() - .range([0,width]) - .domain(d3.extent(dataset, function(d) { return d.month; })); - - var y = d3.scale.linear() - .range([height, 0]) - .domain([0, d3.max(dataset, function(d) { return d.count; })*1.04]); - - var x_axis = d3.svg.axis() - .scale(x) - .orient("bottom") - .tickSize(-(height)) - .ticks(generate_ticks) - .tickFormat(format_months); - - var y_axis = d3.svg.axis() - .scale(y) - .ticks(4) - .tickFormat(d3.format("d")) - .tickSize(-(width)) - .orient("right"); + var x = d3.time + .scale() + .range([0, width]) + .domain( + d3.extent(dataset, function (d) { + return d.month; + }), + ); + + var y = d3.scale + .linear() + .range([height, 0]) + .domain([ + 0, + d3.max(dataset, function (d) { + return d.count; + }) * 1.04, + ]); + + var x_axis = d3.svg + .axis() + .scale(x) + .orient("bottom") + .tickSize(-height) + .ticks(generate_ticks) + .tickFormat(format_months); + + var y_axis = d3.svg + .axis() + .scale(y) + .ticks(4) + .tickFormat(d3.format("d")) + .tickSize(-width) + .orient("right"); /* === Context Chart === */ - var x_context = d3.time.scale() - .range([0, width]) - .domain(d3.extent(dataset, function(d) { return d.month; })); - - var y_context = d3.scale.linear() - .range([height_context, 0]) - .domain(y.domain()); - - var x_axis_context = d3.svg.axis() - .scale(x_context) - .orient("bottom") - .ticks(generate_ticks) - .tickFormat(format_months); + var x_context = d3.time + .scale() + .range([0, width]) + .domain( + d3.extent(dataset, function (d) { + return d.month; + }), + ); + + var y_context = d3.scale + .linear() + .range([height_context, 0]) + .domain(y.domain()); + + var x_axis_context = d3.svg + .axis() + .scale(x_context) + .orient("bottom") + .ticks(generate_ticks) + .tickFormat(format_months); /* - * ======================================================================== - * Variables for brushing and zooming behaviour - * ======================================================================== - */ - - var brush = d3.svg.brush() - .x(x_context) - .on("brush", change_focus_brush) - .on("brushend", check_bounds); - - var zoom = d3.behavior.zoom() - .on("zoom", change_focus_zoom) - .on("zoomend", check_bounds); + * ======================================================================== + * Variables for brushing and zooming behaviour + * ======================================================================== + */ + + var brush = d3.svg + .brush() + .x(x_context) + .on("brush", change_focus_brush) + .on("brushend", check_bounds); + + var zoom = d3.behavior + .zoom() + .on("zoom", change_focus_zoom) + .on("zoomend", check_bounds); /* - * ======================================================================== - * Define the SVG area ("vis") and append all the layers - * ======================================================================== - */ + * ======================================================================== + * Define the SVG area ("vis") and append all the layers + * ======================================================================== + */ // === the main components === // - var vis = d3.select(this.el) - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .attr("class", "line-chart"); + var vis = d3 + .select(this.el) + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .attr("class", "line-chart"); // clipPath is used to keep elements from moving outside of plot area when viwer zooms/scrolls/brushes - vis.append("defs").append("clipPath") - .attr("id", "clip") - .append("rect") - .attr("width", width) - .attr("height", height); - - var pane = vis.append("rect") - .attr("class", "pane") - .attr("width", width) - .attr("height", height) - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - - var context = vis.append("g") - .attr("class", "context") - .attr("transform", "translate(" + margin_context.left + "," + margin_context.top + ")"); - - var focus = vis.append("g") - .attr("class", "focus") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + vis + .append("defs") + .append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width) + .attr("height", height); + + var pane = vis + .append("rect") + .attr("class", "pane") + .attr("width", width) + .attr("height", height) + .attr( + "transform", + "translate(" + margin.left + "," + margin.top + ")", + ); + + var context = vis + .append("g") + .attr("class", "context") + .attr( + "transform", + "translate(" + + margin_context.left + + "," + + margin_context.top + + ")", + ); + + var focus = vis + .append("g") + .attr("class", "focus") + .attr( + "transform", + "translate(" + margin.left + "," + margin.top + ")", + ); // === current date range text & zoom buttons === // - var expl_text = vis.append("g") - .attr("id", "buttons_group") - .attr("transform", "translate(" + 0 + ","+ 0 +")"); + var expl_text = vis + .append("g") + .attr("id", "buttons_group") + .attr("transform", "translate(" + 0 + "," + 0 + ")"); - expl_text.append("text") - .attr("id", "totalCount") - .style("text-anchor", "start") - .attr("transform", "translate(" + 18 + ","+ 11 +")"); + expl_text + .append("text") + .attr("id", "totalCount") + .style("text-anchor", "start") + .attr("transform", "translate(" + 18 + "," + 11 + ")"); - expl_text.append("text") - .attr("id", "displayDates") - .style("text-anchor", "start") - .attr("transform", "translate(" + 20 + ","+ 22 +")"); + expl_text + .append("text") + .attr("id", "displayDates") + .style("text-anchor", "start") + .attr("transform", "translate(" + 20 + "," + 22 + ")"); update_context(); // === the zooming/scaling buttons === // - if ((x_full_extent[1] - x_full_extent[0]) < year_in_ms) { - var button_data =["month","all"]; + if (x_full_extent[1] - x_full_extent[0] < year_in_ms) { + var button_data = ["month", "all"]; } else { - var button_data =["year","month","all"]; - }; - - var button_count = button_data.length -1, - button_g_width = (button_count*button_width) + - (button_count*button_padding) + - margin.right - button_padding; - - expl_text.append("text") - .attr("class", "zoomto_text") - .text("Zoom to") - .style("text-anchor", "start") - .attr("transform", "translate(" + (width - button_g_width - 45) + ","+ 14 +")") - .style("opacity", "0"); - - var button = expl_text.selectAll("g") - .data(button_data) - .enter().append("g") - .attr("class", "scale_button") - .attr("transform", function(d, i) { return "translate(" + ((width - button_g_width) + i*button_width + i*button_padding) + ",4)"; }) - .style("opacity", "0"); - - button.append("rect") - .attr("class", "button_rect") - .attr("width", button_width) - .attr("height", button_height) - .attr("rx", 1) - .attr("ry", 1); - - button.append("text") - .attr("dy", (button_height/2 + 3)) - .attr("dx", button_width/2) - .style("text-anchor", "middle") - .text(function(d) { return d; }); + var button_data = ["year", "month", "all"]; + } + + var button_count = button_data.length - 1, + button_g_width = + button_count * button_width + + button_count * button_padding + + margin.right - + button_padding; + + expl_text + .append("text") + .attr("class", "zoomto_text") + .text("Zoom to") + .style("text-anchor", "start") + .attr( + "transform", + "translate(" + (width - button_g_width - 45) + "," + 14 + ")", + ) + .style("opacity", "0"); + + var button = expl_text + .selectAll("g") + .data(button_data) + .enter() + .append("g") + .attr("class", "scale_button") + .attr("transform", function (d, i) { + return ( + "translate(" + + (width - + button_g_width + + i * button_width + + i * button_padding) + + ",4)" + ); + }) + .style("opacity", "0"); + + button + .append("rect") + .attr("class", "button_rect") + .attr("width", button_width) + .attr("height", button_height) + .attr("rx", 1) + .attr("ry", 1); + + button + .append("text") + .attr("dy", button_height / 2 + 3) + .attr("dx", button_width / 2) + .style("text-anchor", "middle") + .text(function (d) { + return d; + }); /* === focus chart === */ - focus.append("g") - .attr("class", "y axis") - .call(y_axis) - .attr("transform", "translate(" + (width) + ", 0)") - .style("text-anchor", "middle"); + focus + .append("g") + .attr("class", "y axis") + .call(y_axis) + .attr("transform", "translate(" + width + ", 0)") + .style("text-anchor", "middle"); // x-axis - focus.append("g") - .attr("class", "x axis") - .attr("transform", "translate(0," + height + ")") - .call(x_axis) - .style("text-anchor", "middle"); + focus + .append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(x_axis) + .style("text-anchor", "middle"); // enter bars - focus.selectAll(".bar") - .data(dataset) - .enter().append("rect") - .attr("class", "bar") - .attr("id", function(d){return "bar_" + d.month.getTime()}) //id of each bar is "bar_" plus it's associated date in ms - .attr("x", function (d) {return x(d.month); }) - .attr("y", height) - .attr("height", 0) - .attr("width", bar_width) - .style("opacity", 0) - .on("mouseover", function(d) { - var floor_month = d3.time.month.floor(d.month).getTime(); - highlight_bar("#bar_" + floor_month); - highlight_label("#label_" + floor_month); - show_tooltip(d); - }) - .on("mouseout", function(d) { - var floor_month = d3.time.month.floor(d.month).getTime(); - unhighlight_bar("#bar_" + floor_month); - unhighlight_label("#label_" + floor_month); - hide_tooltip(d); - }); + focus + .selectAll(".bar") + .data(dataset) + .enter() + .append("rect") + .attr("class", "bar") + .attr("id", function (d) { + return "bar_" + d.month.getTime(); + }) //id of each bar is "bar_" plus it's associated date in ms + .attr("x", function (d) { + return x(d.month); + }) + .attr("y", height) + .attr("height", 0) + .attr("width", bar_width) + .style("opacity", 0) + .on("mouseover", function (d) { + var floor_month = d3.time.month.floor(d.month).getTime(); + highlight_bar("#bar_" + floor_month); + highlight_label("#label_" + floor_month); + show_tooltip(d); + }) + .on("mouseout", function (d) { + var floor_month = d3.time.month.floor(d.month).getTime(); + unhighlight_bar("#bar_" + floor_month); + unhighlight_label("#label_" + floor_month); + hide_tooltip(d); + }); // animate bars - focus.selectAll(".bar") - .transition() - .duration(450) - .ease("elastic", 1.03, 0.98) - .delay(function(d, i) { - var max_delay = 600; - var z = i / (dataset.length-1); - var line_z = z * max_delay * 0.4; - var log_z = Math.log2(z + 1) * max_delay * 0.6; - return(250+line_z + log_z); - }) - .attr("y", function (d) {return y(d.count); }) - .attr("height", function (d) {return y(0) - y(d.count); }) - .style("opacity", 1); + focus + .selectAll(".bar") + .transition() + .duration(450) + .ease("elastic", 1.03, 0.98) + .delay(function (d, i) { + var max_delay = 600; + var z = i / (dataset.length - 1); + var line_z = z * max_delay * 0.4; + var log_z = Math.log2(z + 1) * max_delay * 0.6; + return 250 + line_z + log_z; + }) + .attr("y", function (d) { + return y(d.count); + }) + .attr("height", function (d) { + return y(0) - y(d.count); + }) + .style("opacity", 1); /* === context chart === */ // enter context bars - context.selectAll(".bar_context") - .data(dataset) - .enter().append("rect") - .attr("class", "bar_context") - .attr("x", function (d) {return x_context(d.month) }) - .attr("y", height_context) - .attr("height", 0) - .attr("width", bar_width) - .style("opacity", 0); + context + .selectAll(".bar_context") + .data(dataset) + .enter() + .append("rect") + .attr("class", "bar_context") + .attr("x", function (d) { + return x_context(d.month); + }) + .attr("y", height_context) + .attr("height", 0) + .attr("width", bar_width) + .style("opacity", 0); // animate context bars - context.selectAll(".bar_context") - .transition() - .duration(450) - .ease("elastic", 1.03, 0.98) - .delay(function(d, i) { - var max_delay = 600; - var z = i / (dataset.length-1); - var line_z = z * max_delay * 0.4; - var log_z = Math.log2(z + 1) * max_delay * 0.6; - return(line_z + log_z); - }) - .attr("y", function (d) {return y_context(d.count); }) - .attr("height", function (d) {return y_context(0) - y_context(d.count); }) - .style("opacity", 1); + context + .selectAll(".bar_context") + .transition() + .duration(450) + .ease("elastic", 1.03, 0.98) + .delay(function (d, i) { + var max_delay = 600; + var z = i / (dataset.length - 1); + var line_z = z * max_delay * 0.4; + var log_z = Math.log2(z + 1) * max_delay * 0.6; + return line_z + log_z; + }) + .attr("y", function (d) { + return y_context(d.count); + }) + .attr("height", function (d) { + return y_context(0) - y_context(d.count); + }) + .style("opacity", 1); // x-axis - context.append("g") - .attr("class", "x axis") - .attr("transform", "translate(0," + height_context + ")") - .call(x_axis_context); + context + .append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + height_context + ")") + .call(x_axis_context); /* === brush === */ - var brushg = context.append("g") - .attr("class", "x brush") - .call(brush); - - brushg.selectAll(".extent") - .attr("y", -6) - .attr("height", height_context + 8) - .style("opacity" , "0"); - - brushg.selectAll(".resize") - .append("rect") - .attr("class", "handle") - .attr("transform", "translate(0," + -3 + ")") - .attr('rx', 1) - .attr('ry', 1) - .attr("height", 0) - .attr("width", 3) - .style("opacity" , "0"); - - brushg.selectAll(".resize") - .append("rect") - .attr("class", "handle-mini") - .attr("transform", "translate(-2,8.5)") - .attr('rx', 2) - .attr('ry', 2) - .attr("height", 0) - .attr("width", 7) - .style("opacity" , "0"); + var brushg = context.append("g").attr("class", "x brush").call(brush); + + brushg + .selectAll(".extent") + .attr("y", -6) + .attr("height", height_context + 8) + .style("opacity", "0"); + + brushg + .selectAll(".resize") + .append("rect") + .attr("class", "handle") + .attr("transform", "translate(0," + -3 + ")") + .attr("rx", 1) + .attr("ry", 1) + .attr("height", 0) + .attr("width", 3) + .style("opacity", "0"); + + brushg + .selectAll(".resize") + .append("rect") + .attr("class", "handle-mini") + .attr("transform", "translate(-2,8.5)") + .attr("rx", 2) + .attr("ry", 2) + .attr("height", 0) + .attr("width", 7) + .style("opacity", "0"); /* === y axis title === */ - vis.append("text") - .attr("class", "y axis title") - .text("Monthly " + this.metricName) - .attr("x", (-((height+margin.top+margin.bottom-50)/2))) - .attr("y", 0) - .attr("dy", "1em") - .attr("transform", "rotate(-90)") - .style("text-anchor", "middle"); + vis + .append("text") + .attr("class", "y axis title") + .text("Monthly " + this.metricName) + .attr("x", -((height + margin.top + margin.bottom - 50) / 2)) + .attr("y", 0) + .attr("dy", "1em") + .attr("transform", "rotate(-90)") + .style("text-anchor", "middle"); // allow zoom, brush, and scale behavior after a small delay, // so that user does not interrupt bar entrance animation. // show UI elements only once user is able to interact with them. - setTimeout(function(){ - - // disable the zoom behavior on wheel zoom event - // add behaviours - pane.call(zoom) - .call(change_focus_zoom) - .on("wheel.zoom", null); - zoom.x(x); - - vis.selectAll(".scale_button") - .style("cursor", "pointer") - .on("click", zoom_to_interval); - - // fade in buttons - vis.selectAll(".scale_button,.zoomto_text") - .transition() - .duration(100) - .ease("cubic") - .style("opacity","1"); - - // fade in brush elements - brushg.selectAll(".extent") - .transition() - .duration(100) - .ease("cubic") - .style("opacity" , "1"); - - brushg.selectAll(".handle-mini") - .transition() - .duration(170) - .ease("linear") - .attr("height", (height_context/2)) - .style("opacity" , "1"); - - brushg.selectAll(".handle") - .transition() - .duration(170) - .ease("linear") - .attr("height", height_context + 6) - .style("opacity" , "1"); - - if (self.viewType === "dataset") { - d3.select(".metric-chart") - .append("div") - .attr("class", "metric_tooltip") - .style("opacity", 0) - .style("width", tooltip_width + "px"); - } - else { - d3.select("#user-" + self.id) - .append("div") - .attr("class", "metric_tooltip") - .style("opacity", 0) - .style("width", tooltip_width + "px"); - } - }, - 900); + setTimeout(function () { + // disable the zoom behavior on wheel zoom event + // add behaviours + pane.call(zoom).call(change_focus_zoom).on("wheel.zoom", null); + zoom.x(x); + + vis + .selectAll(".scale_button") + .style("cursor", "pointer") + .on("click", zoom_to_interval); + + // fade in buttons + vis + .selectAll(".scale_button,.zoomto_text") + .transition() + .duration(100) + .ease("cubic") + .style("opacity", "1"); + + // fade in brush elements + brushg + .selectAll(".extent") + .transition() + .duration(100) + .ease("cubic") + .style("opacity", "1"); + + brushg + .selectAll(".handle-mini") + .transition() + .duration(170) + .ease("linear") + .attr("height", height_context / 2) + .style("opacity", "1"); + + brushg + .selectAll(".handle") + .transition() + .duration(170) + .ease("linear") + .attr("height", height_context + 6) + .style("opacity", "1"); + + if (self.viewType === "dataset") { + d3.select(".metric-chart") + .append("div") + .attr("class", "metric_tooltip") + .style("opacity", 0) + .style("width", tooltip_width + "px"); + } else { + d3.select("#user-" + self.id) + .append("div") + .attr("class", "metric_tooltip") + .style("opacity", 0) + .style("width", tooltip_width + "px"); + } + }, 900); /* - * ======================================================================== - * Functions - * ======================================================================== - */ + * ======================================================================== + * Functions + * ======================================================================== + */ /* ------------------------------------------------------ HELPER FUNCTIONS ------------------------------------------------------ */ - function get_zoom_scale(){ - // custom zoom scale needed to calculate width of bars with zoom/brush. - // can't use zoom.scale() because this needs to be reset (to one) when using change_focus_brush() - var x_current_width = x.domain()[1] - x.domain()[0], - x_total_width = x_full_extent[1] - x_full_extent[0], - zoom_scale = x_total_width/x_current_width; - return(zoom_scale); - }; - - function convert_metric_name(n){ - // remove s from metric name if count is 1 - if (n == 1) { - return metricName.slice(0, -1); - } else { - return metricName; - } - }; + function get_zoom_scale() { + // custom zoom scale needed to calculate width of bars with zoom/brush. + // can't use zoom.scale() because this needs to be reset (to one) when using change_focus_brush() + var x_current_width = x.domain()[1] - x.domain()[0], + x_total_width = x_full_extent[1] - x_full_extent[0], + zoom_scale = x_total_width / x_current_width; + return zoom_scale; + } + + function convert_metric_name(n) { + // remove s from metric name if count is 1 + if (n == 1) { + return metricName.slice(0, -1); + } else { + return metricName; + } + } /* ------------------------------------------------------ HOVER BEHAVIOUR: X-AXIS LABELS, BARS, TOOLTIPS ------------------------------------------------------ */ - function highlight_bar(bar_id){ - // mouseover effect on bar - focus.select(bar_id) - .style("stroke-width", "1") - .style("opacity", "0.9"); - }; + function highlight_bar(bar_id) { + // mouseover effect on bar + focus + .select(bar_id) + .style("stroke-width", "1") + .style("opacity", "0.9"); + } function unhighlight_bar(bar_id) { - // undo mouseover effect on bar - focus.select(bar_id) - .style("stroke-width", "0") - .style("opacity", "1"); - }; - - function highlight_label(label_id){ - // mouseover effect on label - focus.select(label_id).selectAll("text") - .style("font-weight", "bold"); - }; - - function unhighlight_label(label_id){ - // undo mouseover effect on label - focus.select(label_id).selectAll("text") - .style("font-weight", "normal"); - }; + // undo mouseover effect on bar + focus + .select(bar_id) + .style("stroke-width", "0") + .style("opacity", "1"); + } + + function highlight_label(label_id) { + // mouseover effect on label + focus + .select(label_id) + .selectAll("text") + .style("font-weight", "bold"); + } + + function unhighlight_label(label_id) { + // undo mouseover effect on label + focus + .select(label_id) + .selectAll("text") + .style("font-weight", "normal"); + } function add_tick_behaviour() { - // adds html ID and hover behaviour to the x-axis ticks/labels - // this function is called each time these labels/ticks are re-generated. - - focus.selectAll(".x.axis .tick")[0].forEach(function(tick) { - d3.select(tick) - .attr("id", function(d,i) { - return "label_" + d3.time.month.floor(d).getTime(); - }) - .on("mouseover", function(tick) { - // extract the datapoint from dataset that is associated with x-axis label - var floor_month = d3.time.month.floor(tick).getTime(); - var d = dataset.filter( function(d) { return d.month.getTime() === floor_month; })[0]; - highlight_bar("#bar_" + floor_month); - highlight_label("#label_" + floor_month); - show_tooltip(d); - }) - .on("mouseout", function(tick) { - var floor_month = d3.time.month.floor(tick).getTime(); - unhighlight_bar("#bar_" + floor_month); - unhighlight_label("#label_" + floor_month); - hide_tooltip(tick); - }); - }); - }; + // adds html ID and hover behaviour to the x-axis ticks/labels + // this function is called each time these labels/ticks are re-generated. + + focus.selectAll(".x.axis .tick")[0].forEach(function (tick) { + d3.select(tick) + .attr("id", function (d, i) { + return "label_" + d3.time.month.floor(d).getTime(); + }) + .on("mouseover", function (tick) { + // extract the datapoint from dataset that is associated with x-axis label + var floor_month = d3.time.month.floor(tick).getTime(); + var d = dataset.filter(function (d) { + return d.month.getTime() === floor_month; + })[0]; + highlight_bar("#bar_" + floor_month); + highlight_label("#label_" + floor_month); + show_tooltip(d); + }) + .on("mouseout", function (tick) { + var floor_month = d3.time.month.floor(tick).getTime(); + unhighlight_bar("#bar_" + floor_month); + unhighlight_label("#label_" + floor_month); + hide_tooltip(tick); + }); + }); + } function show_tooltip(d) { + if (self.viewType === "dataset") { + var bar_width_px = bar_width * get_zoom_scale(); - if (self.viewType === "dataset") { - - var bar_width_px = bar_width * get_zoom_scale(); + // get the width of the modal. Need for tooltip x-position. + var modal_width = d3 + .select("#metric-modal") + .style("width") + .slice(0, -2); + var modal_width = Math.round(Number(modal_width)); - // get the width of the modal. Need for tooltip x-position. - var modal_width = d3.select("#metric-modal") - .style('width') - .slice(0, -2); - var modal_width = Math.round(Number(modal_width)); + d3.select(".metric_tooltip") + .html( + "<b>" + + display_date_format(d.month) + + "</b><br/>" + + d.count + + " " + + convert_metric_name(d.count), + ) + .style( + "left", + x(d.month) + + (modal_width - (width + margin.left + margin.right)) + + bar_width_px / 2 - + tooltip_width / 2 + + "px", + ) //) + 300 + ((width/dataset.length) * 0.5 * get_zoom_scale())) + "px") + .style("top", y(d.count) + 19 + "px"); - d3.select(".metric_tooltip") - .html("<b>" + display_date_format(d.month) + "</b><br/>" + d.count + " " + convert_metric_name(d.count)) - .style("left", (x(d.month) + (modal_width-(width + margin.left + margin.right)) + (bar_width_px/2) - (tooltip_width/2) + "px"))//) + 300 + ((width/dataset.length) * 0.5 * get_zoom_scale())) + "px") - .style("top", (y(d.count) + 19) + "px"); + d3.select(".metric_tooltip") + .transition() + .duration(60) + .style("opacity", 0.98); + } else { + d3.select("#user-" + self.id + " > .metric_tooltip") + .html( + "<b>" + + display_date_format(d.month) + + "</b><br/>" + + d.count + + " " + + convert_metric_name(d.count), + ) + .style("left", d3.event.pageX - 150 + "px") + .style("top", y(d.count) - y(0) - 150 + "px"); - d3.select(".metric_tooltip") - .transition() - .duration(60) - .style("opacity", 0.98); - } - else { - d3.select("#user-" + self.id + " > .metric_tooltip") - .html("<b>" + display_date_format(d.month) + "</b><br/>" + d.count + " " + convert_metric_name(d.count)) - .style("left", d3.event.pageX - 150 + "px") - .style("top", y(d.count) - y(0) - 150 + "px"); - - d3.select("#user-" + self.id + " > .metric_tooltip") - .transition() - .duration(60) - .style("opacity", 0.98); - } - }; + d3.select("#user-" + self.id + " > .metric_tooltip") + .transition() + .duration(60) + .style("opacity", 0.98); + } + } function hide_tooltip(d) { if (self.viewType === "dataset") { d3.select(".metric_tooltip") - .transition() - .duration(60) - .style("opacity", 0); - } - else { + .transition() + .duration(60) + .style("opacity", 0); + } else { d3.select("#user-" + self.id + " > .metric_tooltip") - .transition() - .duration(60) - .style("opacity", 0); + .transition() + .duration(60) + .style("opacity", 0); } - }; - + } /* ------------------------------------------------------ TICK FORMATTING FUNCTIONS (focus x-axis) ------------------------------------------------------ */ + function generate_ticks(t0, t1, dt) { + var label_size_px = 45; + var max_total_labels = Math.floor(width / label_size_px); + // offset so that labels are at the center of each month. + var offset = (day_in_ms * 30 * bar_width_factor) / 2; - function generate_ticks(t0, t1, dt) { - - var label_size_px = 45; - var max_total_labels = Math.floor(width / label_size_px); - // offset so that labels are at the center of each month. - var offset = (day_in_ms*30*bar_width_factor)/2; - - function step(date, next_step) { - date.setMonth(date.getMonth() + next_step); - } - - var time = d3.time.month.floor(t0), - time = new Date(time.getTime() + offset), - times = [], - monthFactors = [1,3,4,12]; + function step(date, next_step) { + date.setMonth(date.getMonth() + next_step); + } - while (time < t1) { times.push(new Date(+time)), step(time, 1)}; + var time = d3.time.month.floor(t0), + time = new Date(time.getTime() + offset), + times = [], + monthFactors = [1, 3, 4, 12]; - var timesCopy = times; - var i; + while (time < t1) { + times.push(new Date(+time)), step(time, 1); + } - for(i=0 ; times.length > max_total_labels ; i++){ - var times = _.filter(timesCopy, function(d){ return (d.getMonth()) % monthFactors[i] == 0; } ) - }; + var timesCopy = times; + var i; - return times; + for (i = 0; times.length > max_total_labels; i++) { + var times = _.filter(timesCopy, function (d) { + return d.getMonth() % monthFactors[i] == 0; + }); + } - }; + return times; + } - function format_months(d){ - add_tick_behaviour(); // add tick hover behaviour everytime ticks are re-formatted; - var test = (x.domain()[1] - x.domain()[0]) > 132167493818; // when to switch from yyyy to mm-yyyy - if(d.getMonth()==0 & test){//if january - var yearOnly = d3.time.format("%Y"); - return(yearOnly(d)); - } else { - return(display_date_format(d)) - } + function format_months(d) { + add_tick_behaviour(); // add tick hover behaviour everytime ticks are re-formatted; + var test = x.domain()[1] - x.domain()[0] > 132167493818; // when to switch from yyyy to mm-yyyy + if ((d.getMonth() == 0) & test) { + //if january + var yearOnly = d3.time.format("%Y"); + return yearOnly(d); + } else { + return display_date_format(d); + } } /* ------------------------------------------------------ @@ -793,243 +961,285 @@

Source: src/js/views/MetricsChartView.js

------------------------------------------------------ */ function change_focus_brush() { - // make the x domain match the brush domain - x.domain(brush.empty() ? x_context.domain() : brush.extent()); - // reset zoom - zoom.x(x); - // re-draw axis and elements at new scale - update_focus(); - // update the explanatory text (total views, date range) - update_context(); + // make the x domain match the brush domain + x.domain(brush.empty() ? x_context.domain() : brush.extent()); + // reset zoom + zoom.x(x); + // re-draw axis and elements at new scale + update_focus(); + // update the explanatory text (total views, date range) + update_context(); } function change_focus_zoom() { - // make the brush range change with the x domain in focus - brush.extent(x.domain()); - vis.select(".brush").call(brush); - // re-draw axis and elements at new scale - update_focus(); - // update the explanatory text (total views, date range) - update_context(); + // make the brush range change with the x domain in focus + brush.extent(x.domain()); + vis.select(".brush").call(brush); + // re-draw axis and elements at new scale + update_focus(); + // update the explanatory text (total views, date range) + update_context(); } function update_focus() { + // calculate where the bar goes out of focus + var bar_width_days = bar_width_factor * 30.5; - // calculate where the bar goes out of focus - var bar_width_days = bar_width_factor*30.5; - - var left_date = x.domain()[0]; - if(left_date.getDate() < bar_width_days){ - var left_date = d3.time.month.floor(left_date), - left_date = new Date(left_date.getTime()) - }; - - var data_subset_focus = dataset.filter( function(d) { - return d.month <= x.domain()[1] && d.month >= left_date - }); - - var y_max_focus = d3.max(data_subset_focus, function(d) { return d.count; }) * 1.04 || 1.04; - var y_change_duration = 85; - - // reset y-axis - y.domain([0, y_max_focus]); - focus.select(".y.axis") - .transition() - .duration(y_change_duration*0.95) - .call(y_axis); - - // reset bar height given y-axis - focus.selectAll(".bar") - .transition() - .duration(y_change_duration) - .attr("y", function (d) {return y(d.count); }) - .attr("height", function (d) {return y(0) - y(d.count); }); - - // redraw other elements - focus.select(".x.axis").call(x_axis); - focus.selectAll(".bar") - .attr("x", function (d) {return x(d.month); }) - .attr("width", bar_width * get_zoom_scale()) - .style("opacity", "1"); // incase user scrolls before entrance animation finishes. - - }; + var left_date = x.domain()[0]; + if (left_date.getDate() < bar_width_days) { + var left_date = d3.time.month.floor(left_date), + left_date = new Date(left_date.getTime()); + } - function update_context() { + var data_subset_focus = dataset.filter(function (d) { + return d.month <= x.domain()[1] && d.month >= left_date; + }); + + var y_max_focus = + d3.max(data_subset_focus, function (d) { + return d.count; + }) * 1.04 || 1.04; + var y_change_duration = 85; + + // reset y-axis + y.domain([0, y_max_focus]); + focus + .select(".y.axis") + .transition() + .duration(y_change_duration * 0.95) + .call(y_axis); + + // reset bar height given y-axis + focus + .selectAll(".bar") + .transition() + .duration(y_change_duration) + .attr("y", function (d) { + return y(d.count); + }) + .attr("height", function (d) { + return y(0) - y(d.count); + }); + + // redraw other elements + focus.select(".x.axis").call(x_axis); + focus + .selectAll(".bar") + .attr("x", function (d) { + return x(d.month); + }) + .attr("width", bar_width * get_zoom_scale()) + .style("opacity", "1"); // incase user scrolls before entrance animation finishes. + } - // updates display dates, total count, and decreases opacity of context bars out of focus - var b = brush.extent(); + function update_context() { + // updates display dates, total count, and decreases opacity of context bars out of focus + var b = brush.extent(); + + // calculate where the bar goes out of focus + var bar_width_days = bar_width_factor * 30.5; + if (b[0].getDate() >= bar_width_days) { + var left_date = d3.time.month.ceil(b[0]), + left_date = new Date(left_date.getTime()); + } else { + left_date = d3.time.month.floor(b[0]); + } - // calculate where the bar goes out of focus - var bar_width_days = bar_width_factor*30.5; - if(b[0].getDate() >= bar_width_days){ - var left_date = d3.time.month.ceil(b[0]), - left_date = new Date(left_date.getTime()) - } else { - left_date = d3.time.month.floor(b[0]) - }; + // get the range of data in focus + // if there's only one data point, make sure start and end month are the same + if (metricMonths_parsed.length == 1) { + var start_month = display_date_format( + new Date(metricMonths_parsed[0]), + ), + end_month = start_month; + } else { + var start_month = brush.empty() + ? display_date_format(x_full_extent[0]) + : display_date_format(left_date), + end_month = brush.empty() + ? display_date_format(x_full_extent[1]) + : display_date_format(b[1]); + } - // get the range of data in focus - // if there's only one data point, make sure start and end month are the same + var data_subset_focus = dataset.filter(function (d) { if (metricMonths_parsed.length == 1) { - var start_month = display_date_format(new Date(metricMonths_parsed[0])), - end_month = start_month; + return d.month; } else { - var start_month = (brush.empty()) ? display_date_format(x_full_extent[0]) : display_date_format(left_date), - end_month = (brush.empty()) ? display_date_format(x_full_extent[1]) : display_date_format(b[1]); - }; - - var data_subset_focus = dataset.filter(function(d) { - if (metricMonths_parsed.length == 1) { - return d.month - } - else { - return d.month <= display_date_format.parse(end_month) && d.month >= left_date - }; - }); - - // calcualte the total views/downloads within focus area - var total_count = 0; - for (var i = 0; i < data_subset_focus.length; i++) { - total_count += data_subset_focus[i].count; + return ( + d.month <= display_date_format.parse(end_month) && + d.month >= left_date + ); } + }); - // Update start and end dates and total count - vis.select("#displayDates") - .text(start_month == end_month ? "in " + start_month : "from " + start_month + " to " + end_month); - vis.select("#totalCount") - .text(MetacatUI.appView.commaSeparateNumber(total_count) + " " + convert_metric_name(total_count)); - - // Fade all years in the bar chart not within the brush - context.selectAll(".bar_context") - .style("opacity", function(d, i) { - - if (metricMonths_parsed.length == 1) { - return "1"; - } else { - return d.month <= display_date_format.parse(end_month) && d.month >= left_date || brush.empty() ? "1" : ".3"; - } - - }); + // calcualte the total views/downloads within focus area + var total_count = 0; + for (var i = 0; i < data_subset_focus.length; i++) { + total_count += data_subset_focus[i].count; + } - }; + // Update start and end dates and total count + vis + .select("#displayDates") + .text( + start_month == end_month + ? "in " + start_month + : "from " + start_month + " to " + end_month, + ); + vis + .select("#totalCount") + .text( + MetacatUI.appView.commaSeparateNumber(total_count) + + " " + + convert_metric_name(total_count), + ); + + // Fade all years in the bar chart not within the brush + context.selectAll(".bar_context").style("opacity", function (d, i) { + if (metricMonths_parsed.length == 1) { + return "1"; + } else { + return (d.month <= display_date_format.parse(end_month) && + d.month >= left_date) || + brush.empty() + ? "1" + : ".3"; + } + }); + } function check_bounds() { - // when brush stops moving: - - // check whether chart was scrolled out of bounds and fix, - var b = brush.extent(); - var out_of_bounds = brush.extent().some(function(e) { return e < x_full_extent[0] | e > x_full_extent[1]; }); - if (out_of_bounds){ b = move_in_bounds(b) }; - - }; + // when brush stops moving: + + // check whether chart was scrolled out of bounds and fix, + var b = brush.extent(); + var out_of_bounds = brush.extent().some(function (e) { + return (e < x_full_extent[0]) | (e > x_full_extent[1]); + }); + if (out_of_bounds) { + b = move_in_bounds(b); + } + } function move_in_bounds(b) { - // move back to boundaries if user pans outside min and max date. - - var year_in_ms = 31536000000, - brush_start_new, - brush_end_new; - - if (b[0] < x_full_extent[0]) { brush_start_new = x_full_extent[0]; } - else if (b[0] > x_full_extent[1]) { brush_start_new = x_full_extent[0]; } - else { brush_start_new = b[0]; }; - - if (b[1] > x_full_extent[1]) { brush_end_new = x_full_extent[1]; } - else if (b[1] < x_full_extent[0]) { brush_end_new = x_full_extent[1]; } - else { brush_end_new = b[1]; }; - - brush.extent([brush_start_new, brush_end_new]); + // move back to boundaries if user pans outside min and max date. + + var year_in_ms = 31536000000, + brush_start_new, + brush_end_new; + + if (b[0] < x_full_extent[0]) { + brush_start_new = x_full_extent[0]; + } else if (b[0] > x_full_extent[1]) { + brush_start_new = x_full_extent[0]; + } else { + brush_start_new = b[0]; + } - brush(d3.select("#" + self.id + " > .context > .brush").transition()); - change_focus_brush(); - change_focus_zoom(); + if (b[1] > x_full_extent[1]) { + brush_end_new = x_full_extent[1]; + } else if (b[1] < x_full_extent[0]) { + brush_end_new = x_full_extent[1]; + } else { + brush_end_new = b[1]; + } - return(brush.extent()) - }; + brush.extent([brush_start_new, brush_end_new]); - function zoom_to_interval(d,i) { - // action for buttons that zoom focus to certain time interval + brush( + d3.select("#" + self.id + " > .context > .brush").transition(), + ); + change_focus_brush(); + change_focus_zoom(); - var b = brush.extent(), - interval_ms, - brush_end_new, - brush_start_new; + return brush.extent(); + } - if (d == "year") { interval_ms = 31536000000} - else if (d == "month") { interval_ms = 2592000000 }; + function zoom_to_interval(d, i) { + // action for buttons that zoom focus to certain time interval - if ( d == "year" | d == "month" ) { + var b = brush.extent(), + interval_ms, + brush_end_new, + brush_start_new; - if((x_full_extent[1].getTime() - b[1].getTime()) < interval_ms){ - // if brush is too far to the right that increasing the right-hand brush boundary would make the chart go out of bounds.... - brush_start_new = new Date(x_full_extent[1].getTime() - interval_ms); // ...then decrease the left-hand brush boundary... - brush_end_new = x_full_extent[1]; //...and set the right-hand brush boundary to the maxiumum limit. - } else { - // otherwise, increase the right-hand brush boundary. - brush_start_new = b[0]; - brush_end_new = new Date(b[0].getTime() + interval_ms); - }; + if (d == "year") { + interval_ms = 31536000000; + } else if (d == "month") { + interval_ms = 2592000000; + } - } else if ( d == "all") { - brush_start_new = x_full_extent[0]; - brush_end_new = x_full_extent[1] + if ((d == "year") | (d == "month")) { + if (x_full_extent[1].getTime() - b[1].getTime() < interval_ms) { + // if brush is too far to the right that increasing the right-hand brush boundary would make the chart go out of bounds.... + brush_start_new = new Date( + x_full_extent[1].getTime() - interval_ms, + ); // ...then decrease the left-hand brush boundary... + brush_end_new = x_full_extent[1]; //...and set the right-hand brush boundary to the maxiumum limit. } else { - brush_start_new = b[0]; - brush_end_new = b[1]; - }; + // otherwise, increase the right-hand brush boundary. + brush_start_new = b[0]; + brush_end_new = new Date(b[0].getTime() + interval_ms); + } + } else if (d == "all") { + brush_start_new = x_full_extent[0]; + brush_end_new = x_full_extent[1]; + } else { + brush_start_new = b[0]; + brush_end_new = b[1]; + } - brush.extent([brush_start_new, brush_end_new]); + brush.extent([brush_start_new, brush_end_new]); - // now draw the brush to match our extent + // now draw the brush to match our extent - brush(d3.select("#" + self.id + " > .context > .brush").transition()); - // now fire the brushstart, brushmove, and check_bounds events - brush.event(d3.select("#" + self.id + " > .context > .brush").transition()); - }; + brush( + d3.select("#" + self.id + " > .context > .brush").transition(), + ); + // now fire the brushstart, brushmove, and check_bounds events + brush.event( + d3.select("#" + self.id + " > .context > .brush").transition(), + ); + } // that's it! - } + } //Re-render this view when the window is resized this.listenToWindowResize(); - return this; - + return this; }, /** - * Adds a listener so when the window is resized, the chart is redrawn - */ - listenToWindowResize: function(){ - - if( !this.resizeCallback ){ + * Adds a listener so when the window is resized, the chart is redrawn + */ + listenToWindowResize: function () { + if (!this.resizeCallback) { this.resizeCallback = this.render.bind(this); - window.addEventListener('resize', this.resizeCallback, false); + window.addEventListener("resize", this.resizeCallback, false); } }, /** - * Removes the window resize listener set in {@link MetricsChartView#listenToWindowResize} - */ - stopListenToWindowResize: function(){ - - //Remove the listener to window resize - window.removeEventListener("resize", this.resizeCallback, false); - delete this.resizeCallback; - - }, + * Removes the window resize listener set in {@link MetricsChartView#listenToWindowResize} + */ + stopListenToWindowResize: function () { + //Remove the listener to window resize + window.removeEventListener("resize", this.resizeCallback, false); + delete this.resizeCallback; + }, /** - * Cleans up listeners and other artifacts from this view - */ - onClose: function(){ + * Cleans up listeners and other artifacts from this view + */ + onClose: function () { this.stopListenToWindowResize(); - } - - }); - - return MetricsChartView; + }, + }, + ); + return MetricsChartView; });
diff --git a/docs/docs/src_js_views_NavbarView.js.html b/docs/docs/src_js_views_NavbarView.js.html index 9d1247fba..093978136 100644 --- a/docs/docs/src_js_views_NavbarView.js.html +++ b/docs/docs/src_js_views_NavbarView.js.html @@ -44,195 +44,213 @@

Source: src/js/views/NavbarView.js

-
/*global define */
-
-define(['jquery', 'underscore', 'backbone', 'views/SignInView', 'text!templates/navbar.html'],
-	function($, _, Backbone, SignInView, NavbarTemplate) {
-	'use strict';
-
-	/**
-  * @class NavbarView
-  * @classdesc Build the navbar view of the application
-  * @extends Backbone.View
-  * @classcategory Views
-  * @constructor
-  * @screenshot views/NavbarView.png
-  */
-	var NavbarView = Backbone.View.extend(
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "views/SignInView",
+  "text!templates/navbar.html",
+], function ($, _, Backbone, SignInView, NavbarTemplate) {
+  "use strict";
+
+  /**
+   * @class NavbarView
+   * @classdesc Build the navbar view of the application
+   * @extends Backbone.View
+   * @classcategory Views
+   * @constructor
+   * @screenshot views/NavbarView.png
+   */
+  var NavbarView = Backbone.View.extend(
     /** @lends NavbarView.prototype */ {
-
-    /**
-    * @type {string}
-    */
-		el: '#Navbar',
-
-    /**
-    * @type {Underscore.Template}
-    */
-		template: _.template(NavbarTemplate),
-
-    /**
-    * @type {object}
-    */
-		events: {
-						  'click #search_btn' : 'triggerSearch',
-					   'keypress #search_txt' : 'triggerOnEnter',
-			         'click .show-new-search' : 'resetSearch',
-			 		 'click .dropdown-menu a' :  'hideDropdown',
-			 		 	'mouseenter .dropdown' : 'showDropdown',
-			 		 	 'mouseleave .dropdown' : 'hideDropdown',
-			 		 	'click #nav-trigger'  : 'showNav',
-			 		 		  'click .nav li' : 'showSubNav'
-		},
-
-		initialize: function () {
-			// listen to the MetacatUI.appModel for changes in username
-			this.listenTo(MetacatUI.appUserModel, 'change:username', this.render);
-			this.listenTo(MetacatUI.appUserModel, 'change:fullName', this.render);
-			this.listenTo(MetacatUI.appUserModel, 'change:loggedIn', this.render);
-			this.listenTo(MetacatUI.appModel, 'change:headerType', this.toggleHeaderType);
-		},
-
-		render: function () {
-			var name = MetacatUI.appUserModel.get('fullName') ? MetacatUI.appUserModel.get('fullName').charAt(0).toUpperCase() + MetacatUI.appUserModel.get("fullName").substring(1) : MetacatUI.appUserModel.get("username");
-
-			//Insert the navbar template
-			this.$el.html(
-				this.template({
-					username:   MetacatUI.appUserModel.get('username'),
-					formattedName:   name,
-					firstName:  MetacatUI.appUserModel.get('firstName'),
-					loggedIn:   MetacatUI.appUserModel.get("loggedIn"),
-					baseUrl:    MetacatUI.appModel.get('baseUrl')
-				}));
-
-			//Insert the sign-in button
-			var signInView = new SignInView().render();
-			this.$(".login-container").append(signInView.el);
-			signInView.setUpPopup();
-
-			//Initialize the tooltips in the navbar
-			this.$(".tooltip-this").tooltip({
-				delay: {show: 600},
-				trigger: "hover",
-				placement: "bottom"
-			});
-
-			this.changeBackground();
-
-      //Check if the temporary message is in this view
-      if( MetacatUI.appModel.get("temporaryMessageContainer") == "#Navbar"){
-        if( typeof MetacatUI.appView.showTemporaryMessage == "function") {
-           MetacatUI.appView.showTemporaryMessage();
-         }
-      }
-		},
-
-		changeBackground: function(){
-			// Change the background image if there is one
-			var imageEl = $('#bg_image');
-			if ($(imageEl).length > 0) {
-				var imgCnt = $(imageEl).attr('data-image-count');
-
-				//Randomly choose the next background image
-				var bgNum = Math.ceil(Math.random() * imgCnt);
-
-				$(imageEl).css('background-image', "url('" +  MetacatUI.root + "/js/themes/" +  MetacatUI.theme + "/img/backgrounds/bg" + bgNum + ".jpg')");
-			}
-		},
-
-		triggerSearch: function() {
-			// Get the search term entered
-			var searchTerm = $("#search_txt").val();
-
-			//Clear the input value
-			$("#search_txt").val('');
-
-			//Clear the search model to start a fresh search
-			MetacatUI.appSearchModel.clear().set(MetacatUI.appSearchModel.defaults);
-
-			//Create a new array with the new search term
-			var newSearch = [searchTerm];
-
-			//Set up the search model for this new term
-			MetacatUI.appSearchModel.set('all', newSearch);
-
-			// make sure the browser knows where we are
-			MetacatUI.uiRouter.navigate("data", {trigger: true});
-
-			// ...but don't want to follow links
-			return false;
-
-		},
-
-		resetSearch: function(e){
-			e.preventDefault();
-			MetacatUI.appView.resetSearch();
-		},
-
-		hideDropdown: function(e){
-			this.$('.dropdown-menu').addClass('hidden');
-			this.$('.dropdown').removeClass('open');
-		},
-
-		showDropdown: function(e){
-			this.$('.dropdown-menu').removeClass('hidden');
-
-			// Prevent click events immediately following mouseenter events, otherwise
-			// toggleDropdown() is called right after showDropdown on touchscreen devices.
-			// (on touch screen, both mouseenter and click are called when user touches element)
-			this.$('.dropdown .dropdown-toggle').off('click');
-			var view = this;
-			setTimeout(function () {
-				view.$('.dropdown .dropdown-toggle').on('click', function(e){
-					view.toggleDropdown(e)
-				});
-			}, 10);
-		},
-
-		toggleDropdown: function(e){
-			// this.$(".navbar-inner").append(" TOGG: " + e.handleObj.origType)
-			// console.log(e);
-			this.$('.dropdown-menu').toggleClass('hidden');
-			this.$('.dropdown').removeClass('open');
-		},
-
-		showNav: function(){
-			this.$("#main-nav").slideToggle();
-			this.$("#nav-trigger .icon").toggle();
-		},
-
-		showSubNav: function(e){
-			var parentEl = e.target.tagName == "LI"? e.target : $(e.target).parent("li");
-			if(!parentEl || !$(parentEl).length) return;
-
-			$(parentEl).find(".sub-menu").slideToggle();
-		},
-
-		triggerOnEnter: function(e) {
-			if (e.keyCode != 13) return;
-			this.triggerSearch();
-		},
-
-		toggleHeaderType: function(){
-			// set the navbar class based on what the page requested
-			var headerType = MetacatUI.appModel.get('headerType');
-			if (headerType == "default") {
-				//Remove the alt class
-				$(this.$el).removeClass("alt");
-				//Add the class given
-				$(this.$el).addClass(headerType);
-			}
-			else if(headerType == "alt"){
-				//Remove the default class
-				$(this.$el).removeClass("default");
-				//Add the class given
-				$(this.$el).addClass(headerType);
-			}
-		}
-
-	});
-	return NavbarView;
+      /**
+       * @type {string}
+       */
+      el: "#Navbar",
+
+      /**
+       * @type {Underscore.Template}
+       */
+      template: _.template(NavbarTemplate),
+
+      /**
+       * @type {object}
+       */
+      events: {
+        "click #search_btn": "triggerSearch",
+        "keypress #search_txt": "triggerOnEnter",
+        "click .show-new-search": "resetSearch",
+        "click .dropdown-menu a": "hideDropdown",
+        "mouseenter .dropdown": "showDropdown",
+        "mouseleave .dropdown": "hideDropdown",
+        "click #nav-trigger": "showNav",
+        "click .nav li": "showSubNav",
+      },
+
+      initialize: function () {
+        // listen to the MetacatUI.appModel for changes in username
+        this.listenTo(MetacatUI.appUserModel, "change:username", this.render);
+        this.listenTo(MetacatUI.appUserModel, "change:fullName", this.render);
+        this.listenTo(MetacatUI.appUserModel, "change:loggedIn", this.render);
+        this.listenTo(
+          MetacatUI.appModel,
+          "change:headerType",
+          this.toggleHeaderType,
+        );
+      },
+
+      render: function () {
+        var name = MetacatUI.appUserModel.get("fullName")
+          ? MetacatUI.appUserModel.get("fullName").charAt(0).toUpperCase() +
+            MetacatUI.appUserModel.get("fullName").substring(1)
+          : MetacatUI.appUserModel.get("username");
+
+        //Insert the navbar template
+        this.$el.html(
+          this.template({
+            username: MetacatUI.appUserModel.get("username"),
+            formattedName: name,
+            firstName: MetacatUI.appUserModel.get("firstName"),
+            loggedIn: MetacatUI.appUserModel.get("loggedIn"),
+            baseUrl: MetacatUI.appModel.get("baseUrl"),
+          }),
+        );
+
+        //Insert the sign-in button
+        var signInView = new SignInView().render();
+        this.$(".login-container").append(signInView.el);
+        signInView.setUpPopup();
+
+        //Initialize the tooltips in the navbar
+        this.$(".tooltip-this").tooltip({
+          delay: { show: 600 },
+          trigger: "hover",
+          placement: "bottom",
+        });
+
+        this.changeBackground();
+
+        //Check if the temporary message is in this view
+        if (MetacatUI.appModel.get("temporaryMessageContainer") == "#Navbar") {
+          if (typeof MetacatUI.appView.showTemporaryMessage == "function") {
+            MetacatUI.appView.showTemporaryMessage();
+          }
+        }
+      },
+
+      changeBackground: function () {
+        // Change the background image if there is one
+        var imageEl = $("#bg_image");
+        if ($(imageEl).length > 0) {
+          var imgCnt = $(imageEl).attr("data-image-count");
+
+          //Randomly choose the next background image
+          var bgNum = Math.ceil(Math.random() * imgCnt);
+
+          $(imageEl).css(
+            "background-image",
+            "url('" +
+              MetacatUI.root +
+              "/js/themes/" +
+              MetacatUI.theme +
+              "/img/backgrounds/bg" +
+              bgNum +
+              ".jpg')",
+          );
+        }
+      },
+
+      triggerSearch: function () {
+        // Get the search term entered
+        var searchTerm = $("#search_txt").val();
+
+        //Clear the input value
+        $("#search_txt").val("");
+
+        //Clear the search model to start a fresh search
+        MetacatUI.appSearchModel.clear().set(MetacatUI.appSearchModel.defaults);
+
+        //Create a new array with the new search term
+        var newSearch = [searchTerm];
+
+        //Set up the search model for this new term
+        MetacatUI.appSearchModel.set("all", newSearch);
+
+        // make sure the browser knows where we are
+        MetacatUI.uiRouter.navigate("data", { trigger: true });
+
+        // ...but don't want to follow links
+        return false;
+      },
+
+      resetSearch: function (e) {
+        e.preventDefault();
+        MetacatUI.appView.resetSearch();
+      },
+
+      hideDropdown: function (e) {
+        this.$(".dropdown-menu").addClass("hidden");
+        this.$(".dropdown").removeClass("open");
+      },
+
+      showDropdown: function (e) {
+        this.$(".dropdown-menu").removeClass("hidden");
+
+        // Prevent click events immediately following mouseenter events, otherwise
+        // toggleDropdown() is called right after showDropdown on touchscreen devices.
+        // (on touch screen, both mouseenter and click are called when user touches element)
+        this.$(".dropdown .dropdown-toggle").off("click");
+        var view = this;
+        setTimeout(function () {
+          view.$(".dropdown .dropdown-toggle").on("click", function (e) {
+            view.toggleDropdown(e);
+          });
+        }, 10);
+      },
+
+      toggleDropdown: function (e) {
+        // this.$(".navbar-inner").append(" TOGG: " + e.handleObj.origType)
+        // console.log(e);
+        this.$(".dropdown-menu").toggleClass("hidden");
+        this.$(".dropdown").removeClass("open");
+      },
+
+      showNav: function () {
+        this.$("#main-nav").slideToggle();
+        this.$("#nav-trigger .icon").toggle();
+      },
+
+      showSubNav: function (e) {
+        var parentEl =
+          e.target.tagName == "LI" ? e.target : $(e.target).parent("li");
+        if (!parentEl || !$(parentEl).length) return;
+
+        $(parentEl).find(".sub-menu").slideToggle();
+      },
+
+      triggerOnEnter: function (e) {
+        if (e.keyCode != 13) return;
+        this.triggerSearch();
+      },
+
+      toggleHeaderType: function () {
+        // set the navbar class based on what the page requested
+        var headerType = MetacatUI.appModel.get("headerType");
+        if (headerType == "default") {
+          //Remove the alt class
+          $(this.$el).removeClass("alt");
+          //Add the class given
+          $(this.$el).addClass(headerType);
+        } else if (headerType == "alt") {
+          //Remove the default class
+          $(this.$el).removeClass("default");
+          //Add the class given
+          $(this.$el).addClass(headerType);
+        }
+      },
+    },
+  );
+  return NavbarView;
 });
 
diff --git a/docs/docs/src_js_views_RegisterCitationView.js.html b/docs/docs/src_js_views_RegisterCitationView.js.html index 63d956ff7..02696bc13 100644 --- a/docs/docs/src_js_views_RegisterCitationView.js.html +++ b/docs/docs/src_js_views_RegisterCitationView.js.html @@ -44,213 +44,224 @@

Source: src/js/views/RegisterCitationView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', "common/Utilities", 'text!templates/registerCitation.html'],
-    function($, _, Backbone, Utilities, RegisterCitationTemplate) {
-    'use strict';
-
-    /**
-    * @class RegisterCitationView
-    * @classdesc A simple form for a user to input a DOI that cites or uses a dataset in DataONE.
-    * When the form is submitted, the citation is registered with the DataONE Metrics service.
-    * @classcategory Views
-    * @screenshot RegisterCitationView.png
-    * @extends Backbone.View
-    */
-    var RegisterCitationView = Backbone.View.extend(
-      /** @lends RegisterCitationView.prototype */ {
-
-        id:               'citation-modal',
-        className:        'modal fade hide',
-
-        /**
-        * The URL to save the citation to
-        * @type {string}
-        */
-        citationsUrl: MetacatUI.appModel.get("dataoneCitationsUrl"),
-
-        template:         _.template(RegisterCitationTemplate),
-        successFooterTemplate: _.template("<button class='btn btn-indigo'" +
-                                            " data-dismiss='modal'" +
-                                            ">Done</button>"),
-
-        /**
-        * The message to display the citation is successfully submitted
-        * @type {string}
-        */
-        successMessage: 'Thank you! Your citation has been successfully submitted. ' +
-             'It may take up to 24 hours to see the citation on the dataset page.',
-
-       /**
-       * The message to display the citation has failed to submit
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "common/Utilities",
+  "text!templates/registerCitation.html",
+], function ($, _, Backbone, Utilities, RegisterCitationTemplate) {
+  "use strict";
+
+  /**
+   * @class RegisterCitationView
+   * @classdesc A simple form for a user to input a DOI that cites or uses a dataset in DataONE.
+   * When the form is submitted, the citation is registered with the DataONE Metrics service.
+   * @classcategory Views
+   * @screenshot RegisterCitationView.png
+   * @extends Backbone.View
+   */
+  var RegisterCitationView = Backbone.View.extend(
+    /** @lends RegisterCitationView.prototype */ {
+      id: "citation-modal",
+      className: "modal fade hide",
+
+      /**
+       * The URL to save the citation to
        * @type {string}
        */
-        errorMessage: 'Sorry! We encountered an error while registering that citation. Please try ' +
-                      'again or try emailing us the citation.',
-
-        events: {
-          'hidden'                              : 'teardown',
-          'click .btn-register-citation'        : 'registerCitation',
-          "focusout #publication-identifier"    : "validateDOI"
-        },
-
-        initialize: function(options) {
-          _.bindAll(this, 'show', 'teardown', 'render');
-          if((typeof options == "undefined")){
-              var options = {};
-          }
-
-          this.pid = options.pid;
-
-        },
-
-        /**
-        * Shows this view on the page.
-        */
-        show: function() {
-            this.$el.modal('show');
-        },
-
-        /**
-        * Hides and removes this view from the page.
-        */
-        teardown: function() {
-          this.$el.modal('hide');
-          this.$el.data('modal', null);
-          this.remove();
-        },
-
-        /**
-         * Renders the submission form and creates a Bootstrap modal for this view
-         */
-        render: function() {
-          this.$el.html(this.template());
-          this.$el.modal({show:false}); // dont show modal on instantiation
-
-          return this;
-        },
+      citationsUrl: MetacatUI.appModel.get("dataoneCitationsUrl"),
 
+      template: _.template(RegisterCitationTemplate),
+      successFooterTemplate: _.template(
+        "<button class='btn btn-indigo'" +
+          " data-dismiss='modal'" +
+          ">Done</button>",
+      ),
 
+      /**
+       * The message to display the citation is successfully submitted
+       * @type {string}
+       */
+      successMessage:
+        "Thank you! Your citation has been successfully submitted. " +
+        "It may take up to 24 hours to see the citation on the dataset page.",
 
-        /**
-         * Get inputs from the modal and sends it to the DataONE Metrics Service
-         */
-        registerCitation: function() {
-
-          // check if the register button has been disabled
-          if (this.$(".btn-register-citation").is(".disabled")) {
-            return false;
-          }
-
-          // get the input values
-          var publicationIdentifier = this.$("#publication-identifier").val();
-
-          var citationType = this.$("#citationTypeCustomSelect").val();
-          var relation_type = null;
-
-          // If the user has not selected a valid
-          if (citationType != 0) {
-              relation_type = citationType == 1 ? "isCitedBy" : "isReferencedBy";
-          }
-          else {
-              relation_type = "isCitedBy";
-          }
+      /**
+       * The message to display the citation has failed to submit
+       * @type {string}
+       */
+      errorMessage:
+        "Sorry! We encountered an error while registering that citation. Please try " +
+        "again or try emailing us the citation.",
+
+      events: {
+        hidden: "teardown",
+        "click .btn-register-citation": "registerCitation",
+        "focusout #publication-identifier": "validateDOI",
+      },
+
+      initialize: function (options) {
+        _.bindAll(this, "show", "teardown", "render");
+        if (typeof options == "undefined") {
+          var options = {};
+        }
 
-          // get the form data before replacing everything with the loading icon!
-          var formData = {};
-          var citationObject = {};
-          var citaitonRelatedIdentifiersObject = {}
-
-          // initializing the citation POSt object
-          formData["request_type"] = "dataset";
-          formData["submitter"] = MetacatUI.appUserModel.get("username");
-          formData["citations"] = new Array();
-
-          // form the citation object
-          citationObject["related_identifiers"] = new Array();
-          citationObject["source_id"] = publicationIdentifier;
-
-          // set the related identifiers
-          citaitonRelatedIdentifiersObject["identifier"] = this.pid;
-          citaitonRelatedIdentifiersObject["relation_type"] = relation_type;
-
-          // include all the required data
-          citationObject["related_identifiers"].push(citaitonRelatedIdentifiersObject);
-          formData["citations"].push(citationObject);
-
-          // ajax call to submit the given form and then render the results in the content area
-          var viewRef = this;
-
-          var requestSettings = {
-            type: "POST",
-            url: this.citationsUrl,
-            contentType: false,
-            processData: false,
-            data: JSON.stringify(formData),
-            dataType: "json",
-            success: function(data, textStatus, jqXHR) {
-
-              MetacatUI.appView.showAlert(viewRef.successMessage, "alert-success",
-                                          viewRef.$(".modal-body"), null,
-                                          { includeEmail: false,
-                                             replaceContents: true
-                                           });
-
-              viewRef.$(".modal-footer").html(viewRef.successFooterTemplate());
-            },
-            error: function(){
-
-              MetacatUI.appView.showAlert(viewRef.errorMessage, "alert-error",
-                                          viewRef.$(".modal-body"), null,
-                                          { includeEmail: true,
-                                             replaceContents: true
-                                           });
-
-            }
-          }
+        this.pid = options.pid;
+      },
 
-          $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
+      /**
+       * Shows this view on the page.
+       */
+      show: function () {
+        this.$el.modal("show");
+      },
 
-        },
+      /**
+       * Hides and removes this view from the page.
+       */
+      teardown: function () {
+        this.$el.modal("hide");
+        this.$el.data("modal", null);
+        this.remove();
+      },
+
+      /**
+       * Renders the submission form and creates a Bootstrap modal for this view
+       */
+      render: function () {
+        this.$el.html(this.template());
+        this.$el.modal({ show: false }); // dont show modal on instantiation
 
-        /**
-        * Validates if the given input is a valid DOI string or not
-        * @since 2.15.0
-        * @return {undefined}
-        */
-        validateDOI: function(){
-          var identifierInput = this.$("#publication-identifier").val();
+        return this;
+      },
 
-          if(!(Utilities.isValidDOI(identifierInput))){
+      /**
+       * Get inputs from the modal and sends it to the DataONE Metrics Service
+       */
+      registerCitation: function () {
+        // check if the register button has been disabled
+        if (this.$(".btn-register-citation").is(".disabled")) {
+          return false;
+        }
 
-            //Show a warning that the user was trying to edit old content
-            MetacatUI.appView.showAlert({
-              message: "Please enter a valid DOI.",
-              classes: "alert-error",
-              container: this.$("#publication-identifier").parent(),
-              remove: true
-            });
+        // get the input values
+        var publicationIdentifier = this.$("#publication-identifier").val();
 
-            this.$("#publication-identifier").addClass("register-citation-doi-validation");
-            this.$(".btn-register-citation").addClass("disabled")
-          }
-          else {
-            //Remove the validation error
-            this.$(".alert-container").remove();
+        var citationType = this.$("#citationTypeCustomSelect").val();
+        var relation_type = null;
 
-            this.$("#publication-identifier").removeClass("register-citation-doi-validation");
+        // If the user has not selected a valid
+        if (citationType != 0) {
+          relation_type = citationType == 1 ? "isCitedBy" : "isReferencedBy";
+        } else {
+          relation_type = "isCitedBy";
+        }
 
-            // If the Disabled class is active
-            if (this.$(".btn-register-citation").find(".disabled")) {
-              this.$(".btn-register-citation").removeClass("disabled");
-            }
+        // get the form data before replacing everything with the loading icon!
+        var formData = {};
+        var citationObject = {};
+        var citaitonRelatedIdentifiersObject = {};
+
+        // initializing the citation POSt object
+        formData["request_type"] = "dataset";
+        formData["submitter"] = MetacatUI.appUserModel.get("username");
+        formData["citations"] = new Array();
+
+        // form the citation object
+        citationObject["related_identifiers"] = new Array();
+        citationObject["source_id"] = publicationIdentifier;
+
+        // set the related identifiers
+        citaitonRelatedIdentifiersObject["identifier"] = this.pid;
+        citaitonRelatedIdentifiersObject["relation_type"] = relation_type;
+
+        // include all the required data
+        citationObject["related_identifiers"].push(
+          citaitonRelatedIdentifiersObject,
+        );
+        formData["citations"].push(citationObject);
+
+        // ajax call to submit the given form and then render the results in the content area
+        var viewRef = this;
+
+        var requestSettings = {
+          type: "POST",
+          url: this.citationsUrl,
+          contentType: false,
+          processData: false,
+          data: JSON.stringify(formData),
+          dataType: "json",
+          success: function (data, textStatus, jqXHR) {
+            MetacatUI.appView.showAlert(
+              viewRef.successMessage,
+              "alert-success",
+              viewRef.$(".modal-body"),
+              null,
+              { includeEmail: false, replaceContents: true },
+            );
+
+            viewRef.$(".modal-footer").html(viewRef.successFooterTemplate());
+          },
+          error: function () {
+            MetacatUI.appView.showAlert(
+              viewRef.errorMessage,
+              "alert-error",
+              viewRef.$(".modal-body"),
+              null,
+              { includeEmail: true, replaceContents: true },
+            );
+          },
+        };
+
+        $.ajax(
+          _.extend(
+            requestSettings,
+            MetacatUI.appUserModel.createAjaxSettings(),
+          ),
+        );
+      },
+
+      /**
+       * Validates if the given input is a valid DOI string or not
+       * @since 2.15.0
+       * @return {undefined}
+       */
+      validateDOI: function () {
+        var identifierInput = this.$("#publication-identifier").val();
+
+        if (!Utilities.isValidDOI(identifierInput)) {
+          //Show a warning that the user was trying to edit old content
+          MetacatUI.appView.showAlert({
+            message: "Please enter a valid DOI.",
+            classes: "alert-error",
+            container: this.$("#publication-identifier").parent(),
+            remove: true,
+          });
+
+          this.$("#publication-identifier").addClass(
+            "register-citation-doi-validation",
+          );
+          this.$(".btn-register-citation").addClass("disabled");
+        } else {
+          //Remove the validation error
+          this.$(".alert-container").remove();
+
+          this.$("#publication-identifier").removeClass(
+            "register-citation-doi-validation",
+          );
+
+          // If the Disabled class is active
+          if (this.$(".btn-register-citation").find(".disabled")) {
+            this.$(".btn-register-citation").removeClass("disabled");
           }
         }
+      },
+    },
+  );
 
-    });
-
-     return RegisterCitationView;
-  });
+  return RegisterCitationView;
+});
 
diff --git a/docs/docs/src_js_views_SignInView.js.html b/docs/docs/src_js_views_SignInView.js.html index da3561bf6..ef88ceeeb 100644 --- a/docs/docs/src_js_views_SignInView.js.html +++ b/docs/docs/src_js_views_SignInView.js.html @@ -44,348 +44,412 @@

Source: src/js/views/SignInView.js

-
/*global define */
-
-define(['jquery', 'underscore', 'backbone', 'text!templates/login.html',
-        'text!templates/alert.html', 'text!templates/loginButtons.html',
-        'text!templates/loginOptions.html', 'text!templates/login-ldap.html'],
-  function($, _, Backbone, LoginTemplate, AlertTemplate, LoginButtonsTemplate, LoginOptionsTemplate, LdapLoginTemplate) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/login.html",
+  "text!templates/alert.html",
+  "text!templates/loginButtons.html",
+  "text!templates/loginOptions.html",
+  "text!templates/login-ldap.html",
+], function (
+  $,
+  _,
+  Backbone,
+  LoginTemplate,
+  AlertTemplate,
+  LoginButtonsTemplate,
+  LoginOptionsTemplate,
+  LdapLoginTemplate,
+) {
+  "use strict";
 
   /**
-  * @class SignInView
-  * @classcategory Views
-  * @extends Backbone.View
-  * @screenshot views/SignInView.png
-  */
+   * @class SignInView
+   * @classcategory Views
+   * @extends Backbone.View
+   * @screenshot views/SignInView.png
+   */
   var SignInView = Backbone.View.extend(
-    /** @lends SignInView.prototype */{
-
-    template: _.template(LoginTemplate),
-    alertTemplate: _.template(AlertTemplate),
-    buttonsTemplate: _.template(LoginButtonsTemplate),
-    loginOptionsTemplate: _.template(LoginOptionsTemplate),
-    ldapTemplate: _.template(LdapLoginTemplate),
-
-    tagName: "div",
-    className: "sign-in-btns",
-
-    ldapError: false,
-
-    /* Set to true to only show the LDAP login form */
-    ldapOnly: false,
-
-    /* Set to true if this SignInView is the only thing on the page */
-    fullPage: false,
-
-    /* Set to true if this SignInView is in a modal window */
-    inPlace: false,
-
-    /**
-    * Set a query string that will be added to the redirect URL
-    * when the user has logged-in and is returned back to MetacatUI.
-    * This can be useful to return back to MetacatUI with additional information
-    * about the state of the app when the user left to sign in.
-    * @type {string}
-    * @example
-    * // This example may tell the view that the register citation modal was open when Sign In was clicked
-    * "registerCitation=true"
-    * @default ""
-    * @since 2.15.0
-    */
-    redirectQueryString: "",
-
-    /*A message to display at the top of the view */
-    topMessage: "",
-
-    initialize: function(options){
-      if(typeof options !== "undefined"){
-        this.inPlace = options.inPlace;
-        this.topMessage = options.topMessage;
-        this.fullPage = options.fullPage;
-        this.closeButtons = options.closeButtons === false? false : true;
-      }
-    },
-
-    render: function(){
-      //Don't render a SignIn view if there are no Sign In URLs configured
-      if(!MetacatUI.appModel.get("signInUrlOrcid"))
-        return this;
-
-      var view = this;
-
-      if(this.inPlace){
-        this.$el.addClass("hidden modal");
-        this.$el.attr("data-backdrop", "static");
-
-        //Add a message to the top, if supplied
-        if(typeof this.topMessage == "string")
-          this.$el.prepend('<p class="center container">' + this.topMessage + '</p>');
-        else if(typeof this.topMessage == "object")
-          this.$el.prepend(this.topMessage);
-
-        //Copy/paste the contents of the sign-in popup
-        var signInBtns = $.parseHTML($("#signinPopup").html().trim()),
-          signInBtnsContainer = $(document.createElement("div")).addClass("center container").html(signInBtns);
-
-        signInBtnsContainer.find("a.signin").each(function(i, a){
-          var url = $(a).attr("href");
-
-          var redirectUrl = decodeURIComponent(url.substring( url.indexOf("target=")+7 ));
-
-          var urlWithoutHttp = redirectUrl.substring( redirectUrl.indexOf("://") +  3 );
-          var routeName = urlWithoutHttp.substring( urlWithoutHttp.indexOf("/") + 1 );
-
-          if( !routeName ){
-            redirectUrl = redirectUrl + "signinsuccess";
-          }
-          else if( _.contains(MetacatUI.uiRouter.getRouteNames(), MetacatUI.uiRouter.getRouteName(routeName)) ){
-            redirectUrl = redirectUrl.substring(0, redirectUrl.indexOf(routeName)) + "signinsuccess";
-          }
-
-          url = url.substring(0, url.indexOf("target=")+7) + encodeURIComponent(redirectUrl);
-          $(a).attr("href", url);
-        });
-
-        signInBtnsContainer.find("h1, h2").remove();
-
-        this.$el.append(signInBtnsContainer);
-
-        //Remove the accordion widget from the ldap login so it gets displayed as a popup window instead
-        if( this.$("#signinLdap").length ){
-          this.$("[href='" + "#signinLdap']").addClass("signin");
-          this.$(".accordion").removeClass("accordion");
+    /** @lends SignInView.prototype */ {
+      template: _.template(LoginTemplate),
+      alertTemplate: _.template(AlertTemplate),
+      buttonsTemplate: _.template(LoginButtonsTemplate),
+      loginOptionsTemplate: _.template(LoginOptionsTemplate),
+      ldapTemplate: _.template(LdapLoginTemplate),
+
+      tagName: "div",
+      className: "sign-in-btns",
+
+      ldapError: false,
+
+      /* Set to true to only show the LDAP login form */
+      ldapOnly: false,
+
+      /* Set to true if this SignInView is the only thing on the page */
+      fullPage: false,
+
+      /* Set to true if this SignInView is in a modal window */
+      inPlace: false,
+
+      /**
+       * Set a query string that will be added to the redirect URL
+       * when the user has logged-in and is returned back to MetacatUI.
+       * This can be useful to return back to MetacatUI with additional information
+       * about the state of the app when the user left to sign in.
+       * @type {string}
+       * @example
+       * // This example may tell the view that the register citation modal was open when Sign In was clicked
+       * "registerCitation=true"
+       * @default ""
+       * @since 2.15.0
+       */
+      redirectQueryString: "",
+
+      /*A message to display at the top of the view */
+      topMessage: "",
+
+      initialize: function (options) {
+        if (typeof options !== "undefined") {
+          this.inPlace = options.inPlace;
+          this.topMessage = options.topMessage;
+          this.fullPage = options.fullPage;
+          this.closeButtons = options.closeButtons === false ? false : true;
         }
-
-        this.$el.prepend($(document.createElement("div")).addClass("container").prepend(
-            $(document.createElement("a")).text("Close").addClass("close").prepend(
-            $(document.createElement("i")).addClass("icon icon-on-left icon-remove"))));
-
-        //Listen for clicks
-        this.$("a.signin").on("click", function(e){
-          //Get the link URL and change the target to a special route
-          e.preventDefault();
-
-          var link = e.target;
-          if(link.nodeName != "A") link = $(link).parents("a");
-
-          var url = $(link).attr("href");
-
-          //Open up a new small window with this URL to allow the user to login
-          window.open(url, "Login", "height=600,width=700");
-
-          //Listen for successful sign-in
-          window.listenForSignIn = setInterval(function(){
-            MetacatUI.appUserModel.checkToken(function(data){
-              $(".modal.sign-in-btns").modal("hide");
-              clearInterval(window.listenForSignIn);
-
-              if(MetacatUI.appUserModel.get("checked"))
-                MetacatUI.appUserModel.trigger("change:checked");
-              else
-                MetacatUI.appUserModel.set("checked", true);
-            });
-          }, 750);
-        });
-
-        if( this.closeButtons ){
-          this.$("a.close").on("click", function(e){
-            view.$el.modal("hide");
+      },
+
+      render: function () {
+        //Don't render a SignIn view if there are no Sign In URLs configured
+        if (!MetacatUI.appModel.get("signInUrlOrcid")) return this;
+
+        var view = this;
+
+        if (this.inPlace) {
+          this.$el.addClass("hidden modal");
+          this.$el.attr("data-backdrop", "static");
+
+          //Add a message to the top, if supplied
+          if (typeof this.topMessage == "string")
+            this.$el.prepend(
+              '<p class="center container">' + this.topMessage + "</p>",
+            );
+          else if (typeof this.topMessage == "object")
+            this.$el.prepend(this.topMessage);
+
+          //Copy/paste the contents of the sign-in popup
+          var signInBtns = $.parseHTML($("#signinPopup").html().trim()),
+            signInBtnsContainer = $(document.createElement("div"))
+              .addClass("center container")
+              .html(signInBtns);
+
+          signInBtnsContainer.find("a.signin").each(function (i, a) {
+            var url = $(a).attr("href");
+
+            var redirectUrl = decodeURIComponent(
+              url.substring(url.indexOf("target=") + 7),
+            );
+
+            var urlWithoutHttp = redirectUrl.substring(
+              redirectUrl.indexOf("://") + 3,
+            );
+            var routeName = urlWithoutHttp.substring(
+              urlWithoutHttp.indexOf("/") + 1,
+            );
+
+            if (!routeName) {
+              redirectUrl = redirectUrl + "signinsuccess";
+            } else if (
+              _.contains(
+                MetacatUI.uiRouter.getRouteNames(),
+                MetacatUI.uiRouter.getRouteName(routeName),
+              )
+            ) {
+              redirectUrl =
+                redirectUrl.substring(0, redirectUrl.indexOf(routeName)) +
+                "signinsuccess";
+            }
+
+            url =
+              url.substring(0, url.indexOf("target=") + 7) +
+              encodeURIComponent(redirectUrl);
+            $(a).attr("href", url);
           });
-        }
-        else{
-          this.$(".close").remove();
 
-          //Create a sign out link
-          var divider     = document.createElement("hr"),
-              signOutLink = $(document.createElement("a"))
-                              .addClass("error underline")
-                              .text("Sign Out");
+          signInBtnsContainer.find("h1, h2").remove();
 
-          //Add the Sign Out link to the view
-          this.$(".modal-body").append(divider, signOutLink);
+          this.$el.append(signInBtnsContainer);
 
-          //If we're on the EML211EditorView, then show a warning message that unsaved changes will be lost
-          if( MetacatUI.appView.currentView && MetacatUI.appView.currentView.type == "EML211Editor" ){
-            signOutLink.after( $(document.createElement("p"))
-              .addClass("error")
-              .text("   Warning! - All your unsaved changes will be lost."));
+          //Remove the accordion widget from the ldap login so it gets displayed as a popup window instead
+          if (this.$("#signinLdap").length) {
+            this.$("[href='" + "#signinLdap']").addClass("signin");
+            this.$(".accordion").removeClass("accordion");
           }
 
-          //When the sign out link is clicked, we can just refresh the page.
-          signOutLink.on("click", function(e){
-            window.location.reload();
+          this.$el.prepend(
+            $(document.createElement("div"))
+              .addClass("container")
+              .prepend(
+                $(document.createElement("a"))
+                  .text("Close")
+                  .addClass("close")
+                  .prepend(
+                    $(document.createElement("i")).addClass(
+                      "icon icon-on-left icon-remove",
+                    ),
+                  ),
+              ),
+          );
+
+          //Listen for clicks
+          this.$("a.signin").on("click", function (e) {
+            //Get the link URL and change the target to a special route
+            e.preventDefault();
+
+            var link = e.target;
+            if (link.nodeName != "A") link = $(link).parents("a");
+
+            var url = $(link).attr("href");
+
+            //Open up a new small window with this URL to allow the user to login
+            window.open(url, "Login", "height=600,width=700");
+
+            //Listen for successful sign-in
+            window.listenForSignIn = setInterval(function () {
+              MetacatUI.appUserModel.checkToken(function (data) {
+                $(".modal.sign-in-btns").modal("hide");
+                clearInterval(window.listenForSignIn);
+
+                if (MetacatUI.appUserModel.get("checked"))
+                  MetacatUI.appUserModel.trigger("change:checked");
+                else MetacatUI.appUserModel.set("checked", true);
+              });
+            }, 750);
           });
 
-        }
-      }
-      else{
-
-        //If it's a full-page sign-in view, then empty it first
-        if(this.el == MetacatUI.appView.el || this.fullPage){
-          this.$el.empty();
-          var container = document.createElement("div");
-          container.className = "container login";
-
-          if( this.ldapOnly ){
-
-            var redirectUrl = window.location.origin + window.location.pathname;
-            redirectUrl = redirectUrl.substring(0, redirectUrl.lastIndexOf("/"));
-            redirectUrl + "/signinSuccessLdap";
-
-            $(container).append(this.ldapTemplate({
-              redirectUrl:  redirectUrl
-            }));
-            this.$el.append(container);
+          if (this.closeButtons) {
+            this.$("a.close").on("click", function (e) {
+              view.$el.modal("hide");
+            });
+          } else {
+            this.$(".close").remove();
 
-            //Hide all the other page elements so it's just the login form
-            $("#Navbar, #HeaderContainer, #Footer").hide();
+            //Create a sign out link
+            var divider = document.createElement("hr"),
+              signOutLink = $(document.createElement("a"))
+                .addClass("error underline")
+                .text("Sign Out");
+
+            //Add the Sign Out link to the view
+            this.$(".modal-body").append(divider, signOutLink);
+
+            //If we're on the EML211EditorView, then show a warning message that unsaved changes will be lost
+            if (
+              MetacatUI.appView.currentView &&
+              MetacatUI.appView.currentView.type == "EML211Editor"
+            ) {
+              signOutLink.after(
+                $(document.createElement("p"))
+                  .addClass("error")
+                  .text("   Warning! - All your unsaved changes will be lost."),
+              );
+            }
+
+            //When the sign out link is clicked, we can just refresh the page.
+            signOutLink.on("click", function (e) {
+              window.location.reload();
+            });
           }
-          else{
-            $(container).append(this.buttonsTemplate({
-              signInUrl: MetacatUI.appModel.get('signInUrlOrcid') + this.getRedirectURL()
-            }));
-            this.$el.append(container);
+        } else {
+          //If it's a full-page sign-in view, then empty it first
+          if (this.el == MetacatUI.appView.el || this.fullPage) {
+            this.$el.empty();
+            var container = document.createElement("div");
+            container.className = "container login";
+
+            if (this.ldapOnly) {
+              var redirectUrl =
+                window.location.origin + window.location.pathname;
+              redirectUrl = redirectUrl.substring(
+                0,
+                redirectUrl.lastIndexOf("/"),
+              );
+              redirectUrl + "/signinSuccessLdap";
+
+              $(container).append(
+                this.ldapTemplate({
+                  redirectUrl: redirectUrl,
+                }),
+              );
+              this.$el.append(container);
+
+              //Hide all the other page elements so it's just the login form
+              $("#Navbar, #HeaderContainer, #Footer").hide();
+            } else {
+              $(container).append(
+                this.buttonsTemplate({
+                  signInUrl:
+                    MetacatUI.appModel.get("signInUrlOrcid") +
+                    this.getRedirectURL(),
+                }),
+              );
+              this.$el.append(container);
+            }
+          } else {
+            if (this.ldapOnly) {
+              var redirectUrl = MetacatUI.root + "/signinSuccessLdap";
+
+              this.$el.append(
+                this.ldapTemplate({
+                  redirectUrl: redirectUrl,
+                }),
+              );
+            } else {
+              let signInUrl =
+                MetacatUI.appModel.get("signInUrlOrcid") +
+                this.getRedirectURL();
+
+              this.$el.append(
+                this.buttonsTemplate({
+                  signInUrl: signInUrl,
+                }),
+              );
+            }
           }
 
-        }
-        else{
-
-          if( this.ldapOnly ){
-
-            var redirectUrl = MetacatUI.root + "/signinSuccessLdap";
-
-            this.$el.append(this.ldapTemplate({
-              redirectUrl: redirectUrl
-            }));
-
+          //Insert the sign in popup screen once
+          if (!$("#signinPopup").length) {
+            var target = this.getRedirectURL();
+            var signInUrl = MetacatUI.appModel.get("signInUrl")
+              ? MetacatUI.appModel.get("signInUrl") + target
+              : null;
+            var signInUrlOrcid = MetacatUI.appModel.get("signInUrlOrcid")
+              ? MetacatUI.appModel.get("signInUrlOrcid") + target
+              : null;
+            var signInUrlLdap = MetacatUI.appModel.get("signInUrlLdap")
+                ? MetacatUI.appModel.get("signInUrlLdap") + target
+                : null,
+              redirectUrl =
+                window.location.href.indexOf("signinldaperror") > -1
+                  ? window.location.href.replace("signinldaperror", "")
+                  : window.location.href;
+
+            $("body").append(
+              this.template({
+                signInUrl: signInUrl,
+                signInUrlOrcid: signInUrlOrcid,
+                signInUrlLdap: signInUrlLdap,
+                ldapLoginForm: this.ldapTemplate({
+                  redirectUrl: redirectUrl,
+                }),
+                currentUrl: window.location.href,
+                loginOptions: this.loginOptionsTemplate({
+                  signInUrl: signInUrl,
+                }).trim(),
+                collapseLdap: !MetacatUI.appUserModel.get("errorLogin"),
+                redirectUrl: redirectUrl,
+              }),
+            );
+
+            this.setUpPopup();
           }
-          else{
-            let signInUrl = MetacatUI.appModel.get('signInUrlOrcid') + this.getRedirectURL();
 
-            this.$el.append(this.buttonsTemplate({
-              signInUrl: signInUrl
-            }));
+          //If there is an error message in the URL, it means authentication has failed
+          if (this.ldapError) {
+            MetacatUI.appUserModel.failedLdapLogin();
+            this.failedLdapLogin();
           }
-
         }
 
-        //Insert the sign in popup screen once
-        if(!$("#signinPopup").length){
-          var target = this.getRedirectURL();
-          var signInUrl = MetacatUI.appModel.get('signInUrl')? MetacatUI.appModel.get('signInUrl') + target : null;
-          var signInUrlOrcid = MetacatUI.appModel.get('signInUrlOrcid') ? MetacatUI.appModel.get('signInUrlOrcid') + target : null;
-          var signInUrlLdap = MetacatUI.appModel.get('signInUrlLdap') ? MetacatUI.appModel.get('signInUrlLdap') + target : null,
-              redirectUrl = (window.location.href.indexOf("signinldaperror") > -1) ?
-                  window.location.href.replace("signinldaperror", "") : window.location.href;
-
-          $("body").append(this.template({
-            signInUrl:  signInUrl,
-            signInUrlOrcid:  signInUrlOrcid,
-            signInUrlLdap:  signInUrlLdap,
-            ldapLoginForm: this.ldapTemplate({
-              redirectUrl: redirectUrl
-            }),
-            currentUrl: window.location.href,
-            loginOptions: this.loginOptionsTemplate({ signInUrl: signInUrl }).trim(),
-            collapseLdap: !MetacatUI.appUserModel.get("errorLogin"),
-            redirectUrl: redirectUrl
-          }));
-
-          this.setUpPopup();
+        return this;
+      },
+
+      /*
+       * This function is executed when LDAP authentication fails in the DataONE portal
+       */
+      failedLdapLogin: function () {
+        //Insert an error message
+        this.$("form").before(
+          this.alertTemplate({
+            classes: "alert-error",
+            msg: "Incorrect username or password. Please try again.",
+          }),
+        );
+
+        //If this is a full-page sign-in view, then take the form and insert it into the page
+        if (this.$el.attr("id") == "Content" && !$("#Content form").length)
+          $("#Content").html($("#signinLdap").html());
+        //Else, just show the login in the modal window
+        else if (!this.ldapOnly) {
+          $("#signinPopup").modal("show");
         }
 
-        //If there is an error message in the URL, it means authentication has failed
-        if(this.ldapError){
-          MetacatUI.appUserModel.failedLdapLogin();
-          this.failedLdapLogin();
-        };
-      }
-
-      return this;
-    },
-
-    /*
-     * This function is executed when LDAP authentication fails in the DataONE portal
-     */
-    failedLdapLogin: function(){
-      //Insert an error message
-      this.$("form").before(this.alertTemplate({
-        classes: "alert-error",
-        msg: "Incorrect username or password. Please try again."
-      }));
-
-      //If this is a full-page sign-in view, then take the form and insert it into the page
-      if(this.$el.attr("id") == "Content" && !$("#Content form").length)
-        $("#Content").html( $("#signinLdap").html() );
-      //Else, just show the login in the modal window
-      else if(!this.ldapOnly){
-        $("#signinPopup").modal("show");
-      }
-
-      //Show the LDAP login form
-      $('#signinLdap').removeClass("collapse").css("height", "auto");
-    },
-
-    setUpPopup: function(){
-      var view = this;
-
-      //Initialize the modal elements
-      $("#signupPopup, #signinPopup").modal({
-        show: false,
-        shown: function(){
-
-          //Update the sign-in URLs so we are redirected back to the previous page after authentication
-          if( MetacatUI.appModel.get("enableCILogonSignIn") ){
-            $("a.update-sign-in-url").attr("href", MetacatUI.appModel.get("signInUrl") + encodeURIComponent(window.location.href));
+        //Show the LDAP login form
+        $("#signinLdap").removeClass("collapse").css("height", "auto");
+      },
+
+      setUpPopup: function () {
+        var view = this;
+
+        //Initialize the modal elements
+        $("#signupPopup, #signinPopup").modal({
+          show: false,
+          shown: function () {
+            //Update the sign-in URLs so we are redirected back to the previous page after authentication
+            if (MetacatUI.appModel.get("enableCILogonSignIn")) {
+              $("a.update-sign-in-url").attr(
+                "href",
+                MetacatUI.appModel.get("signInUrl") +
+                  encodeURIComponent(window.location.href),
+              );
+            }
+            $("a.update-orcid-sign-in-url").attr(
+              "href",
+              MetacatUI.appModel.get("signInUrlOrcid") +
+                encodeURIComponent(window.location.href),
+            );
+          },
+        });
+      },
+
+      /**
+       * Constructs and returns a string of the URL that the user should return to when they are done signing in.
+       * This URL is sent to the DataONE portal service during login, via the `target` URL attribute. The DataONE
+       * portal will redirect the user back to the `target` URL when sign in is complete.
+       * @returns {string}
+       */
+      getRedirectURL: function () {
+        let redirectURL = window.location.href;
+
+        if (this.redirectQueryString && this.redirectQueryString.length) {
+          let currentQueryString = window.location.search;
+
+          //If there is a current query string in the window.location, concatenate the
+          // new query string properly
+          if (currentQueryString.length) {
+            //Exclude the "?" character from the query string, if it is there already
+            if (this.redirectQueryString.charAt(0) == "?") {
+              this.redirectQueryString = this.redirectQueryString.substring(1);
+            }
+
+            //Add the new query string parameters
+            redirectURL += "&" + this.redirectQueryString;
+          } else {
+            redirectURL += "?" + this.redirectQueryString;
           }
-          $("a.update-orcid-sign-in-url").attr("href", MetacatUI.appModel.get("signInUrlOrcid") + encodeURIComponent(window.location.href));
-
         }
-      });
-    },
 
-    /**
-    * Constructs and returns a string of the URL that the user should return to when they are done signing in.
-    * This URL is sent to the DataONE portal service during login, via the `target` URL attribute. The DataONE
-    * portal will redirect the user back to the `target` URL when sign in is complete.
-    * @returns {string}
-    */
-    getRedirectURL: function(){
-      let redirectURL = window.location.href;
-
-      if( this.redirectQueryString && this.redirectQueryString.length ){
-        let currentQueryString = window.location.search;
-
-        //If there is a current query string in the window.location, concatenate the
-        // new query string properly
-        if( currentQueryString.length ){
-
-          //Exclude the "?" character from the query string, if it is there already
-          if( this.redirectQueryString.charAt(0) == "?" ){
-            this.redirectQueryString = this.redirectQueryString.substring(1)
-          }
-
-          //Add the new query string parameters
-          redirectURL += "&" + this.redirectQueryString;
+        return redirectURL;
+      },
 
-        }
-        else{
-          redirectURL += "?" + this.redirectQueryString;
-        }
-      }
+      onClose: function () {
+        this.$el.empty();
 
-      return redirectURL;
+        if (window.listenForSignIn) clearInterval(window.listenForSignIn);
+      },
     },
-
-    onClose: function(){
-      this.$el.empty();
-
-      if(window.listenForSignIn)  clearInterval(window.listenForSignIn);
-    }
-  });
+  );
   return SignInView;
-
 });
 
diff --git a/docs/docs/src_js_views_StatsView.js.html b/docs/docs/src_js_views_StatsView.js.html index 9fc927061..a9a9864b1 100644 --- a/docs/docs/src_js_views_StatsView.js.html +++ b/docs/docs/src_js_views_StatsView.js.html @@ -44,990 +44,1219 @@

Source: src/js/views/StatsView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'd3', 'LineChart', 'BarChart', 'DonutChart', 'CircleBadge',
-'collections/Citations', 'models/MetricsModel', 'models/Stats', 'MetricsChart', 'views/CitationListView',
-'text!templates/metricModalTemplate.html',  'text!templates/profile.html',
-'text!templates/alert.html', 'text!templates/loading.html', 'text!templates/loading-metrics.html', ],
-	function($, _, Backbone, d3, LineChart, BarChart, DonutChart, CircleBadge, Citations, MetricsModel,
-    StatsModel, MetricsChart, CitationList, MetricModalTemplate, profileTemplate, AlertTemplate,
-    LoadingTemplate, MetricsLoadingTemplate) {
-	'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "d3",
+  "LineChart",
+  "BarChart",
+  "DonutChart",
+  "CircleBadge",
+  "collections/Citations",
+  "models/MetricsModel",
+  "models/Stats",
+  "MetricsChart",
+  "views/CitationListView",
+  "text!templates/metricModalTemplate.html",
+  "text!templates/profile.html",
+  "text!templates/alert.html",
+  "text!templates/loading.html",
+  "text!templates/loading-metrics.html",
+], function (
+  $,
+  _,
+  Backbone,
+  d3,
+  LineChart,
+  BarChart,
+  DonutChart,
+  CircleBadge,
+  Citations,
+  MetricsModel,
+  StatsModel,
+  MetricsChart,
+  CitationList,
+  MetricModalTemplate,
+  profileTemplate,
+  AlertTemplate,
+  LoadingTemplate,
+  MetricsLoadingTemplate,
+) {
+  "use strict";
+
+  var StatsView = Backbone.View.extend(
+    /** @lends StatsView.prototype */ {
+      el: "#Content",
+
+      model: null,
+
+      hideUpdatesChart: false,
+
+      /*
+       * Flag to indicate whether the statsview is a node summary view
+       * @type {boolean}
+       */
+      nodeSummaryView: false,
+
+      /**
+       * Whether or not to show the graph that indicated the assessment score for all metadata in the query.
+       * @type {boolean}
+       */
+      hideMetadataAssessment: false,
+
+      subviews: [],
+
+      template: _.template(profileTemplate),
+
+      metricTemplate: _.template(MetricModalTemplate),
+
+      alertTemplate: _.template(AlertTemplate),
+
+      loadingTemplate: _.template(LoadingTemplate),
+
+      metricsLoadingTemplate: _.template(MetricsLoadingTemplate),
+
+      initialize: function (options) {
+        if (!options) options = {};
+
+        this.title =
+          typeof options.title === "undefined"
+            ? "Summary of Holdings"
+            : options.title;
+        this.description =
+          typeof options.description === "undefined"
+            ? "A summary of all datasets in our catalog."
+            : options.description;
+        this.metricsModel =
+          typeof options.metricsModel === undefined
+            ? undefined
+            : options.metricsModel;
+
+        this.userType =
+          typeof options.userType === undefined ? undefined : options.userType;
+        this.userId =
+          typeof options.userId === undefined ? undefined : options.userId;
+        this.userLabel =
+          typeof options.userLabel === undefined
+            ? undefined
+            : options.userLabel;
+
+        if (typeof options.el === "undefined") this.el = options.el;
+
+        this.hideUpdatesChart =
+          options.hideUpdatesChart === true ? true : false;
+        this.hideMetadataAssessment =
+          typeof options.hideMetadataAssessment === "undefined"
+            ? true
+            : options.hideMetadataAssessment;
+        this.hideCitationsChart =
+          typeof options.hideCitationsChart === "undefined"
+            ? true
+            : options.hideCitationsChart;
+        this.hideDownloadsChart =
+          typeof options.hideDownloadsChart === "undefined"
+            ? true
+            : options.hideDownloadsChart;
+        this.hideViewsChart =
+          typeof options.hideViewsChart === "undefined"
+            ? true
+            : options.hideViewsChart;
+
+        this.model = options.model || null;
+      },
+
+      render: function (options) {
+        //The Node info needs to be fetched first since a lot of this code requires info about MNs
+        if (
+          !MetacatUI.nodeModel.get("checked") &&
+          !MetacatUI.nodeModel.get("error")
+        ) {
+          this.listenToOnce(
+            MetacatUI.nodeModel,
+            "change:checked error",
+            function () {
+              //Remove listeners and render the view, even if there was an error fetching the NodeModel
+              this.stopListening(MetacatUI.nodeModel);
+              this.render(options);
+            },
+          );
+
+          this.$el.html(this.loadingTemplate);
+
+          return;
+        }
 
-	var StatsView = Backbone.View.extend(
-  	/** @lends StatsView.prototype */{
+        if (!options) options = {};
 
-		el: '#Content',
+        var view = this,
+          userIsCN = false,
+          nodeId,
+          isHostedRepo = false;
 
-		model: null,
+        // Check if the node is a coordinating node
+        this.userIsCN = userIsCN;
+        if (this.userType !== undefined && this.userLabel !== undefined) {
+          if (this.userType === "repository") {
+            userIsCN = MetacatUI.nodeModel.isCN(this.userId);
+            if (userIsCN && typeof isCN !== "undefined") this.userIsCN = true;
+          }
+        }
 
-		hideUpdatesChart: false,
+        if (options.nodeSummaryView) {
+          this.nodeSummaryView = true;
+          nodeId = MetacatUI.appModel.get("nodeId");
+          userIsCN = MetacatUI.nodeModel.isCN(nodeId);
+
+          //Save whether this profile is for a CN
+          if (userIsCN && typeof userIsCN !== "undefined") {
+            this.userIsCN = true;
+          }
+          //Figure out if this profile is for a hosted repo
+          else if (nodeId) {
+            isHostedRepo = _.contains(
+              MetacatUI.appModel.get("dataoneHostedRepos"),
+              nodeId,
+            );
+          }
+
+          // Disable the metrics if the nodeId is not available or if it is not a DataONE Hosted Repo
+          if (
+            !this.userIsCN &&
+            (nodeId === "undefined" || nodeId === null || !isHostedRepo)
+          ) {
+            this.hideCitationsChart = true;
+            this.hideDownloadsChart = true;
+            this.hideViewsChart = true;
+            this.hideMetadataAssessment = true;
+          } else {
+            // Overwrite the metrics display flags as set in the AppModel
+            this.hideMetadataAssessment = MetacatUI.appModel.get(
+              "hideSummaryMetadataAssessment",
+            );
+            this.hideCitationsChart = MetacatUI.appModel.get(
+              "hideSummaryCitationsChart",
+            );
+            this.hideDownloadsChart = MetacatUI.appModel.get(
+              "hideSummaryDownloadsChart",
+            );
+            this.hideViewsChart = MetacatUI.appModel.get(
+              "hideSummaryViewsChart",
+            );
+          }
+        }
 
-		/*
-		* Flag to indicate whether the statsview is a node summary view
-		* @type {boolean}
-		*/
-		nodeSummaryView: false,
+        if (
+          !this.hideCitationsChart ||
+          !this.hideDownloadsChart ||
+          !this.hideViewsChart
+        ) {
+          if (typeof this.metricsModel === "undefined") {
+            // Create a list with the repository ID
+            var pid_list = new Array();
+            pid_list.push(nodeId);
+
+            // Create a new object of the metrics model
+            var metricsModel = new MetricsModel({
+              pid_list: pid_list,
+              type: this.userType,
+            });
+            metricsModel.fetch();
+            this.metricsModel = metricsModel;
+          }
+        }
 
-		/**
-		 * Whether or not to show the graph that indicated the assessment score for all metadata in the query.
-		 * @type {boolean}
-		 */
-		hideMetadataAssessment: false,
+        if (!this.model) {
+          this.model = new StatsModel({
+            hideMetadataAssessment: this.hideMetadataAssessment,
+            mdqImageId: nodeId,
+          });
+        }
 
-    subviews: [],
+        //Clear the page
+        this.$el.html("");
+
+        //Only trigger the functions that draw SVG charts if d3 loaded correctly
+        if (d3) {
+          //Draw a chart that shows the temporal coverage of all datasets in this collection
+          this.listenTo(
+            this.model,
+            "change:temporalCoverage",
+            this.drawCoverageChart,
+          );
+
+          //Draw charts that plot the latest updates of metadata and data files
+          this.listenTo(
+            this.model,
+            "change:dataUpdateDates",
+            this.drawDataUpdatesChart,
+          );
+          this.listenTo(
+            this.model,
+            "change:metadataUpdateDates",
+            this.drawMetadataUpdatesChart,
+          );
+
+          //Render the total file size of all contents in this collection
+          this.listenTo(this.model, "change:totalSize", this.displayTotalSize);
+
+          //Render the total number of datasets in this collection
+          this.listenTo(
+            this.model,
+            "change:metadataCount",
+            this.displayTotalCount,
+          );
+
+          // Display replicas only for member nodes
+          if (this.userType === "repository" && !this.userIsCN)
+            this.listenTo(
+              this.model,
+              "change:totalReplicas",
+              this.displayTotalReplicas,
+            );
+
+          //Draw charts that show the breakdown of format IDs for metadata and data files
+          this.listenTo(
+            this.model,
+            "change:dataFormatIDs",
+            this.drawDataCountChart,
+          );
+          this.listenTo(
+            this.model,
+            "change:metadataFormatIDs",
+            this.drawMetadataCountChart,
+          );
+        }
 
-		template: _.template(profileTemplate),
+        //When the last coverage endDate is found, draw a title for the temporal coverage chart
+        this.listenTo(
+          this.model,
+          "change:lastEndDate",
+          this.drawCoverageChartTitle,
+        );
 
-		metricTemplate: _.template(MetricModalTemplate),
+        //When the total count is updated, check if there if the count is 0, so we can show there is no "activity" for this collection
+        this.listenTo(this.model, "change:totalCount", this.showNoActivity);
 
-		alertTemplate: _.template(AlertTemplate),
+        // set the header type
+        MetacatUI.appModel.set("headerType", "default");
 
-		loadingTemplate: _.template(LoadingTemplate),
+        // Loading template for the FAIR chart
+        var fairLoadingHtml = this.metricsLoadingTemplate({
+          message: "Running an assessment report...",
+          character: "none",
+          type: "FAIR",
+        });
 
-		metricsLoadingTemplate: _.template(MetricsLoadingTemplate),
+        // Loading template for the citations section
+        var citationsLoadingHtml = this.metricsLoadingTemplate({
+          message:
+            "Scouring our records for publications that cited these datasets...",
+          character: "none",
+          type: "citations",
+        });
 
-		initialize: function(options){
-			if(!options) options = {};
+        // Loading template for the downloads bar chart
+        var downloadsLoadingHtml = this.metricsLoadingTemplate({
+          message: "Crunching some numbers...",
+          character: "developer",
+          type: "barchart",
+        });
 
-			this.title = (typeof options.title === "undefined") ? "Summary of Holdings" : options.title;
-			this.description = (typeof options.description === "undefined") ?
-					"A summary of all datasets in our catalog." : options.description;
-			this.metricsModel = (typeof options.metricsModel === undefined) ? undefined : options.metricsModel;
+        // Loading template for the views bar chart
+        var viewsLoadingHtml = this.metricsLoadingTemplate({
+          message: "Just doing a few more calculations...",
+          character: "statistician",
+          type: "barchart",
+        });
 
-			this.userType = (typeof options.userType === undefined) ? undefined : options.userType;
-			this.userId = (typeof options.userId === undefined) ? undefined : options.userId;
-			this.userLabel = (typeof options.userLabel === undefined) ? undefined : options.userLabel;
+        //Insert the template
+        this.$el.html(
+          this.template({
+            query: this.model.get("query"),
+            title: this.title,
+            description: this.description,
+            userType: this.userType,
+            userIsCN: this.userIsCN,
+            fairLoadingHtml: fairLoadingHtml,
+            citationsLoadingHtml: citationsLoadingHtml,
+            downloadsLoadingHtml: downloadsLoadingHtml,
+            viewsLoadingHtml: viewsLoadingHtml,
+            hideUpdatesChart: this.hideUpdatesChart,
+            hideCitationsChart: this.hideCitationsChart,
+            hideDownloadsChart: this.hideDownloadsChart,
+            hideViewsChart: this.hideViewsChart,
+            hideMetadataAssessment: this.hideMetadataAssessment,
+          }),
+        );
+
+        // Insert the metadata assessment chart
+        var view = this;
+        if (this.hideMetadataAssessment !== true) {
+          this.listenTo(
+            this.model,
+            "change:mdqScoresImage",
+            this.drawMetadataAssessment,
+          );
+          this.listenTo(this.model, "change:mdqScoresError", function () {
+            view.renderMetadataAssessmentError();
+          });
+        }
 
-			if(typeof options.el === "undefined")
-				this.el = options.el;
+        //Insert the loading template into the space where the charts will go
+        if (d3) {
+          this.$(".chart").html(this.loadingTemplate);
+          this.$(".show-loading").html(this.loadingTemplate);
+        }
+        //If SVG isn't supported, insert an info warning
+        else {
+          this.$el.prepend(
+            this.alertTemplate({
+              classes: "alert-info",
+              msg: "Please upgrade your browser or use a different browser to view graphs of these statistics.",
+              email: false,
+            }),
+          );
+        }
+
+        this.$el.data("view", this);
+
+        if (this.userType == "portal" || this.userType === "repository") {
+          if (
+            !this.hideCitationsChart ||
+            !this.hideDownloadsChart ||
+            !this.hideViewsChart
+          ) {
+            if (this.metricsModel.get("totalViews") !== null) {
+              this.renderMetrics();
+            } else {
+              // render metrics on fetch success.
+              this.listenTo(view.metricsModel, "sync", this.renderMetrics);
+
+              // in case when there is an error for the fetch call.
+              this.listenTo(
+                view.metricsModel,
+                "error",
+                this.renderUsageMetricsError,
+              );
+
+              var view = this;
+              setTimeout(function () {
+                if (
+                  view
+                    .$(".views-metrics, .downloads-metrics, #user-citations")
+                    .find(".metric-chart-loading").length
+                ) {
+                  view.renderUsageMetricsError();
+                  view.stopListening(
+                    view.metricsModel,
+                    "error",
+                    view.renderUsageMetricsError,
+                  );
+                }
+              }, 6000);
+            }
+          }
+        }
+
+        //Start retrieving data from Solr
+        this.model.getAll();
+
+        // Only gather replication stats if the view is a repository view
+        if (this.userType === "repository") {
+          if (this.userLabel !== undefined) {
+            var identifier = MetacatUI.appSearchModel.escapeSpecialChar(
+              encodeURIComponent(this.userId),
+            );
+            this.model.getTotalReplicas(identifier);
+          } else if (this.nodeSummaryView) {
+            var nodeId = MetacatUI.appModel.get("nodeId");
+            var identifier = MetacatUI.appSearchModel.escapeSpecialChar(
+              encodeURIComponent(nodeId),
+            );
+            this.model.getTotalReplicas(identifier);
+          }
+        }
 
-			this.hideUpdatesChart = (options.hideUpdatesChart === true)? true : false;
-			this.hideMetadataAssessment = (typeof options.hideMetadataAssessment === "undefined") ? true : options.hideMetadataAssessment;
-			this.hideCitationsChart = (typeof options.hideCitationsChart === "undefined") ? true : options.hideCitationsChart;
-			this.hideDownloadsChart = (typeof options.hideDownloadsChart === "undefined") ? true : options.hideDownloadsChart;
-			this.hideViewsChart = (typeof options.hideViewsChart === "undefined") ? true : options.hideViewsChart;
+        return this;
+      },
+
+      /**
+       * drawMetadataAssessment - Insert the metadata assessment image into the view
+       */
+      drawMetadataAssessment: function () {
+        try {
+          var scoresImage = this.model.get("mdqScoresImage");
+          if (scoresImage) {
+            // Replace the preloader figure with the assessment chart
+            this.$("#metadata-assessment-graphic").html(scoresImage);
+          }
+          // If there was no image received from the MDQ scores service,
+          // then show a warning message
+          else {
+            this.renderMetadataAssessmentError();
+          }
+        } catch (e) {
+          // If there's an error inserting the image, log an error message
+          console.log(
+            "Error displaying the metadata assessment figure. Error message: " +
+              e,
+          );
+          this.renderMetadataAssessmentError();
+        }
+      },
+
+      renderMetrics: function () {
+        if (!this.hideCitationsChart) this.renderCitationMetric();
+
+        if (!this.hideDownloadsChart) this.renderDownloadMetric();
+
+        if (!this.hideViewsChart) this.renderViewMetric();
+      },
+
+      renderCitationMetric: function () {
+        var citationSectionEl = this.$("#user-citations");
+        var citationEl = this.$(".citations-metrics-list");
+        var citationCountEl = this.$(".citation-count");
+        var metricName = "Citations";
+        var metricCount = this.metricsModel.get("totalCitations");
+        citationCountEl.text(
+          MetacatUI.appView.numberAbbreviator(metricCount, 1),
+        );
+
+        // Displaying Citations
+        var resultDetails = this.metricsModel.get("resultDetails");
+
+        // Creating a new collection object
+        // Parsing result-details with citation dictionary format
+        var resultDetailsCitationCollection = new Array();
+        for (var key in resultDetails["citations"]) {
+          resultDetailsCitationCollection.push(resultDetails["citations"][key]);
+        }
 
+        var citationCollection = new Citations(
+          resultDetailsCitationCollection,
+          { parse: true },
+        );
 
-			this.model = options.model || null;
-		},
+        this.citationCollection = citationCollection;
 
-		render: function (options) {
+        // Checking if there are any citations available for the List display.
+        if (this.metricsModel.get("totalCitations") == 0) {
+          var citationList = new CitationList();
 
-      //The Node info needs to be fetched first since a lot of this code requires info about MNs
-      if( !MetacatUI.nodeModel.get("checked") && !MetacatUI.nodeModel.get("error") ){
-        this.listenToOnce( MetacatUI.nodeModel, "change:checked error", function(){
-          //Remove listeners and render the view, even if there was an error fetching the NodeModel
-          this.stopListening(MetacatUI.nodeModel);
-          this.render(options);
+          // reattaching the citations at the bottom when the counts are 0.
+          var detachCitationEl = this.$(citationSectionEl).detach();
+          this.$(".charts-container").append(detachCitationEl);
+        } else {
+          var citationList = new CitationList({
+            citations: this.citationCollection,
+          });
+        }
+
+        this.citationList = citationList;
+
+        citationEl.html(this.citationList.render().$el.html());
+      },
+
+      renderDownloadMetric: function () {
+        var downloadEl = this.$(".downloads-metrics > .metric-chart");
+        var metricName = "Downloads";
+        var metricCount = this.metricsModel.get("totalDownloads");
+        var downloadCountEl = this.$(".download-count");
+        downloadCountEl.text(
+          MetacatUI.appView.numberAbbreviator(metricCount, 1),
+        );
+
+        var metricChartView = this.createMetricsChart(metricName);
+
+        downloadEl.html(metricChartView.el);
+
+        metricChartView.render();
+      },
+
+      renderViewMetric: function () {
+        var viewEl = this.$(".views-metrics > .metric-chart");
+        var metricName = "Views";
+        var metricCount = this.metricsModel.get("totalViews");
+        var viewCountEl = this.$(".view-count");
+        viewCountEl.text(MetacatUI.appView.numberAbbreviator(metricCount, 1));
+
+        var metricChartView = this.createMetricsChart(metricName);
+
+        viewEl.html(metricChartView.el);
+
+        metricChartView.render();
+      },
+
+      // Currently only being used for portals and profile views
+      createMetricsChart: function (metricName) {
+        var metricNameLemma = metricName.toLowerCase();
+        var metricMonths = this.metricsModel.get("months");
+        var metricCount = this.metricsModel.get(metricNameLemma);
+        var chartEl = document.getElementById(
+          "user-" + metricNameLemma + "-chart",
+        );
+        var viewType = this.userType;
+
+        // Draw a metric chart
+        var modalMetricChart = new MetricsChart({
+          id: metricNameLemma + "-chart",
+          metricCount: metricCount,
+          metricMonths: metricMonths,
+          type: viewType,
+          metricName: metricName,
         });
 
-        this.$el.html(this.loadingTemplate);
+        this.subviews.push(modalMetricChart);
+
+        return modalMetricChart;
+      },
+
+      drawDataCountChart: function () {
+        var dataCount = this.model.get("dataCount");
+        var data = this.model.get("dataFormatIDs");
+
+        if (dataCount) {
+          var svgClass = "data";
+        } else if (
+          !this.model.get("dataCount") &&
+          this.model.get("metadataCount")
+        ) {
+          //Are we drawing a blank chart (i.e. 0 data objects found)?
+          var svgClass = "data default";
+        } else if (
+          !this.model.get("metadataCount") &&
+          !this.model.get("dataCount")
+        )
+          var svgClass = "data no-activity";
+
+        //If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
+        if (!d3) {
+          this.$(".format-charts-data").html(
+            "<h2 class='" +
+              svgClass +
+              " fallback'>" +
+              MetacatUI.appView.commaSeparateNumber(dataCount) +
+              " data files</h2>",
+          );
+
+          return;
+        }
 
-        return;
-      }
+        //Draw a donut chart
+        var donut = new DonutChart({
+          id: "data-chart",
+          data: data,
+          total: this.model.get("dataCount"),
+          titleText: "data files",
+          titleCount: dataCount,
+          svgClass: svgClass,
+          countClass: "data",
+          height: 300,
+          width: 380,
+          formatLabel: function (name) {
+            //If this is the application/vnd.ms-excel formatID - let's just display "MS Excel"
+            if (name !== undefined && name.indexOf("ms-excel") > -1)
+              name = "MS Excel";
+            else if (
+              name != undefined &&
+              name ==
+                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+            )
+              name = "MS Excel OpenXML";
+            else if (
+              name != undefined &&
+              name ==
+                "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+            )
+              name = "MS Word OpenXML";
+            //Application/octet-stream - shorten it
+            else if (name !== undefined && name == "application/octet-stream")
+              name = "Application file";
+
+            if (name === undefined) name = "";
+
+            return name;
+          },
+        });
+        this.$(".format-charts-data").html(donut.render().el);
+      },
+
+      drawMetadataCountChart: function () {
+        var metadataCount = this.model.get("metadataCount");
+        var data = this.model.get("metadataFormatIDs");
+
+        if (metadataCount) {
+          var svgClass = "metadata";
+        } else if (
+          !this.model.get("metadataCount") &&
+          this.model.get("dataCount")
+        ) {
+          //Are we drawing a blank chart (i.e. 0 data objects found)?
+          var svgClass = "metadata default";
+        } else if (
+          !this.model.get("metadataCount") &&
+          !this.model.get("dataCount")
+        )
+          var svgClass = "metadata no-activity";
+
+        //If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
+        if (!d3) {
+          this.$(".format-charts-metadata").html(
+            "<h2 class='" +
+              svgClass +
+              " fallback'>" +
+              MetacatUI.appView.commaSeparateNumber(metadataCount) +
+              " metadata files</h2>",
+          );
+
+          return;
+        }
 
-			if ( !options )
-				options = {};
+        //Draw a donut chart
+        var donut = new DonutChart({
+          id: "metadata-chart",
+          data: data,
+          total: this.model.get("metadataCount"),
+          titleText: "metadata files",
+          titleCount: metadataCount,
+          svgClass: svgClass,
+          countClass: "metadata",
+          height: 300,
+          width: 380,
+          formatLabel: function (name) {
+            if (
+              name !== undefined &&
+              (name.indexOf("//ecoinformatics.org") > -1 ||
+                name.indexOf("//eml.ecoinformatics.org") > -1)
+            ) {
+              //EML - extract the version only
+              if (
+                name.substring(0, 4) == "eml:" ||
+                name.substring(0, 6) == "https:"
+              )
+                name = name
+                  .substr(name.lastIndexOf("/") + 1)
+                  .toUpperCase()
+                  .replace("-", " ");
+
+              //EML modules
+              if (
+                name.indexOf("-//ecoinformatics.org//eml-") > -1 ||
+                name.indexOf("-//eml.ecoinformatics.org//eml-") > -1
+              )
+                name =
+                  "EML " +
+                  name.substring(
+                    name.indexOf("//eml-") + 6,
+                    name.lastIndexOf("-"),
+                  ) +
+                  " " +
+                  name.substr(name.lastIndexOf("-") + 1, 5);
+            }
+            //Dryad - shorten it
+            else if (
+              name !== undefined &&
+              name == "http://datadryad.org/profile/v3.1"
+            )
+              name = "Dryad 3.1";
+            //FGDC - just display "FGDC {year}"
+            else if (name !== undefined && name.indexOf("FGDC") > -1)
+              name = "FGDC " + name.substring(name.length - 4);
+            //Onedcx v1.0
+            else if (
+              name !== undefined &&
+              name == "http://ns.dataone.org/metadata/schema/onedcx/v1.0"
+            )
+              name = "Onedcx v1.0";
+            //GMD-NOAA
+            else if (
+              name !== undefined &&
+              name == "http://www.isotc211.org/2005/gmd-noaa"
+            )
+              name = "GMD-NOAA";
+            //GMD-PANGAEA
+            else if (
+              name !== undefined &&
+              name == "http://www.isotc211.org/2005/gmd-pangaea"
+            )
+              name = "GMD-PANGAEA";
+
+            if (name === undefined) name = "";
+            return name;
+          },
+        });
 
-			var view = this,
-          userIsCN = false,
-          nodeId,
-          isHostedRepo = false;
+        this.$(".format-charts-metadata").html(donut.render().el);
+      },
+
+      //drawUploadChart will get the upload stats from the stats model and draw a time series cumulative chart
+      drawUploadChart: function () {
+        //Get the width of the chart by using the parent container width
+        var parentEl = this.$(".upload-chart");
+        var width = parentEl.width() || null;
+
+        //If there was no first upload, draw a blank chart and exit
+        if (
+          (!this.model.get("metadataUploads") ||
+            !this.model.get("metadataUploads").length) &&
+          (!this.model.get("dataUploads") ||
+            !this.model.get("dataUploads").length)
+        ) {
+          var lineChartView = new LineChart({
+            id: "upload-chart",
+            yLabel: "files uploaded",
+            frequency: 0,
+            width: width,
+          });
+
+          this.$(".upload-chart").html(lineChartView.render().el);
+
+          return;
+        }
 
-			// Check if the node is a coordinating node
-			this.userIsCN = userIsCN;
-			if( this.userType !== undefined && this.userLabel !== undefined) {
-				if (this.userType === "repository") {
-					userIsCN = MetacatUI.nodeModel.isCN(this.userId);
-					if (userIsCN && typeof isCN !== 'undefined')
-						this.userIsCN = true;
-				}
-			}
-
-			if ( options.nodeSummaryView ) {
-				this.nodeSummaryView = true;
-				nodeId = MetacatUI.appModel.get("nodeId");
-				userIsCN = MetacatUI.nodeModel.isCN(nodeId);
-
-        //Save whether this profile is for a CN
-				if (userIsCN && typeof userIsCN !== 'undefined'){
-					this.userIsCN = true;
+        //Set the frequency of our points
+        var frequency = 12;
+
+        //Check which line we should draw first since the scale will be based off the first line
+        if (this.model.get("metadataUploads") > this.model.get("dataUploads")) {
+          //If there isn't a lot of point to graph, draw points more frequently on the line
+          if (this.model.get("metadataUploadDates").length < 40) frequency = 1;
+
+          //Create the line chart and draw the metadata line
+          var lineChartView = new LineChart({
+            data: this.model.get("metadataUploadDates"),
+            formatFromSolrFacets: true,
+            cumulative: true,
+            id: "upload-chart",
+            className: "metadata",
+            yLabel: "files uploaded",
+            labelValue: "Metadata: ",
+            width: width,
+            labelDate: "M-y",
+          });
+
+          this.$(".upload-chart").html(lineChartView.render().el);
+
+          //Only draw the data file line if there was at least one uploaded
+          if (this.model.get("dataUploads")) {
+            //Add a line to our chart for data uploads
+            lineChartView.className = "data";
+            lineChartView.labelValue = "Data: ";
+            lineChartView.addLine(this.model.get("dataUploadDates"));
+          }
+        } else {
+          var lineChartView = new LineChart({
+            data: this.model.get("dataUploadDates"),
+            formatFromSolrFacets: true,
+            cumulative: true,
+            id: "upload-chart",
+            className: "data",
+            yLabel: "files uploaded",
+            labelValue: "Data: ",
+            width: width,
+            labelDate: "M-y",
+          });
+
+          this.$(".upload-chart").html(lineChartView.render().el);
+
+          //If no metadata files were uploaded, we don't want to draw the data file line
+          if (this.model.get("metadataUploads")) {
+            //Add a line to our chart for metadata uploads
+            lineChartView.className = "metadata";
+            lineChartView.labelValue = "Metadata: ";
+            lineChartView.addLine(this.model.get("metadataUploadDates"));
+          }
         }
-        //Figure out if this profile is for a hosted repo
-        else if( nodeId ){
-          isHostedRepo = _.contains(MetacatUI.appModel.get("dataoneHostedRepos"), nodeId);
+      },
+
+      //drawUploadTitle will draw a circle badge title for the uploads time series chart
+      drawUploadTitle: function () {
+        //If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
+        if (!d3) {
+          this.$("#uploads-title").html(
+            "<h2 class='packages fallback'>" +
+              MetacatUI.appView.commaSeparateNumber(
+                this.model.get("totalCount"),
+              ) +
+              "</h2>",
+          );
+
+          return;
         }
 
-				// Disable the metrics if the nodeId is not available or if it is not a DataONE Hosted Repo
-				if (!this.userIsCN && (nodeId === "undefined" || nodeId === null || !isHostedRepo) ) {
-					this.hideCitationsChart = true;
-					this.hideDownloadsChart = true;
-					this.hideViewsChart = true;
-          this.hideMetadataAssessment = true;
-				}
-        else{
-          // Overwrite the metrics display flags as set in the AppModel
-          this.hideMetadataAssessment = MetacatUI.appModel.get("hideSummaryMetadataAssessment");
-          this.hideCitationsChart = MetacatUI.appModel.get("hideSummaryCitationsChart");
-          this.hideDownloadsChart = MetacatUI.appModel.get("hideSummaryDownloadsChart");
-          this.hideViewsChart = MetacatUI.appModel.get("hideSummaryViewsChart");
+        if (
+          !this.model.get("dataUploads") &&
+          !this.model.get("metadataUploads")
+        ) {
+          //Draw the upload chart title
+          var uploadChartTitle = new CircleBadge({
+            id: "upload-chart-title",
+            className: "no-activity",
+            globalR: 60,
+            data: [{ count: 0, label: "uploads" }],
+          });
+
+          this.$("#uploads-title").prepend(uploadChartTitle.render().el);
+
+          return;
         }
-			}
-
-			if ( !this.hideCitationsChart || !this.hideDownloadsChart || !this.hideViewsChart ) {
-
-				if ( typeof this.metricsModel === "undefined" ) {
-					// Create a list with the repository ID
-					var pid_list = new Array();
-					pid_list.push(nodeId);
-
-					// Create a new object of the metrics model
-					var metricsModel = new MetricsModel({
-						pid_list: pid_list,
-						type: this.userType
-					});
-					metricsModel.fetch();
-					this.metricsModel = metricsModel;
-				}
-
-			}
-
-			if( !this.model ){
-				this.model = new StatsModel({
-          hideMetadataAssessment: this.hideMetadataAssessment,
-          mdqImageId: nodeId
+
+        //Get information for our upload chart title
+        var titleChartData = [],
+          metadataUploads = this.model.get("metadataUploads"),
+          dataUploads = this.model.get("dataUploads"),
+          metadataClass = "metadata",
+          dataClass = "data";
+
+        if (metadataUploads == 0) metadataClass = "default";
+        if (dataUploads == 0) dataClass = "default";
+
+        var titleChartData = [
+          {
+            count: this.model.get("metadataUploads"),
+            label: "metadata",
+            className: metadataClass,
+          },
+          {
+            count: this.model.get("dataUploads"),
+            label: "data",
+            className: dataClass,
+          },
+        ];
+
+        //Draw the upload chart title
+        var uploadChartTitle = new CircleBadge({
+          id: "upload-chart-title",
+          data: titleChartData,
+          className: "chart-title",
+          useGlobalR: true,
+          globalR: 60,
         });
-			}
-
-			//Clear the page
-			this.$el.html("");
-
-			//Only trigger the functions that draw SVG charts if d3 loaded correctly
-			if(d3){
-        //Draw a chart that shows the temporal coverage of all datasets in this collection
-				this.listenTo(this.model, 'change:temporalCoverage', this.drawCoverageChart);
-
-        //Draw charts that plot the latest updates of metadata and data files
-				this.listenTo(this.model, "change:dataUpdateDates",     this.drawDataUpdatesChart);
-        this.listenTo(this.model, "change:metadataUpdateDates", this.drawMetadataUpdatesChart);
-
-        //Render the total file size of all contents in this collection
-				this.listenTo(this.model, "change:totalSize", this.displayTotalSize);
-
-        //Render the total number of datasets in this collection
-				this.listenTo(this.model, 'change:metadataCount', this.displayTotalCount);
-
-        // Display replicas only for member nodes
-				if (this.userType === "repository" && !this.userIsCN)
-					this.listenTo(this.model, "change:totalReplicas", this.displayTotalReplicas);
-
-        //Draw charts that show the breakdown of format IDs for metadata and data files
-				this.listenTo(this.model, 'change:dataFormatIDs',     this.drawDataCountChart);
-				this.listenTo(this.model, 'change:metadataFormatIDs', this.drawMetadataCountChart);
-			}
-
-      //When the last coverage endDate is found, draw a title for the temporal coverage chart
-			this.listenTo(this.model, 'change:lastEndDate', this.drawCoverageChartTitle);
-
-      //When the total count is updated, check if there if the count is 0, so we can show there is no "activity" for this collection
-			this.listenTo(this.model, "change:totalCount", this.showNoActivity);
-
-			// set the header type
-			MetacatUI.appModel.set('headerType', 'default');
-
-			// Loading template for the FAIR chart
-			var fairLoadingHtml = this.metricsLoadingTemplate({
-				message: "Running an assessment report...",
-				character: "none",
-				type: "FAIR"
-			});
-
-			// Loading template for the citations section
-			var citationsLoadingHtml = this.metricsLoadingTemplate({
-				message: "Scouring our records for publications that cited these datasets...",
-				character: "none",
-				type: "citations"
-			});
-
-			// Loading template for the downloads bar chart
-			var downloadsLoadingHtml = this.metricsLoadingTemplate({
-				message: "Crunching some numbers...",
-				character: "developer",
-				type: "barchart"
-			});
-
-			// Loading template for the views bar chart
-			var viewsLoadingHtml = this.metricsLoadingTemplate({
-				message: "Just doing a few more calculations...",
-				character: "statistician",
-				type: "barchart"
-			});
-
-			//Insert the template
-			this.$el.html(this.template({
-				query: this.model.get('query'),
-				title: this.title,
-				description: this.description,
-				userType: this.userType,
-				userIsCN: this.userIsCN,
-				fairLoadingHtml: fairLoadingHtml,
-				citationsLoadingHtml: citationsLoadingHtml,
-				downloadsLoadingHtml: downloadsLoadingHtml,
-				viewsLoadingHtml: viewsLoadingHtml,
-				hideUpdatesChart: this.hideUpdatesChart,
-				hideCitationsChart: this.hideCitationsChart,
-				hideDownloadsChart: this.hideDownloadsChart,
-				hideViewsChart: this.hideViewsChart,
-				hideMetadataAssessment: this.hideMetadataAssessment
-			}));
-
-		// Insert the metadata assessment chart
-		var view = this;
-		if( this.hideMetadataAssessment !== true ){
-			this.listenTo(this.model, "change:mdqScoresImage", this.drawMetadataAssessment);
-			this.listenTo(this.model, "change:mdqScoresError", function () {
-					view.renderMetadataAssessmentError();
-				});
-		}
-
-		//Insert the loading template into the space where the charts will go
-		if(d3){
-			this.$(".chart").html(this.loadingTemplate);
-			this.$(".show-loading").html(this.loadingTemplate);
-		}
-		//If SVG isn't supported, insert an info warning
-		else{
-			this.$el.prepend(this.alertTemplate({
-				classes: "alert-info",
-				msg: "Please upgrade your browser or use a different browser to view graphs of these statistics.",
-				email: false
-			}));
-		}
-
-		this.$el.data("view", this);
-
-			if (this.userType == "portal" || this.userType === "repository") {
-				if ( !this.hideCitationsChart || !this.hideDownloadsChart || !this.hideViewsChart ) {
-					if (this.metricsModel.get("totalViews") !== null) {
-						this.renderMetrics();
-					}
-					else{
-						// render metrics on fetch success.
-						this.listenTo(view.metricsModel, "sync" , this.renderMetrics);
-
-						// in case when there is an error for the fetch call.
-						this.listenTo(view.metricsModel, "error", this.renderUsageMetricsError);
-
-            var view = this;
-            setTimeout(function(){
-              if( view.$('.views-metrics, .downloads-metrics, #user-citations').find(".metric-chart-loading").length ){
-                view.renderUsageMetricsError();
-                view.stopListening(view.metricsModel, "error", view.renderUsageMetricsError);
-              }
-            }, 6000);
-					}
-				}
-			}
-
-		//Start retrieving data from Solr
-		this.model.getAll();
-
-		// Only gather replication stats if the view is a repository view
-		if (this.userType === "repository") {
-			if (this.userLabel !== undefined)
-			{
-				var identifier = MetacatUI.appSearchModel.escapeSpecialChar(encodeURIComponent(this.userId));
-				this.model.getTotalReplicas(identifier);
-			}
-			else if (this.nodeSummaryView) {
-				var nodeId = MetacatUI.appModel.get("nodeId");
-				var identifier = MetacatUI.appSearchModel.escapeSpecialChar(encodeURIComponent(nodeId));
-				this.model.getTotalReplicas(identifier);
-			}
-
-		}
-
-		return this;
-	},
-
-    /**
-     * drawMetadataAssessment - Insert the metadata assessment image into the view
-     */
-    drawMetadataAssessment: function(){
-      try {
-        var scoresImage = this.model.get("mdqScoresImage");
-        if( scoresImage ){
-          // Replace the preloader figure with the assessment chart
-        	this.$("#metadata-assessment-graphic").html(scoresImage);
+        this.$("#uploads-title").prepend(uploadChartTitle.render().el);
+      },
+
+      /*
+       * displayTotalCount - renders a simple count of total metadata files/datasets
+       */
+      displayTotalCount: function () {
+        var className = "quick-stats-count";
+
+        if (!this.model.get("metadataCount") && !this.model.get("dataCount"))
+          className += " no-activity";
+
+        var countEl = $(document.createElement("p"))
+          .addClass(className)
+          .text(
+            MetacatUI.appView.commaSeparateNumber(
+              this.model.get("metadataCount"),
+            ),
+          );
+
+        var titleEl = $(document.createElement("p"))
+          .addClass("chart-title")
+          .text("datasets");
+
+        this.$("#total-datasets").html(countEl);
+        this.$("#total-datasets").append(titleEl);
+      },
+
+      /*
+       * displayTotalSize renders a count of the total file size of
+       * all current metadata and data files
+       */
+      displayTotalSize: function () {
+        var className = "quick-stats-count";
+        var count = "";
+
+        if (!this.model.get("totalSize")) {
+          count = "0 bytes";
+          className += " no-activity";
+        } else {
+          count = this.bytesToSize(this.model.get("totalSize"));
         }
-        // If there was no image received from the MDQ scores service,
-				// then show a warning message
-        else {
-					this.renderMetadataAssessmentError();
+
+        var countEl = $(document.createElement("p"))
+          .addClass(className)
+          .text(count);
+
+        var titleEl = $(document.createElement("p"))
+          .addClass("chart-title")
+          .text("of content");
+
+        this.$("#total-size").html(countEl);
+        this.$("#total-size").append(titleEl);
+      },
+
+      /**
+       * Draws both the metadata and data update date charts.
+       * Note that this function may be deprecated in the future.
+       *  Views should directly call drawMetadataUpdatesChart() or drawDataUpdatesChart() directly,
+       *  since metadata and data dates are fetched via separate AJAX calls.
+       */
+      drawUpdatesChart: function () {
+        //Draw the metadata and data updates charts
+        this.drawMetadataUpdatesChart();
+        this.drawDataUpdatesChart();
+      },
+
+      /**
+       * Draws a line chart representing the latest metadata updates over time
+       */
+      drawMetadataUpdatesChart: function () {
+        //Set some configurations for the LineChart
+        var chartClasses = "data",
+          data;
+
+        //If the number of metadata objects in this data collection is 0, then set the data for the LineChart to null.
+        // And add a "no-activity" class to the chart.
+        if (
+          !this.model.get("metadataUpdateDates") ||
+          !this.model.get("metadataUpdateDates").length
+        ) {
+          data = null;
+          chartClasses += " no-activity";
+        } else {
+          //Use the metadata update dates for the LineChart
+          data = this.model.get("metadataUpdateDates");
         }
-      } catch (e) {
-        // If there's an error inserting the image, log an error message
-        console.log("Error displaying the metadata assessment figure. Error message: " + e);
-				this.renderMetadataAssessmentError();
-      }
-    },
 
-		renderMetrics: function(){
-			if(!this.hideCitationsChart)
-				this.renderCitationMetric();
+        //Create the line chart for metadata updates
+        var metadataLineChart = new LineChart({
+          data: data,
+          formatFromSolrFacets: true,
+          cumulative: false,
+          id: "updates-chart",
+          className: chartClasses,
+          yLabel: "metadata files updated",
+          width: this.$(".metadata-updates-chart").width(),
+          labelDate: "M-y",
+        });
 
-			if(!this.hideDownloadsChart)
-				this.renderDownloadMetric();
+        //Render the LineChart and insert it into the container element
+        this.$(".metadata-updates-chart").html(metadataLineChart.render().el);
+      },
 
-			if(!this.hideViewsChart)
-				this.renderViewMetric();
+      /**
+       * Draws a line chart representing the latest metadata updates over time
+       */
+      drawDataUpdatesChart: function () {
+        //Set some configurations for the LineChart
+        var chartClasses = "data",
+          view = this,
+          data;
 
-		},
+        //Use the data update dates for the LineChart
+        if (this.model.get("dataCount")) {
+          data = this.model.get("dataUpdateDates");
+        } else {
+          //If the number of data objects in this data collection is 0, then set the data for the LineChart to null.
+          // And add a "no-activity" class to the chart.
+          data = null;
+          chartClasses += " no-activity";
+        }
 
-		renderCitationMetric: function() {
-			var citationSectionEl = this.$('#user-citations');
-			var citationEl = this.$('.citations-metrics-list');
-			var citationCountEl = this.$('.citation-count');
-			var metricName = "Citations";
-			var metricCount = this.metricsModel.get("totalCitations");
-			citationCountEl.text(MetacatUI.appView.numberAbbreviator(metricCount,1));
+        //Create the line chart for data updates
+        var dataLineChart = new LineChart({
+          data: data,
+          formatFromSolrFacets: true,
+          cumulative: false,
+          id: "updates-chart",
+          className: chartClasses,
+          yLabel: "data files updated",
+          width: this.$(".data-updates-chart").width(),
+          labelDate: "M-y",
+        });
 
-			// Displaying Citations
-			var resultDetails = this.metricsModel.get("resultDetails");
+        //Render the LineChart and insert it into the container element
+        this.$(".data-updates-chart").html(dataLineChart.render().el);
 
-			// Creating a new collection object
-			// Parsing result-details with citation dictionary format
-			var resultDetailsCitationCollection = new Array();
-			for (var key in resultDetails["citations"]) {
-				resultDetailsCitationCollection.push(resultDetails["citations"][key]);
-			}
-
-			var citationCollection = new Citations(resultDetailsCitationCollection, {parse:true});
-
-			this.citationCollection = citationCollection;
-
-			// Checking if there are any citations available for the List display.
-			if(this.metricsModel.get("totalCitations") == 0) {
-				var citationList = new CitationList();
-
-				// reattaching the citations at the bottom when the counts are 0.
-				var detachCitationEl = this.$(citationSectionEl).detach();
-				this.$('.charts-container').append(detachCitationEl);
-			}
-			else {
-				var citationList = new CitationList({citations: this.citationCollection});
-			}
-
-			this.citationList = citationList;
-
-			citationEl.html(this.citationList.render().$el.html());
-		},
-
-		renderDownloadMetric: function() {
-			var downloadEl = this.$('.downloads-metrics > .metric-chart');
-			var metricName = "Downloads";
-			var metricCount = this.metricsModel.get("totalDownloads");
-			var downloadCountEl = this.$('.download-count');
-			downloadCountEl.text(MetacatUI.appView.numberAbbreviator(metricCount,1));
-
-      var metricChartView = this.createMetricsChart(metricName);
-
-			downloadEl.html(metricChartView.el);
-
-      metricChartView.render();
-
-		},
-
-		renderViewMetric: function() {
-			var viewEl = this.$('.views-metrics > .metric-chart');
-			var metricName = "Views";
-			var metricCount = this.metricsModel.get("totalViews");
-			var viewCountEl = this.$('.view-count');
-			viewCountEl.text(MetacatUI.appView.numberAbbreviator(metricCount,1));
-
-      var metricChartView = this.createMetricsChart(metricName);
-
-			viewEl.html(metricChartView.el);
-
-      metricChartView.render();
-		},
-
-		// Currently only being used for portals and profile views
-		createMetricsChart: function(metricName){
-			var metricNameLemma = metricName.toLowerCase()
-			var metricMonths    = this.metricsModel.get("months");
-			var metricCount     = this.metricsModel.get(metricNameLemma);
-			var chartEl         = document.getElementById('user-'+metricNameLemma+'-chart' );
-			var viewType        = this.userType;
-
-			// Draw a metric chart
-			var modalMetricChart = new MetricsChart({
-														id: metricNameLemma + "-chart",
-														metricCount: metricCount,
-														metricMonths: metricMonths,
-														type: viewType,
-														metricName: metricName,
-			});
-
-      this.subviews.push(modalMetricChart);
-
-			return modalMetricChart;
-		},
-
-		drawDataCountChart: function(){
-			var dataCount = this.model.get('dataCount');
-			var data = this.model.get('dataFormatIDs');
-
-			if(dataCount){
-				var svgClass = "data";
-			}
-			else if(!this.model.get('dataCount') && this.model.get('metadataCount')){	//Are we drawing a blank chart (i.e. 0 data objects found)?
-				var svgClass = "data default";
-			}
-			else if(!this.model.get('metadataCount') && !this.model.get('dataCount'))
-				var svgClass = "data no-activity";
-
-			//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
-			if(!d3){
-				this.$('.format-charts-data').html("<h2 class='" + svgClass + " fallback'>" + MetacatUI.appView.commaSeparateNumber(dataCount) + " data files</h2>");
-
-				return;
-			}
-
-			//Draw a donut chart
-			var donut = new DonutChart({
-							id: "data-chart",
-							data: data,
-							total: this.model.get('dataCount'),
-							titleText: "data files",
-							titleCount: dataCount,
-							svgClass: svgClass,
-							countClass: "data",
-							height: 300,
-              width: 380,
-							formatLabel: function(name){
-								//If this is the application/vnd.ms-excel formatID - let's just display "MS Excel"
-								if((name !== undefined) && (name.indexOf("ms-excel") > -1)) name = "MS Excel";
-								else if((name != undefined) && (name == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) name= "MS Excel OpenXML"
-								else if((name != undefined) && (name == "application/vnd.openxmlformats-officedocument.wordprocessingml.document")) name= "MS Word OpenXML"
-								//Application/octet-stream - shorten it
-								else if((name !== undefined) && (name == "application/octet-stream")) name = "Application file";
-
-								if(name === undefined) name = "";
-
-								return name;
-							}
-						});
-			this.$('.format-charts-data').html(donut.render().el);
-		},
-
-		drawMetadataCountChart: function(){
-			var metadataCount = this.model.get("metadataCount");
-			var data = this.model.get('metadataFormatIDs');
-
-			if(metadataCount){
-				var svgClass = "metadata";
-			}
-			else if(!this.model.get('metadataCount') && this.model.get('dataCount')){	//Are we drawing a blank chart (i.e. 0 data objects found)?
-				var svgClass = "metadata default";
-			}
-			else if(!this.model.get('metadataCount') && !this.model.get('dataCount'))
-				var svgClass = "metadata no-activity";
-
-			//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
-			if(!d3){
-				this.$('.format-charts-metadata').html("<h2 class='" + svgClass + " fallback'>" + MetacatUI.appView.commaSeparateNumber(metadataCount) + " metadata files</h2>");
-
-				return;
-			}
-
-			//Draw a donut chart
-			var donut = new DonutChart({
-							id: "metadata-chart",
-							data: data,
-							total: this.model.get('metadataCount'),
-							titleText: "metadata files",
-							titleCount: metadataCount,
-							svgClass: svgClass,
-							countClass: "metadata",
-							height: 300,
-              width: 380,
-							formatLabel: function(name){
-								if((name !== undefined) && ((name.indexOf("//ecoinformatics.org") > -1) || (name.indexOf("//eml.ecoinformatics.org") > -1))){
-									//EML - extract the version only
-									if((name.substring(0,4) == "eml:") || (name.substring(0,6) == "https:")) name = name.substr(name.lastIndexOf("/")+1).toUpperCase().replace('-', ' ');
-
-									//EML modules
-									if((name.indexOf("-//ecoinformatics.org//eml-") > -1) || (name.indexOf("-//eml.ecoinformatics.org//eml-") > -1)) name = "EML " + name.substring(name.indexOf("//eml-")+6, name.lastIndexOf("-")) + " " + name.substr(name.lastIndexOf("-")+1, 5);
-
-								}
-								//Dryad - shorten it
-								else if((name !== undefined) && (name == "http://datadryad.org/profile/v3.1")) name = "Dryad 3.1";
-								//FGDC - just display "FGDC {year}"
-								else if((name !== undefined) && (name.indexOf("FGDC") > -1)) name = "FGDC " + name.substring(name.length-4);
-
-								//Onedcx v1.0
-								else if((name !== undefined) && (name == "http://ns.dataone.org/metadata/schema/onedcx/v1.0")) name = "Onedcx v1.0";
-
-								//GMD-NOAA
-								else if((name !== undefined) && (name == "http://www.isotc211.org/2005/gmd-noaa")) name = "GMD-NOAA";
-
-								//GMD-PANGAEA
-								else if((name !== undefined) && (name == "http://www.isotc211.org/2005/gmd-pangaea")) name = "GMD-PANGAEA";
-
-								if(name === undefined) name = "";
-								return name;
-							}
-						});
-
-			this.$('.format-charts-metadata').html(donut.render().el);
-		},
-
-		//drawUploadChart will get the upload stats from the stats model and draw a time series cumulative chart
-		drawUploadChart: function(){
-			//Get the width of the chart by using the parent container width
-			var parentEl = this.$('.upload-chart');
-			var width = parentEl.width() || null;
-
-			//If there was no first upload, draw a blank chart and exit
-			if( (!this.model.get("metadataUploads") || !this.model.get("metadataUploads").length) && (!this.model.get("dataUploads") || !this.model.get("dataUploads").length) ){
-
-				var lineChartView = new LineChart(
-						{	  id: "upload-chart",
-						 	yLabel: "files uploaded",
-						 frequency: 0,
-						 	 width: width
-						});
-
-				this.$('.upload-chart').html(lineChartView.render().el);
-
-				return;
-			}
-
-			//Set the frequency of our points
-			var frequency = 12;
-
-			//Check which line we should draw first since the scale will be based off the first line
-			if(this.model.get("metadataUploads") > this.model.get("dataUploads") ){
-
-				//If there isn't a lot of point to graph, draw points more frequently on the line
-				if(this.model.get("metadataUploadDates").length < 40) frequency = 1;
-
-				//Create the line chart and draw the metadata line
-				var lineChartView = new LineChart(
-						{	  data: this.model.get('metadataUploadDates'),
-			  		formatFromSolrFacets: true,
-						cumulative: true,
-								id: "upload-chart",
-						 className: "metadata",
-						 	yLabel: "files uploaded",
-						labelValue: "Metadata: ",
-							width: width,
-						    labelDate: "M-y"
-						});
-
-				this.$('.upload-chart').html(lineChartView.render().el);
-
-				//Only draw the data file line if there was at least one uploaded
-				if(this.model.get("dataUploads")){
-					//Add a line to our chart for data uploads
-					lineChartView.className = "data";
-					lineChartView.labelValue ="Data: ";
-					lineChartView.addLine(this.model.get('dataUploadDates'));
-				}
-			}
-			else{
-					var lineChartView = new LineChart(
-							{	  data: this.model.get('dataUploadDates'),
-				  formatFromSolrFacets: true,
-							cumulative: true,
-									id: "upload-chart",
-							 className: "data",
-							 	yLabel: "files uploaded",
-							labelValue: "Data: ",
-								 width: width,
-						    labelDate: "M-y"
-							 });
-
-					this.$('.upload-chart').html(lineChartView.render().el);
-
-					//If no metadata files were uploaded, we don't want to draw the data file line
-					if(this.model.get("metadataUploads")){
-						//Add a line to our chart for metadata uploads
-						lineChartView.className = "metadata";
-						lineChartView.labelValue = "Metadata: ";
-						lineChartView.addLine(this.model.get('metadataUploadDates'));
-					}
-				}
-		},
-
-		//drawUploadTitle will draw a circle badge title for the uploads time series chart
-		drawUploadTitle: function(){
-
-			//If d3 isn't supported in this browser or didn't load correctly, insert a text title instead
-			if(!d3){
-				this.$('#uploads-title').html("<h2 class='packages fallback'>" + MetacatUI.appView.commaSeparateNumber(this.model.get('totalCount')) + "</h2>");
-
-				return;
-			}
-
-			if(!this.model.get('dataUploads') && !this.model.get('metadataUploads')){
-				//Draw the upload chart title
-				var uploadChartTitle = new CircleBadge({
-					id: "upload-chart-title",
-					className: "no-activity",
-					globalR: 60,
-					data: [{ count: 0, label: "uploads" }]
-				});
-
-				this.$('#uploads-title').prepend(uploadChartTitle.render().el);
-
-				return;
-			}
-
-			//Get information for our upload chart title
-			var titleChartData = [],
-				metadataUploads = this.model.get("metadataUploads"),
-				dataUploads = this.model.get("dataUploads"),
-				metadataClass = "metadata",
-				dataClass = "data";
-
-			if(metadataUploads == 0) metadataClass = "default";
-			if(dataUploads == 0) dataClass = "default";
-
-
-			var titleChartData = [
-			                      {count: this.model.get("metadataUploads"), label: "metadata", className: metadataClass},
-							      {count: this.model.get("dataUploads"), 	  label: "data", 	 className: dataClass}
-								 ];
-
-			//Draw the upload chart title
-			var uploadChartTitle = new CircleBadge({
-				id: "upload-chart-title",
-				data: titleChartData,
-				className: "chart-title",
-				useGlobalR: true,
-				globalR: 60
-			});
-			this.$('#uploads-title').prepend(uploadChartTitle.render().el);
-		},
-
-		/*
-		 * displayTotalCount - renders a simple count of total metadata files/datasets
-		 */
-		displayTotalCount: function(){
-
-			var className = "quick-stats-count";
-
-			if( !this.model.get("metadataCount") && !this.model.get("dataCount") )
-				className += " no-activity";
-
-			var countEl = $(document.createElement("p"))
-							.addClass(className)
-							.text(MetacatUI.appView.commaSeparateNumber(this.model.get("metadataCount")));
-
-			var titleEl = $(document.createElement("p"))
-							.addClass("chart-title")
-							.text("datasets");
-
-			this.$('#total-datasets').html(countEl);
-			this.$('#total-datasets').append(titleEl);
-		},
-
-		/*
-		 * displayTotalSize renders a count of the total file size of
-		 * all current metadata and data files
-		 */
-		displayTotalSize: function(){
-
-			var className = "quick-stats-count";
-			var count = "";
-
-			if( !this.model.get("totalSize") ){
-				count = "0 bytes";
-				className += " no-activity";
-			}
-			else{
-				count = this.bytesToSize( this.model.get("totalSize") );
-			}
-
-			var countEl = $(document.createElement("p"))
-							.addClass(className)
-							.text(count);
-
-			var titleEl = $(document.createElement("p"))
-							.addClass("chart-title")
-							.text("of content");
-
-			this.$('#total-size').html(countEl);
-			this.$('#total-size').append(titleEl);
-		},
-
-    /**
-     * Draws both the metadata and data update date charts.
-     * Note that this function may be deprecated in the future.
-     *  Views should directly call drawMetadataUpdatesChart() or drawDataUpdatesChart() directly,
-     *  since metadata and data dates are fetched via separate AJAX calls.
-     */
-    drawUpdatesChart: function(){
-
-      //Draw the metadata and data updates charts
-      this.drawMetadataUpdatesChart();
-      this.drawDataUpdatesChart();
+        // redraw the charts to avoid overlap at different widths
+        $(window).on("resize", function () {
+          if (!view.hideUpdatesChart) view.drawUpdatesChart();
+        });
+      },
 
-    },
+      //Draw a bar chart for the temporal coverage
+      drawCoverageChart: function (e, data) {
+        //Get the width of the chart by using the parent container width
+        var parentEl = this.$(".temporal-coverage-chart");
 
-    /**
-     * Draws a line chart representing the latest metadata updates over time
-     */
-    drawMetadataUpdatesChart: function(){
+        if (this.userType == "repository") {
+          parentEl.addClass("repository-portal-view");
+        }
+        var width = parentEl.width() || null;
 
-      //Set some configurations for the LineChart
-      var chartClasses = "data",
-          data;
+        // If results were found but none have temporal coverage, draw a default chart
+        if (!this.model.get("temporalCoverage")) {
+          parentEl.html(
+            "<p class='subtle center'>There are no metadata documents that describe temporal coverage.</p>",
+          );
 
-      //If the number of metadata objects in this data collection is 0, then set the data for the LineChart to null.
-      // And add a "no-activity" class to the chart.
-      if( !this.model.get("metadataUpdateDates") || !this.model.get("metadataUpdateDates").length ){
-        data = null;
-        chartClasses += " no-activity";
-      }
-      else{
-        //Use the metadata update dates for the LineChart
-        data = this.model.get('metadataUpdateDates');
-      }
-
-      //Create the line chart for metadata updates
-      var metadataLineChart = new LineChart({
-        data: data,
-        formatFromSolrFacets: true,
-        cumulative: false,
-        id: "updates-chart",
-        className: chartClasses,
-        yLabel: "metadata files updated",
-        width: this.$('.metadata-updates-chart').width(),
-        labelDate: "M-y"
-      });
-
-      //Render the LineChart and insert it into the container element
-      this.$('.metadata-updates-chart').html(metadataLineChart.render().el);
-    },
+          return;
+        }
 
-    /**
-    * Draws a line chart representing the latest metadata updates over time
-    */
-    drawDataUpdatesChart: function(){
-      //Set some configurations for the LineChart
-      var chartClasses = "data",
-          view = this,
-          data;
+        var options = {
+          data: data,
+          formatFromSolrFacets: true,
+          id: "temporal-coverage-chart",
+          yLabel: "data packages",
+          yFormat: d3.format(",d"),
+          barClass: "packages",
+          roundedRect: true,
+          roundedRadius: 3,
+          barLabelClass: "packages",
+          width: width,
+        };
+
+        var barChart = new BarChart(options);
+        parentEl.html(barChart.render().el);
+      },
+
+      drawCoverageChartTitle: function () {
+        if (
+          !this.model.get("firstBeginDate") ||
+          !this.model.get("lastEndDate") ||
+          !this.model.get("temporalCoverage")
+        )
+          return;
+
+        //Create the range query
+        var yearRange =
+          this.model.get("firstBeginDate").getUTCFullYear() +
+          " - " +
+          this.model.get("lastEndDate").getUTCFullYear();
+
+        //Find the year range element
+        this.$("#data-coverage-year-range").text(yearRange);
+      },
+
+      /*
+       * Shows that this person/group/node has no activity
+       */
+      showNoActivity: function () {
+        if (
+          this.model.get("metadataCount") === 0 &&
+          this.model.get("dataCount") === 0
+        ) {
+          this.$(".show-loading .loading").remove();
+          this.$(".stripe").addClass("no-activity");
+          this.$(".metric-chart-loading svg animate").remove();
+          $.each($(".metric-chart-loading .message"), function (i, messageEl) {
+            $(messageEl).html("No metrics to show");
+          });
+        }
+      },
+
+      /**
+       * Convert number of bytes into human readable format
+       *
+       * @param integer bytes     Number of bytes to convert
+       * @param integer precision Number of digits after the decimal separator
+       * @return string
+       */
+      bytesToSize: function (bytes, precision) {
+        var kibibyte = 1024;
+        var mebibyte = kibibyte * 1024;
+        var gibibyte = mebibyte * 1024;
+        var tebibyte = gibibyte * 1024;
+
+        if (typeof bytes === "undefined") var bytes = this.get("size");
+
+        if (bytes >= 0 && bytes < kibibyte) {
+          return bytes + " B";
+        } else if (bytes >= kibibyte && bytes < mebibyte) {
+          return (bytes / kibibyte).toFixed(precision) + " KiB";
+        } else if (bytes >= mebibyte && bytes < gibibyte) {
+          return (bytes / mebibyte).toFixed(precision) + " MiB";
+        } else if (bytes >= gibibyte && bytes < tebibyte) {
+          return (bytes / gibibyte).toFixed(precision) + " GiB";
+        } else if (bytes >= tebibyte) {
+          return (bytes / tebibyte).toFixed(precision) + " TiB";
+        } else {
+          return bytes + " B";
+        }
+      },
+
+      renderUsageMetricsError: function () {
+        var message =
+          "<p class='check-back-message'><strong>This might take some time. Check back in 24 hours to see these results.</strong></p>";
+
+        $.each(
+          $(".views-metrics, .downloads-metrics, #user-citations"),
+          function (i, metricEl) {
+            $(metricEl).find(".check-back-message").remove();
+            $(metricEl).find(".message").append(message);
+          },
+        );
+      },
+
+      /**
+       * renderMetadataAssessmentError - update the metadata assessment
+       * pre-loading figure to indicate to the user that the assessment is not
+       * available at the moment.
+       */
+      renderMetadataAssessmentError: function () {
+        try {
+          $("#metadata-assessment-graphic .message").append(
+            "<br><strong>This might take some time. Check back in 24 hours to see these results.</strong>",
+          );
+        } catch (e) {
+          console.log(
+            "Error showing the metadata assessment error message in the metrics. " +
+              e,
+          );
+        }
+      },
+
+      /*
+       * getReplicas gets the number of replicas in this member node
+       */
+      displayTotalReplicas: function () {
+        var view = this;
+        var className = "quick-stats-count";
+        var count;
+
+        if (this.model.get("totalReplicas") > 0) {
+          count = MetacatUI.appView.commaSeparateNumber(
+            view.model.get("totalReplicas"),
+          );
+
+          var countEl = $(document.createElement("p"))
+            .addClass(className)
+            .text(count);
+
+          var titleEl = $(document.createElement("p"))
+            .addClass("chart-title")
+            .text("replicas");
+
+          // display the totals
+          this.$("#total-replicas").html(countEl);
+          this.$("#total-replicas").append(titleEl);
+        } else {
+          // hide the replicas container if the replica count is 0.
+          this.$("#replicas-container").hide();
+        }
+      },
 
-      //Use the data update dates for the LineChart
-      if(this.model.get("dataCount")){
-        data = this.model.get('dataUpdateDates');
-      }
-      else{
-        //If the number of data objects in this data collection is 0, then set the data for the LineChart to null.
-        // And add a "no-activity" class to the chart.
-        data = null;
-        chartClasses += " no-activity";
-      }
-
-      //Create the line chart for data updates
-      var dataLineChart = new LineChart({
-        data: data,
-        formatFromSolrFacets: true,
-        cumulative: false,
-        id: "updates-chart",
-        className: chartClasses,
-        yLabel: "data files updated",
-        width: this.$('.data-updates-chart').width(),
-        labelDate: "M-y"
-      });
-
-      //Render the LineChart and insert it into the container element
-      this.$('.data-updates-chart').html(dataLineChart.render().el);
-
-			// redraw the charts to avoid overlap at different widths
-			$(window).on("resize", function(){
-
-				if(!view.hideUpdatesChart)
-					view.drawUpdatesChart();
-
-			});
-
-		},
-
-		//Draw a bar chart for the temporal coverage
-		drawCoverageChart: function(e, data){
-
-			//Get the width of the chart by using the parent container width
-			var parentEl = this.$('.temporal-coverage-chart');
-
-			if (this.userType == "repository") {
-				parentEl.addClass("repository-portal-view");
-			}
-			var width = parentEl.width() || null;
-
-			// If results were found but none have temporal coverage, draw a default chart
-			if(!this.model.get('temporalCoverage')){
-
-				parentEl.html("<p class='subtle center'>There are no metadata documents that describe temporal coverage.</p>");
-
-				return;
-			}
-
-				var options = {
-						data: data,
-						formatFromSolrFacets: true,
-						id: "temporal-coverage-chart",
-						yLabel: "data packages",
-						yFormat: d3.format(",d"),
-						barClass: "packages",
-						roundedRect: true,
-						roundedRadius: 3,
-						barLabelClass: "packages",
-						width: width
-					};
-
-			var barChart = new BarChart(options);
-			parentEl.html(barChart.render().el);
-
-		},
-
-		drawCoverageChartTitle: function(){
-			if((!this.model.get('firstBeginDate')) || (!this.model.get('lastEndDate')) || !this.model.get("temporalCoverage") ) return;
-
-			//Create the range query
-			var yearRange = this.model.get('firstBeginDate').getUTCFullYear() + " - " + this.model.get('lastEndDate').getUTCFullYear();
-
-			//Find the year range element
-			this.$('#data-coverage-year-range').text(yearRange);
-		},
-
-		/*
-		 * Shows that this person/group/node has no activity
-		 */
-		showNoActivity: function(){
-
-			if( this.model.get("metadataCount") === 0 && this.model.get("dataCount") === 0 ){
-					this.$(".show-loading .loading").remove();
-					this.$(".stripe").addClass("no-activity");
-						this.$(".metric-chart-loading svg animate").remove();
-						$.each($(".metric-chart-loading .message"), function(i,messageEl){
-							$(messageEl).html("No metrics to show")
-						});
-			}
-
-		},
-
-		/**
-		 * Convert number of bytes into human readable format
-		 *
-		 * @param integer bytes     Number of bytes to convert
-		 * @param integer precision Number of digits after the decimal separator
-		 * @return string
-		 */
-		bytesToSize: function(bytes, precision){
-		    var kibibyte = 1024;
-		    var mebibyte = kibibyte * 1024;
-		    var gibibyte = mebibyte * 1024;
-		    var tebibyte = gibibyte * 1024;
-
-		    if(typeof bytes === "undefined") var bytes = this.get("size");
-
-		    if ((bytes >= 0) && (bytes < kibibyte)) {
-		        return bytes + ' B';
-
-		    } else if ((bytes >= kibibyte) && (bytes < mebibyte)) {
-		        return (bytes / kibibyte).toFixed(precision) + ' KiB';
-
-		    } else if ((bytes >= mebibyte) && (bytes < gibibyte)) {
-		        return (bytes / mebibyte).toFixed(precision) + ' MiB';
+      onClose: function () {
+        //Clear the template
+        this.$el.html("");
+
+        //Stop listening to changes in the model
+        this.stopListening(this.model);
+
+        //Stop listening to resize
+        $(window).off("resize");
+
+        //Reset the stats model
+        this.model = null;
+      },
+    },
+  );
 
-		    } else if ((bytes >= gibibyte) && (bytes < tebibyte)) {
-		        return (bytes / gibibyte).toFixed(precision) + ' GiB';
-
-		    } else if (bytes >= tebibyte) {
-		        return (bytes / tebibyte).toFixed(precision) + ' TiB';
-
-		    } else {
-		        return bytes + ' B';
-		    }
-		},
-
-		renderUsageMetricsError: function() {
-      		var message = "<p class='check-back-message'><strong>This might take some time. Check back in 24 hours to see these results.</strong></p>";
-
-			$.each($('.views-metrics, .downloads-metrics, #user-citations'), function(i,metricEl){
-				$(metricEl).find(".check-back-message").remove();
-				$(metricEl).find(".message").append(message);
-			});
-		},
-
-		/**
-		 * renderMetadataAssessmentError - update the metadata assessment
-		 * pre-loading figure to indicate to the user that the assessment is not
-		 * available at the moment.
-		 */
-		renderMetadataAssessmentError: function(){
-			try {
-				$("#metadata-assessment-graphic .message").append("<br><strong>This might take some time. Check back in 24 hours to see these results.</strong>")
-			} catch (e) {
-				console.log("Error showing the metadata assessment error message in the metrics. " + e);
-			}
-		},
-
-		/*
-		 * getReplicas gets the number of replicas in this member node
-		 */
-		displayTotalReplicas: function(){
-
-			var view = this;
-			var className = "quick-stats-count";
-			var count;
-
-			if( this.model.get("totalReplicas") > 0 ){
-				count = MetacatUI.appView.commaSeparateNumber(view.model.get("totalReplicas"));
-
-				var countEl = $(document.createElement("p"))
-							.addClass(className)
-							.text(count);
-
-				var titleEl = $(document.createElement("p"))
-								.addClass("chart-title")
-								.text("replicas");
-
-				// display the totals
-				this.$('#total-replicas').html(countEl);
-				this.$('#total-replicas').append(titleEl);
-
-			}
-			else{
-				// hide the replicas container if the replica count is 0.
-				this.$('#replicas-container').hide()
-			}
-
-		},
-
-		onClose: function () {
-			//Clear the template
-			this.$el.html("");
-
-			//Stop listening to changes in the model
-			this.stopListening(this.model);
-
-			//Stop listening to resize
-			$(window).off("resize");
-
-			//Reset the stats model
-			this.model = null;
-		}
-
-	});
-
-	return StatsView;
+  return StatsView;
 });
 
diff --git a/docs/docs/src_js_views_TOCView.js.html b/docs/docs/src_js_views_TOCView.js.html index 1a7f0c03a..b68efa403 100644 --- a/docs/docs/src_js_views_TOCView.js.html +++ b/docs/docs/src_js_views_TOCView.js.html @@ -44,15 +44,15 @@

Source: src/js/views/TOCView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "text!templates/tableOfContentsLi.html",
-    "text!templates/tableOfContentsUL.html",
-    "text!templates/tableOfContents.html"],
-    function($, _, Backbone, TOCTemplateLi, TOCTemplateUl, TOCTemplate){
-
-    /**
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/tableOfContentsLi.html",
+  "text!templates/tableOfContentsUL.html",
+  "text!templates/tableOfContents.html",
+], function ($, _, Backbone, TOCTemplateLi, TOCTemplateUl, TOCTemplate) {
+  /**
      * @class TOCView
      * @classdesc    The Table of Contents View is a vertical navigation menu that links to other
         sections within the same view.
@@ -65,31 +65,30 @@ 

Source: src/js/views/TOCView.js

* @classcategory Views * @extends Backbone.View */ - var TOCView = Backbone.View.extend( - /** @lends TOCView.prototype */{ - - tagName: "div", - className: "toc toc-view", - - type: "TOC", - - /* Renders the compiled template into HTML */ - templateUL: _.template(TOCTemplateUl), - templateLI: _.template(TOCTemplateLi), - mainTemplate: _.template(TOCTemplate), - templateInvisibleH1: _.template( - '<h1 id="<%=linkDisplay%>" style="display: inline"></h1>' - ), - - events: { - 'click .dropdown' : 'toggleDropdown' - }, - - // The element on the page that contains the content that this table of contents - // is associated with. - contentEl: null, - - /* + var TOCView = Backbone.View.extend( + /** @lends TOCView.prototype */ { + tagName: "div", + className: "toc toc-view", + + type: "TOC", + + /* Renders the compiled template into HTML */ + templateUL: _.template(TOCTemplateUl), + templateLI: _.template(TOCTemplateLi), + mainTemplate: _.template(TOCTemplate), + templateInvisibleH1: _.template( + '<h1 id="<%=linkDisplay%>" style="display: inline"></h1>', + ), + + events: { + "click .dropdown": "toggleDropdown", + }, + + // The element on the page that contains the content that this table of contents + // is associated with. + contentEl: null, + + /* * A list of custom items to insert into the TOC * { "text": "Portal Description", @@ -98,409 +97,427 @@

Source: src/js/views/TOCView.js

"showSubItems": false } */ - topLevelItems: {}, - - //If set to true, will render one level of sub items/links - showSubItems: true, - - /* Construct a new instance */ - initialize: function(options) { - if(typeof options !== "undefined"){ - this.topLevelItems = options.topLevelItems || ""; - this.contentEl = options.contentEl || ""; - this.className = options.className || "toc toc-view"; - this.addScrollspy = options.addScrollspy || true; - this.affix = options.affix || true; - - if(options.showSubItems === false){ - this.showSubItems = false; - } + topLevelItems: {}, + + //If set to true, will render one level of sub items/links + showSubItems: true, + + /* Construct a new instance */ + initialize: function (options) { + if (typeof options !== "undefined") { + this.topLevelItems = options.topLevelItems || ""; + this.contentEl = options.contentEl || ""; + this.className = options.className || "toc toc-view"; + this.addScrollspy = options.addScrollspy || true; + this.affix = options.affix || true; + + if (options.showSubItems === false) { + this.showSubItems = false; + } + } + }, + + /* Render the view */ + render: function () { + var liTemplate = this.templateLI, + h1Template = this.templateInvisibleH1; + + this.$el.html( + this.mainTemplate({ + ulTemplate: this.templateUL(), + }), + ); + + // Save references to where we should insert the links + this.desktopUl = this.$el.find(".desktop ul"); + this.mobile = this.$el.find(".mobile"); + // Save references to the toggle links (and divider) so we can update their text/display on spyScoll + this.topLevelMobileToggle = this.mobile.find( + ".top-level-items .dropdown-toggle", + ); + this.secondLevelMobileToggle = this.mobile.find( + ".second-level-items .dropdown-toggle", + ); + this.mobileDivider = this.mobile.find(".mobile-toc-divider"); + + // Render the top level items that have been passed in + _.each( + this.topLevelItems, + function (topLevelItem) { + // Create a link to display based on the text of the TOC item + topLevelItem.linkDisplay = topLevelItem.text + .replace(/[\W_]+/g, "-") + .toLowerCase() + .replace(/^[\W_]+/g, ""); + + // Make an invisible (empty) H1 tag and stick it into the el + // that's the target of the TOC + $(topLevelItem.link).prepend( + h1Template({ linkDisplay: topLevelItem.linkDisplay }), + ); + + // Render the top level item + this.desktopUl.append(liTemplate(topLevelItem)); + + if ( + typeof topLevelItem.showSubItems == "undefined" || + topLevelItem.showSubItems == true + ) { + _.each( + this.createLinksFromHeaders(topLevelItem.link), + function (link, index) { + this.appendLink(link, index); + }, + this, + ); } - }, - - /* Render the view */ - render: function() { - - var liTemplate = this.templateLI, - h1Template = this.templateInvisibleH1; - - this.$el.html( - this.mainTemplate({ - ulTemplate: this.templateUL() - }) + }, + this, + ); + + // If no custom top-level items were given, find the headers in the content + if (!this.topLevelItems.length && this.contentEl) { + //Create links from the headers found in the content + _.each( + this.createLinksFromHeaders(), + function (link, index) { + this.appendLink(link, index); + }, + this, ); + } - // Save references to where we should insert the links - this.desktopUl = this.$el.find(".desktop ul"); - this.mobile = this.$el.find(".mobile"); - // Save references to the toggle links (and divider) so we can update their text/display on spyScoll - this.topLevelMobileToggle = this.mobile.find(".top-level-items .dropdown-toggle"); - this.secondLevelMobileToggle = this.mobile.find(".second-level-items .dropdown-toggle"); - this.mobileDivider = this.mobile.find(".mobile-toc-divider"); - - // Render the top level items that have been passed in - _.each(this.topLevelItems, function(topLevelItem){ - // Create a link to display based on the text of the TOC item - topLevelItem.linkDisplay = topLevelItem.text.replace(/[\W_]+/g, '-').toLowerCase().replace(/^[\W_]+/g, ''); - - // Make an invisible (empty) H1 tag and stick it into the el - // that's the target of the TOC - $(topLevelItem.link).prepend(h1Template({linkDisplay: topLevelItem.linkDisplay})); - - // Render the top level item - this.desktopUl.append(liTemplate(topLevelItem)); - - if( typeof topLevelItem.showSubItems == "undefined" || topLevelItem.showSubItems == true ){ - _.each(this.createLinksFromHeaders(topLevelItem.link), function(link, index){ - this.appendLink(link, index); - }, this); - } - - }, this); + return this; + }, + + /** + * appendLink - Adds the generated link to both the desktop and mobile TOCs + * + * @param {HTMLLIElement} link The top-level item to add, including second-level UL if present + * @param {number} index The index of the top-level item + */ + appendLink: function (link, index) { + // Append to the main (desktop) navigation + this.desktopUl.append(link); + + // Append to the mobile navigation + var mobileLink = $(link.clone()), + submenu = $(mobileLink.find("ul").clone()); + + // Make a list of only top-level li's + mobileLink.find("ul").remove(); + mobileLink.data("index", index); + this.mobile.find(".top-level-items ul").append(mobileLink); + // If there's a submenu, add it to the second-level-items menu. + // Add ID that allows us to match the parent to the submenu. + if (submenu && submenu.length) { + submenu.addClass("submenu hidden"); + submenu.data("index", index); + submenu.children("li").data("index", index); + this.mobile + .find(".second-level-items .dropdown-menu") + .append(submenu); + } + }, - // If no custom top-level items were given, find the headers in the content - if( !this.topLevelItems.length && this.contentEl ){ + createLinksFromHeaders: function (contentEl, headerLevel) { + //If no content element is specified, use the one attached to this view + if (!contentEl && this.contentEl) { + var contentEl = this.contentEl; + } + //If there is no content element attached to the view either, then exit + else if (!contentEl && !this.contentEl) { + return []; + } - //Create links from the headers found in the content - _.each(this.createLinksFromHeaders(), function(link, index){ - this.appendLink(link, index); - }, this); + //If there is no header level specified, find them in the content + if (!headerLevel) { + var headerLevel; + //Use the first-level header if it exists + if ($(contentEl).find("h1").length) { + headerLevel = "h1"; } - - return this; - - }, - - - /** - * appendLink - Adds the generated link to both the desktop and mobile TOCs - * - * @param {HTMLLIElement} link The top-level item to add, including second-level UL if present - * @param {number} index The index of the top-level item - */ - appendLink: function(link, index){ - - // Append to the main (desktop) navigation - this.desktopUl.append(link); - - // Append to the mobile navigation - var mobileLink = $(link.clone()), - submenu = $(mobileLink.find("ul").clone()); - - // Make a list of only top-level li's - mobileLink.find("ul").remove(); - mobileLink.data("index", index); - this.mobile.find(".top-level-items ul").append(mobileLink); - // If there's a submenu, add it to the second-level-items menu. - // Add ID that allows us to match the parent to the submenu. - if(submenu && submenu.length){ - submenu.addClass("submenu hidden"); - submenu.data("index", index); - submenu.children("li").data("index", index); - this.mobile.find(".second-level-items .dropdown-menu").append(submenu); + //Otherwise find second-level headers + else if ($(contentEl).find("h2").length) { + headerLevel = "h2"; } - }, - - createLinksFromHeaders: function( contentEl, headerLevel ){ - - //If no content element is specified, use the one attached to this view - if( !contentEl && this.contentEl ){ - var contentEl = this.contentEl; + //Otherwise find third-level headers + else if ($(contentEl).find("h3").length) { + headerLevel = "h3"; } - //If there is no content element attached to the view either, then exit - else if( !contentEl && !this.contentEl ){ + //Exit this function if there are no headers + else { return []; } + } - //If there is no header level specified, find them in the content - if( !headerLevel ){ - var headerLevel; - - //Use the first-level header if it exists - if( $(contentEl).find("h1").length ){ - headerLevel = "h1"; - } - //Otherwise find second-level headers - else if( $(contentEl).find("h2").length ){ - headerLevel = "h2"; - } - //Otherwise find third-level headers - else if( $(contentEl).find("h3").length ){ - headerLevel = "h3"; - } - //Exit this function if there are no headers - else{ - return []; - } - } - - //Create an array to contain all the link elements - var linkItems = []; - - // Within each top level item, look for header tags and - // render them as second level TOC items - var headers = $(contentEl).find(headerLevel); - - _.each(headers, function(header) { - - //Create the link HTML - var linkItem = $(this.templateLI({ - "link": "#" + $(header).attr("id"), - "text": $(header).text(), - "LIclass": "top-level-item" - })); - - linkItem.addClass("top-level-item"); - - //If we want to show subitems in the table of contents, find them - if( this.showSubItems ){ - - var nextEl = $(header).next(), - subHeaderLevel = "H" + (parseInt(headerLevel.charAt(1)) + 1), - subItems = $(this.templateUL()); - - while(nextEl.length){ - - if( nextEl[0].tagName == subHeaderLevel ){ - subItems.append(this.templateLI({ - "link": "#" + nextEl.attr("id"), - "text": nextEl.text(), - "LIclass": "second-level-item" - })); - - } - else if( nextEl[0].tagName == headerLevel.toUpperCase() ){ - break; - } - - nextEl = nextEl.next(); + //Create an array to contain all the link elements + var linkItems = []; + + // Within each top level item, look for header tags and + // render them as second level TOC items + var headers = $(contentEl).find(headerLevel); + + _.each( + headers, + function (header) { + //Create the link HTML + var linkItem = $( + this.templateLI({ + link: "#" + $(header).attr("id"), + text: $(header).text(), + LIclass: "top-level-item", + }), + ); + + linkItem.addClass("top-level-item"); + + //If we want to show subitems in the table of contents, find them + if (this.showSubItems) { + var nextEl = $(header).next(), + subHeaderLevel = "H" + (parseInt(headerLevel.charAt(1)) + 1), + subItems = $(this.templateUL()); + + while (nextEl.length) { + if (nextEl[0].tagName == subHeaderLevel) { + subItems.append( + this.templateLI({ + link: "#" + nextEl.attr("id"), + text: nextEl.text(), + LIclass: "second-level-item", + }), + ); + } else if (nextEl[0].tagName == headerLevel.toUpperCase()) { + break; } - //If at least one subheader/subitem was found, add them to the top-level item - if( subItems.children().length ){ - linkItem.append(subItems); - } + nextEl = nextEl.next(); } - //Create the link item and add to the array - linkItems.push( linkItem ); - - }, this); - - return linkItems; - - }, - - - /** - * addScrollspy - Adds and refreshes bootstrap's scrollSpy functionality, - * and sets the listener to call this view's scrollSpyExtras when - * Bootstrap's "activate" event is called. This function should be called - * anytime the DOM is updated. - */ - renderScrollspy: function(){ - - try { - - var view = this; - var scrollSpyClass = "scrollspy-TOC-" + this.cid; - var scrollSpyTarget = "." + scrollSpyClass; - - this.$el.addClass(scrollSpyClass); + //If at least one subheader/subitem was found, add them to the top-level item + if (subItems.children().length) { + linkItem.append(subItems); + } + } - // Manually set scrollspy data, - // see https://github.com/twbs/bootstrap/issues/20022#issuecomment-561376832 - var $spy = $("body").scrollspy({ target: scrollSpyTarget, offset: 35}); - var newSpyData = $spy.data(); - newSpyData.scrollspy.selector = scrollSpyTarget + " .nav li > a"; - $.fn.scrollspy.call($spy, newSpyData); - $spy.scrollspy("process"); + //Create the link item and add to the array + linkItems.push(linkItem); + }, + this, + ); + + return linkItems; + }, + + /** + * addScrollspy - Adds and refreshes bootstrap's scrollSpy functionality, + * and sets the listener to call this view's scrollSpyExtras when + * Bootstrap's "activate" event is called. This function should be called + * anytime the DOM is updated. + */ + renderScrollspy: function () { + try { + var view = this; + var scrollSpyClass = "scrollspy-TOC-" + this.cid; + var scrollSpyTarget = "." + scrollSpyClass; + + this.$el.addClass(scrollSpyClass); + + // Manually set scrollspy data, + // see https://github.com/twbs/bootstrap/issues/20022#issuecomment-561376832 + var $spy = $("body").scrollspy({ + target: scrollSpyTarget, + offset: 35, + }); + var newSpyData = $spy.data(); + newSpyData.scrollspy.selector = scrollSpyTarget + " .nav li > a"; + $.fn.scrollspy.call($spy, newSpyData); + $spy.scrollspy("process"); + $spy.scrollspy("refresh"); + + // Remove any active classes to start + var activeEls = this.$(scrollSpyTarget + " .active"); + activeEls.removeClass("active"); + + // Add scroll spy + $("body").off("activate"); + $("body").on("activate", function (e) { + view.scrollSpyExtras(e); + }); + $(window).off("resize"); + $(window).on("resize", function () { $spy.scrollspy("refresh"); - - // Remove any active classes to start - var activeEls = this.$(scrollSpyTarget + " .active"); - activeEls.removeClass("active"); - - // Add scroll spy - $("body").off("activate"); - $("body").on("activate", function (e) { - view.scrollSpyExtras(e); - }); - $(window).off("resize"); - $(window).on("resize", function () { - $spy.scrollspy("refresh"); - }); - - } catch (e) { - console.log("Error adding scrollspy! Error message: " + e); + }); + } catch (e) { + console.log("Error adding scrollspy! Error message: " + e); + } + }, + + /** + * Adds and refreshes bootstrap's affix functionality. This function + * should be called after the DOM has been rendered or updated. Renamed + * from postRender to avoid it being called automatically by Backbone. + * @since 2.27.0 + */ + setAffix: function () { + try { + var isVisible = this.$el.find(":visible").length > 0; + + if (!isVisible || !this.$el.offset()) { + return; } - }, - - - /** - * Adds and refreshes bootstrap's affix functionality. This function - * should be called after the DOM has been rendered or updated. Renamed - * from postRender to avoid it being called automatically by Backbone. - * @since 2.27.0 - */ - setAffix: function(){ - - try { - - var isVisible = this.$el.find(":visible").length > 0; + if (this.affix === true) { + this.$el.affix({ offset: this.$el.offset().top }); + } - if(!isVisible || !this.$el.offset()){ + if (this.addScrollspy) { + this.renderScrollspy(); + } + } catch (e) { + console.log( + "Error affixing the table of contents, error message: " + e, + ); + } + }, + + /** + * scrollSpyExtras - Adds extra functionality to Bootstrap's scrollSpy function. + * This function is called anytime the "activate" event is called by bootstrap. + * For the desktop TOC, if activates the parent LI in the case that a second-level + * LI is active. For the mobile TOC, it changes text displayed in this.topLevelMobileToggle + * and this.secondLevelMobileToggle to the active top-level and second-level item, respectively. + * It also makes only the active second-level menu visible under the secondLevelMobile dropdown. + * + * @param {event} e The "activate" event triggered when an LI element is activated by bootstrap's ScrollSpy + */ + scrollSpyExtras: function (e) { + try { + if (e && e.target) { + // console.log($(e.target)[0].innerText); + + var activeLI = $(e.target), + mobileContainer = activeLI.closest(".mobile"), + isTopLevel = activeLI.hasClass("top-level-item"), + isMobile = mobileContainer && mobileContainer.length; + + // --- DESKTOP --- // + + // For the desktop nav, just highlight the parent item if the + // activated item is a second-level item. + if (!isMobile) { + if (!isTopLevel) { + activeLI.closest(".top-level-item").addClass("active"); + } return; } - if (this.affix === true) { - this.$el.affix({ offset: this.$el.offset().top }); + // --- MOBILE --- // + + var allSubmenus = mobileContainer.find(".submenu"), + allToplevelLIs = mobileContainer.find(".top-level-item"), + itemText = activeLI.find("a").text().trim(), + index = activeLI.data("index"); // Used to match submenus to parent LIs. + + if (isTopLevel) { + // Update the toggle text, hide submenu displays + this.topLevelMobileToggle.text(itemText); + this.secondLevelMobileToggle.text(""); + this.secondLevelMobileToggle + .closest(".second-level-items") + .addClass("hidden"); + this.mobileDivider.addClass("hidden"); + + // Get the corresponding child submenu that should be active + var activeSubMenu = _.filter(allSubmenus, function (submenu) { + return $(submenu).data("index") == index; + }); + } else { + // Get the parent LI, make it active, and update the toggle text + activeTopLI = _.filter(allToplevelLIs, function (topLI) { + return $(topLI).data("index") == index; + }); + $(activeTopLI).addClass("active"); + this.topLevelMobileToggle.text( + $(activeTopLI).children("a").text().trim(), + ); + this.secondLevelMobileToggle.text(itemText); + + // Find the menu this LI is within + var activeSubMenu = activeLI.closest("ul"); } - if(this.addScrollspy){ - this.renderScrollspy(); + // Hide all submenus so only max 1 is active at a time + allSubmenus.addClass("hidden"); + + // Ensure the active submenu & associated toggle text is shown + if (activeSubMenu && activeSubMenu.length) { + $(activeSubMenu).removeClass("hidden"); + this.secondLevelMobileToggle + .closest(".second-level-items") + .removeClass("hidden"); + this.mobileDivider.removeClass("hidden"); + if (this.secondLevelMobileToggle.text() == "") { + this.secondLevelMobileToggle.text("Sub-sections"); + } } - - } catch (e) { - console.log("Error affixing the table of contents, error message: " + e); } - - }, - - /** - * scrollSpyExtras - Adds extra functionality to Bootstrap's scrollSpy function. - * This function is called anytime the "activate" event is called by bootstrap. - * For the desktop TOC, if activates the parent LI in the case that a second-level - * LI is active. For the mobile TOC, it changes text displayed in this.topLevelMobileToggle - * and this.secondLevelMobileToggle to the active top-level and second-level item, respectively. - * It also makes only the active second-level menu visible under the secondLevelMobile dropdown. - * - * @param {event} e The "activate" event triggered when an LI element is activated by bootstrap's ScrollSpy - */ - scrollSpyExtras: function(e){ - - try { - if(e && e.target){ - // console.log($(e.target)[0].innerText); - - var activeLI = $(e.target), - mobileContainer = activeLI.closest(".mobile"), - isTopLevel = activeLI.hasClass("top-level-item"), - isMobile = (mobileContainer && mobileContainer.length); - - // --- DESKTOP --- // - - // For the desktop nav, just highlight the parent item if the - // activated item is a second-level item. - if(!isMobile){ - if(!isTopLevel){ - activeLI.closest(".top-level-item").addClass("active"); - } - return - } - - // --- MOBILE --- // - - var allSubmenus = mobileContainer.find(".submenu"), - allToplevelLIs = mobileContainer.find(".top-level-item"), - itemText = activeLI.find("a").text().trim(), - index = activeLI.data("index"); // Used to match submenus to parent LIs. - - if(isTopLevel){ - - // Update the toggle text, hide submenu displays - this.topLevelMobileToggle.text(itemText); - this.secondLevelMobileToggle.text(""); - this.secondLevelMobileToggle.closest(".second-level-items").addClass("hidden"); - this.mobileDivider.addClass("hidden"); - - // Get the corresponding child submenu that should be active - var activeSubMenu = _.filter(allSubmenus, function(submenu){ - return ($(submenu).data("index") == index) - }); - + } catch (error) { + console.log( + "error adding extra scrollSpy functionality to portal section, error message: " + + error, + ); + } + }, + + /** + * toggleDropdown - Extends bootstrap's dropdown menu functionality by + * hiding the dropdown menu when the user clicks the dropdown toggle or + * any of the options within the dropdown menu. + * + * @param {event} e The click event on any part of the dropdown element + */ + toggleDropdown: function (e) { + try { + if ( + e && + e.target && + $(e.target).closest(".dropdown").children(".dropdown-menu") + ) { + // The entire dropdown element including toggle and menu + var $dropdown = $(e.target).closest(".dropdown"), + // The menu that we wish to show and hide on click + $menu = $dropdown.children(".dropdown-menu"); + + // Wait for bootstrap to add or remove the open class on $dropdown + setTimeout(function () { + if ($menu.hasClass("hidden") || $dropdown.hasClass("open")) { + $menu.removeClass("hidden"); } else { - - // Get the parent LI, make it active, and update the toggle text - activeTopLI = _.filter(allToplevelLIs, function(topLI){ - return ($(topLI).data("index") == index) - }); - $(activeTopLI).addClass("active"); - this.topLevelMobileToggle.text($(activeTopLI).children("a").text().trim()); - this.secondLevelMobileToggle.text(itemText); - - // Find the menu this LI is within - var activeSubMenu = activeLI.closest("ul"); - } - - // Hide all submenus so only max 1 is active at a time - allSubmenus.addClass("hidden"); - - // Ensure the active submenu & associated toggle text is shown - if (activeSubMenu && activeSubMenu.length){ - $(activeSubMenu).removeClass("hidden"); - this.secondLevelMobileToggle.closest(".second-level-items").removeClass("hidden"); - this.mobileDivider.removeClass("hidden"); - if(this.secondLevelMobileToggle.text() == ""){ - this.secondLevelMobileToggle.text("Sub-sections") - } + $menu.addClass("hidden"); } - }; - - } catch (error) { - console.log("error adding extra scrollSpy functionality to portal section, error message: " + error); + }, 5); } - }, - - /** - * toggleDropdown - Extends bootstrap's dropdown menu functionality by - * hiding the dropdown menu when the user clicks the dropdown toggle or - * any of the options within the dropdown menu. - * - * @param {event} e The click event on any part of the dropdown element - */ - toggleDropdown: function(e){ - - try { - if(e && e.target && $(e.target).closest(".dropdown").children(".dropdown-menu")){ - - // The entire dropdown element including toggle and menu - var $dropdown = $(e.target).closest(".dropdown"), - // The menu that we wish to show and hide on click - $menu = $dropdown.children(".dropdown-menu"); - - // Wait for bootstrap to add or remove the open class on $dropdown - setTimeout(function () { - if($menu.hasClass("hidden") || $dropdown.hasClass("open")){ - $menu.removeClass("hidden"); - } else { - $menu.addClass("hidden"); - } - }, 5); - - } - } catch (error) { - console.log("error hiding TOC dropdown menu on click, error message: " + error); - } - - }, - - - /** - * onClose - Close and destroy the view - */ - onClose: function() { - // Make sure to stop scrollSpy listeners - $("body").off("activate"); - $(window).off("resize"); - + } catch (error) { + console.log( + "error hiding TOC dropdown menu on click, error message: " + error, + ); } - - }); - - return TOCView; + }, + + /** + * onClose - Close and destroy the view + */ + onClose: function () { + // Make sure to stop scrollSpy listeners + $("body").off("activate"); + $(window).off("resize"); + }, + }, + ); + + return TOCView; });
diff --git a/docs/docs/src_js_views_TableEditorView.js.html b/docs/docs/src_js_views_TableEditorView.js.html index 4ef1e5ac1..721fbf840 100644 --- a/docs/docs/src_js_views_TableEditorView.js.html +++ b/docs/docs/src_js_views_TableEditorView.js.html @@ -45,332 +45,363 @@

Source: src/js/views/TableEditorView.js

define([
-    "underscore",
-    "jquery",
-    "backbone",
-    "markdownTableFromJson",
-    "markdownTableToJson",
-    "text!templates/tableEditor.html"
-  ],
-  function(
-    _,
-    $,
-    Backbone,
-    markdownTableFromJson,
-    markdownTableToJson,
-    Template
-  ){
-
-    /**
-     * @class TableEditorView
-     * @classdesc A view of an HTML textarea with markdown editor UI and preview tab
-     * @classcategory Views
-     * @extends Backbone.View
-     * @constructor
-     */
-    var TableEditorView = Backbone.View.extend(
-      /** @lends TableEditorView.prototype */
-      {
-
-        /**
-         * The type of View this is
-         * @type {string}
-         * @readonly
-         */
-        type: "TableEditor",
-
-        /**
-         * The HTML classes to use for this view's element
-         * @type {string}
-         */
-        className: "table-editor",
-
-        /**
-         * References to templates for this view. HTML files are converted to
-         * Underscore.js templates
-         * @type {Underscore.Template}
-         */
-        template: _.template(Template),
-
-        /**
-         * The current number of rows displayed in the spreadsheet, including the
-         * header row
-         * @type {number}
-         */
-        rowCount: 0, // No of rows
-
-        /**
-         * The current number of columns displayed in the spreadsheet, including the
-         * row number column
-         * @type {number}
-         */
-        colCount: 0, // No of cols
-
-        /**
-         * The same data shown in the table as a stringified JSON object.
-         * @type {string}
-         */
-        tableData: "",
-
-        /**
-         * Map for storing the sorting history of every column
-         * @type {map}
-         */
-        sortingHistory: new Map(),
-
-        /**
-         * The events this view will listen to and the associated function to call.
-         * @type {Object}
-         */
-        events: {
-          "click #reset": "resetData",
-          "focusout table": "updateData",
-          "click .table-body": "handleBodyClick",
-          "click .table-headers": "handleHeadersClick",
-          "click *": "closeDropdown",
-        },
-
-        /**
-         * Default row & column count for empty tables
-         * @type {object}
-         */
-        defaults: {
-          initialRowCount: 7,
-          initialColCount: 3
-        },
-
-        /**
-         * Initialize is executed when a new tableEditor is created.
-         * @constructs TableEditorView
-         * @param {Object} options - A literal object with options to pass to the view
-         */
-        initialize: function(options) {
-
-          try {
-            options = _.extend(this.defaults, options);
-
-            // Get all the options and apply them to this view
-            if (options) {
-              var optionKeys = Object.keys(options);
-              _.each(optionKeys, function(key, i) {
+  "underscore",
+  "jquery",
+  "backbone",
+  "markdownTableFromJson",
+  "markdownTableToJson",
+  "text!templates/tableEditor.html",
+], function (
+  _,
+  $,
+  Backbone,
+  markdownTableFromJson,
+  markdownTableToJson,
+  Template,
+) {
+  /**
+   * @class TableEditorView
+   * @classdesc A view of an HTML textarea with markdown editor UI and preview tab
+   * @classcategory Views
+   * @extends Backbone.View
+   * @constructor
+   */
+  var TableEditorView = Backbone.View.extend(
+    /** @lends TableEditorView.prototype */
+    {
+      /**
+       * The type of View this is
+       * @type {string}
+       * @readonly
+       */
+      type: "TableEditor",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "table-editor",
+
+      /**
+       * References to templates for this view. HTML files are converted to
+       * Underscore.js templates
+       * @type {Underscore.Template}
+       */
+      template: _.template(Template),
+
+      /**
+       * The current number of rows displayed in the spreadsheet, including the
+       * header row
+       * @type {number}
+       */
+      rowCount: 0, // No of rows
+
+      /**
+       * The current number of columns displayed in the spreadsheet, including the
+       * row number column
+       * @type {number}
+       */
+      colCount: 0, // No of cols
+
+      /**
+       * The same data shown in the table as a stringified JSON object.
+       * @type {string}
+       */
+      tableData: "",
+
+      /**
+       * Map for storing the sorting history of every column
+       * @type {map}
+       */
+      sortingHistory: new Map(),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "click #reset": "resetData",
+        "focusout table": "updateData",
+        "click .table-body": "handleBodyClick",
+        "click .table-headers": "handleHeadersClick",
+        "click *": "closeDropdown",
+      },
+
+      /**
+       * Default row & column count for empty tables
+       * @type {object}
+       */
+      defaults: {
+        initialRowCount: 7,
+        initialColCount: 3,
+      },
+
+      /**
+       * Initialize is executed when a new tableEditor is created.
+       * @constructs TableEditorView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          options = _.extend(this.defaults, options);
+
+          // Get all the options and apply them to this view
+          if (options) {
+            var optionKeys = Object.keys(options);
+            _.each(
+              optionKeys,
+              function (key, i) {
                 this[key] = options[key];
-              }, this);
-            }
-          } catch (e) {
-            console.log("Failed to initialize the table editor view, error message: " + e);
+              },
+              this,
+            );
           }
-
-        },
-
-        /**
-         * render - Renders the tableEditor - add UI for creating and editing tables
-         */
-        render: function() {
-          try {
-            // Insert the template into the view
-            this.$el.html(this.template({
-              cid: this.cid
-            })).data("view", this);
-
-            // If initalized with markdown, convert to JSON and use as table data
-            // Parse the table string into a javascript object so that we can pass it
-            // into the table editor view to be edited by the user.
-            if (this.markdown && this.markdown.length > 0) {
-              var tableArray = this.getJSONfromMarkdown(this.markdown);
-              if (tableArray && Array.isArray(tableArray) && tableArray.length) {
-                this.saveData(tableArray);
-                this.createSpreadsheet();
-                // Add the column that we use for row numbers in the editor
-                this.addColumn(0, "left");
-              }
-            } else {
+        } catch (e) {
+          console.log(
+            "Failed to initialize the table editor view, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * render - Renders the tableEditor - add UI for creating and editing tables
+       */
+      render: function () {
+        try {
+          // Insert the template into the view
+          this.$el
+            .html(
+              this.template({
+                cid: this.cid,
+              }),
+            )
+            .data("view", this);
+
+          // If initalized with markdown, convert to JSON and use as table data
+          // Parse the table string into a javascript object so that we can pass it
+          // into the table editor view to be edited by the user.
+          if (this.markdown && this.markdown.length > 0) {
+            var tableArray = this.getJSONfromMarkdown(this.markdown);
+            if (tableArray && Array.isArray(tableArray) && tableArray.length) {
+              this.saveData(tableArray);
               this.createSpreadsheet();
+              // Add the column that we use for row numbers in the editor
+              this.addColumn(0, "left");
             }
-          } catch (e) {
-            console.log("Failed to render the table editor view, error message: " + e);
-          }
-        },
-
-        /**
-         * createSpreadsheet - Creates or re-creates the table & headers with data,
-         * if there is any.
-         */
-        createSpreadsheet: function() {
-          try {
-            const spreadsheetData = this.getData();
-
-            this.rowCount = spreadsheetData.length - 1 || this.initialRowCount;
-            this.colCount = spreadsheetData[0].length - 1 || this.initialColCount;
-
-            const tableHeaderElement = this.$el.find(".table-headers")[0];
-            const tableBodyElement = this.$el.find(".table-body")[0];
-
-            const tableBody = tableBodyElement.cloneNode(true);
-            tableBodyElement.parentNode.replaceChild(tableBody, tableBodyElement);
-            const tableHeaders = tableHeaderElement.cloneNode(true);
-            tableHeaderElement.parentNode.replaceChild(tableHeaders, tableHeaderElement);
-
-            tableHeaders.innerHTML = "";
-            tableBody.innerHTML = "";
-
-            tableHeaders.appendChild(this.createHeaderRow(this.colCount));
-            this.createTableBody(tableBody, this.rowCount, this.colCount);
-
-            this.populateTable();
-          } catch (e) {
-            console.log("Failed to create a spreadsheet in the table editor view, error message: " + e);
+          } else {
+            this.createSpreadsheet();
           }
-        },
-
-        /**
-         * populateTable - Fill data in created table from saved data
-         */
-        populateTable: function() {
-          try {
-            const data = this.getData();
-            if (data === undefined || data === null) return;
-
-            for (let i = 0; i < data.length; i++) {
-              for (let j = 1; j < data[i].length; j++) {
-                const cell = this.$el.find(`#r-${i}-${j}`)[0];
-                let value = data[i][j];
-                if (i > 0) {
-                  cell.innerHTML = data[i][j];
-                } else {
-                  // table headers
-                  if (!value) {
-                    value = "Col " + j;
-                  }
-                  $(cell).find(".column-header-span")[0].innerHTML = value;
+        } catch (e) {
+          console.log(
+            "Failed to render the table editor view, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * createSpreadsheet - Creates or re-creates the table & headers with data,
+       * if there is any.
+       */
+      createSpreadsheet: function () {
+        try {
+          const spreadsheetData = this.getData();
+
+          this.rowCount = spreadsheetData.length - 1 || this.initialRowCount;
+          this.colCount = spreadsheetData[0].length - 1 || this.initialColCount;
+
+          const tableHeaderElement = this.$el.find(".table-headers")[0];
+          const tableBodyElement = this.$el.find(".table-body")[0];
+
+          const tableBody = tableBodyElement.cloneNode(true);
+          tableBodyElement.parentNode.replaceChild(tableBody, tableBodyElement);
+          const tableHeaders = tableHeaderElement.cloneNode(true);
+          tableHeaderElement.parentNode.replaceChild(
+            tableHeaders,
+            tableHeaderElement,
+          );
+
+          tableHeaders.innerHTML = "";
+          tableBody.innerHTML = "";
+
+          tableHeaders.appendChild(this.createHeaderRow(this.colCount));
+          this.createTableBody(tableBody, this.rowCount, this.colCount);
+
+          this.populateTable();
+        } catch (e) {
+          console.log(
+            "Failed to create a spreadsheet in the table editor view, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * populateTable - Fill data in created table from saved data
+       */
+      populateTable: function () {
+        try {
+          const data = this.getData();
+          if (data === undefined || data === null) return;
+
+          for (let i = 0; i < data.length; i++) {
+            for (let j = 1; j < data[i].length; j++) {
+              const cell = this.$el.find(`#r-${i}-${j}`)[0];
+              let value = data[i][j];
+              if (i > 0) {
+                cell.innerHTML = data[i][j];
+              } else {
+                // table headers
+                if (!value) {
+                  value = "Col " + j;
                 }
-
+                $(cell).find(".column-header-span")[0].innerHTML = value;
               }
             }
-          } catch (e) {
-            console.log("Failed to populate the table in the table editor view, error message: " + e);
           }
-        },
-
-        /**
-         * getData - Get the saved data and parse it. If there's no saved data,
-         * create it.
-         */
-        getData: function() {
-          try {
-            let data = this.tableData;
-            if (data === undefined || data === null || data.length == 0) {
-              return this.initializeData();
-            }
-            return JSON.parse(data);
-          } catch (e) {
-            console.log("Failed to get and parse data in the Table Editor View, error message: " + e);
+        } catch (e) {
+          console.log(
+            "Failed to populate the table in the table editor view, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * getData - Get the saved data and parse it. If there's no saved data,
+       * create it.
+       */
+      getData: function () {
+        try {
+          let data = this.tableData;
+          if (data === undefined || data === null || data.length == 0) {
+            return this.initializeData();
           }
-        },
-
-        /**
-         * initializeData - Create some empty arrays to hold data
-         */
-        initializeData: function() {
-          try {
-            const data = [];
-            for (let i = 0; i <= this.rowCount; i++) {
-              const child = [];
-              for (let j = 0; j <= this.colCount; j++) {
-                child.push("");
-              }
-              data.push(child);
+          return JSON.parse(data);
+        } catch (e) {
+          console.log(
+            "Failed to get and parse data in the Table Editor View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * initializeData - Create some empty arrays to hold data
+       */
+      initializeData: function () {
+        try {
+          const data = [];
+          for (let i = 0; i <= this.rowCount; i++) {
+            const child = [];
+            for (let j = 0; j <= this.colCount; j++) {
+              child.push("");
             }
-            return data;
-          } catch (e) {
-            console.log("Failed to create new data in the Table Editor View, error message: " + e);
+            data.push(child);
           }
-        },
-
-        /**
-         * updateData - When the user focuses out, presume they've changed the data,
-         * and updated the saved data.
-         *
-         * @param  {event} e The focus out event that triggered this function
-         */
-        updateData: function(e) {
-          try {
-            if (e.target) {
-              let item;
-              let newValue;
-              if (e.target.nodeName === "TD") {
-                item = e.target;
-                newValue = item.textContent;
-              } else if (e.target.classList.contains("column-header-span")) {
-                item = e.target.parentNode;
-                newValue = e.target.textContent;
-              }
-              if (item) {
-                const indices = item.id.split("-");
-                let spreadsheetData = this.getData();
-                spreadsheetData[indices[1]][indices[2]] = newValue;
-                this.saveData(spreadsheetData);
-              }
+          return data;
+        } catch (e) {
+          console.log(
+            "Failed to create new data in the Table Editor View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * updateData - When the user focuses out, presume they've changed the data,
+       * and updated the saved data.
+       *
+       * @param  {event} e The focus out event that triggered this function
+       */
+      updateData: function (e) {
+        try {
+          if (e.target) {
+            let item;
+            let newValue;
+            if (e.target.nodeName === "TD") {
+              item = e.target;
+              newValue = item.textContent;
+            } else if (e.target.classList.contains("column-header-span")) {
+              item = e.target.parentNode;
+              newValue = e.target.textContent;
             }
-          } catch (e) {
-            console.log("Failed to update data in the Table Editor View, error message: " + e);
-          }
-        },
-
-        /**
-         * saveData - Save the data as a string.
-         *
-         * @param  {type} data description
-         * @return {type}      description
-         */
-        saveData: function(data) {
-          try {
-            this.tableData = JSON.stringify(data);
-          } catch (e) {
-            console.log("Failed to save data in the Table Editor View, error message: " + e);
-          }
-        },
-
-        /**
-         * resetData - Clear the saved data and reset the table to the default
-         * number of rows & columns
-         *
-         * @param  {event} e - the event that triggered this function
-         */
-        resetData: function(e) {
-          try {
-            confirmation = confirm("This will erase all data and reset the table. Are you sure?");
-            if (confirmation == true) {
-              this.tableData = "";
-              this.rowCount = this.initialRowCount;
-              this.colCount = this.initialColCount;
-              this.createSpreadsheet();
-            } else {
-              return
+            if (item) {
+              const indices = item.id.split("-");
+              let spreadsheetData = this.getData();
+              spreadsheetData[indices[1]][indices[2]] = newValue;
+              this.saveData(spreadsheetData);
             }
-          } catch (e) {
-            console.log("Failed to reset data in the Table Editor View, error message: " + e);
           }
-        },
-
-        /**
-         * createHeaderRow - Create a header row for the table
-         */
-        createHeaderRow: function() {
-          try {
-            const tr = document.createElement("tr");
-            tr.setAttribute("id", "r-0");
-            for (let i = 0; i <= this.colCount; i++) {
-              const th = document.createElement("th");
-              th.setAttribute("id", `r-0-${i}`);
-              th.setAttribute("class", `${i === 0 ? "" : "column-header"}`);
-              if (i !== 0) {
-                const span = document.createElement("span");
-                span.innerHTML = `Col ${i}`;
-                span.setAttribute("class", "column-header-span");
-                span.setAttribute("contentEditable", "true");
-                const dropDownDiv = document.createElement("div");
-                dropDownDiv.setAttribute("class", "dropdown");
-                dropDownDiv.innerHTML = `
+        } catch (e) {
+          console.log(
+            "Failed to update data in the Table Editor View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * saveData - Save the data as a string.
+       *
+       * @param  {type} data description
+       * @return {type}      description
+       */
+      saveData: function (data) {
+        try {
+          this.tableData = JSON.stringify(data);
+        } catch (e) {
+          console.log(
+            "Failed to save data in the Table Editor View, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * resetData - Clear the saved data and reset the table to the default
+       * number of rows & columns
+       *
+       * @param  {event} e - the event that triggered this function
+       */
+      resetData: function (e) {
+        try {
+          confirmation = confirm(
+            "This will erase all data and reset the table. Are you sure?",
+          );
+          if (confirmation == true) {
+            this.tableData = "";
+            this.rowCount = this.initialRowCount;
+            this.colCount = this.initialColCount;
+            this.createSpreadsheet();
+          } else {
+            return;
+          }
+        } catch (e) {
+          console.log(
+            "Failed to reset data in the Table Editor View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * createHeaderRow - Create a header row for the table
+       */
+      createHeaderRow: function () {
+        try {
+          const tr = document.createElement("tr");
+          tr.setAttribute("id", "r-0");
+          for (let i = 0; i <= this.colCount; i++) {
+            const th = document.createElement("th");
+            th.setAttribute("id", `r-0-${i}`);
+            th.setAttribute("class", `${i === 0 ? "" : "column-header"}`);
+            if (i !== 0) {
+              const span = document.createElement("span");
+              span.innerHTML = `Col ${i}`;
+              span.setAttribute("class", "column-header-span");
+              span.setAttribute("contentEditable", "true");
+              const dropDownDiv = document.createElement("div");
+              dropDownDiv.setAttribute("class", "dropdown");
+              dropDownDiv.innerHTML = `
             <button class="dropbtn" id="col-dropbtn-${i}">
               <i class="icon pointer icon-caret-down"></i>
             </button>
@@ -381,36 +412,39 @@ 

Source: src/js/views/TableEditorView.js

<button class="col-dropdown-option col-delete"><i class="icon icon-remove icon-on-left"></i>Delete column</button> </div> `; - th.appendChild(span); - th.appendChild(dropDownDiv); - } - tr.appendChild(th); + th.appendChild(span); + th.appendChild(dropDownDiv); } - return tr; - } catch (e) { - console.log("Failed to create header row in the Table Editor View, error message: " + e); + tr.appendChild(th); } - }, - - /** - * createTableBodyRow - Create a row for the table - * - * @param {number} rowNum The table row number to add to the table, where 0 is the header row - */ - createTableBodyRow: function(rowNum) { - try { - const tr = document.createElement("tr"); - tr.setAttribute("id", `r-${rowNum}`); - for (let i = 0; i <= this.colCount; i++) { - const cell = document.createElement(`${i === 0 ? "th" : "td"}`); - // header - if (i === 0) { - cell.contentEditable = false; - const span = document.createElement("span"); - const dropDownDiv = document.createElement("div"); - span.innerHTML = rowNum; - dropDownDiv.setAttribute("class", "dropdown"); - dropDownDiv.innerHTML = ` + return tr; + } catch (e) { + console.log( + "Failed to create header row in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * createTableBodyRow - Create a row for the table + * + * @param {number} rowNum The table row number to add to the table, where 0 is the header row + */ + createTableBodyRow: function (rowNum) { + try { + const tr = document.createElement("tr"); + tr.setAttribute("id", `r-${rowNum}`); + for (let i = 0; i <= this.colCount; i++) { + const cell = document.createElement(`${i === 0 ? "th" : "td"}`); + // header + if (i === 0) { + cell.contentEditable = false; + const span = document.createElement("span"); + const dropDownDiv = document.createElement("div"); + span.innerHTML = rowNum; + dropDownDiv.setAttribute("class", "dropdown"); + dropDownDiv.innerHTML = ` <button class="dropbtn" id="row-dropbtn-${rowNum}"> <i class="icon pointer icon-caret-right"></i> </button> @@ -420,409 +454,448 @@

Source: src/js/views/TableEditorView.js

<button class="row-dropdown-option row-delete"><i class="icon icon-remove icon-on-left"></i>Delete row</button> </div> `; - cell.appendChild(span); - cell.appendChild(dropDownDiv); - cell.setAttribute("class", "row-header"); - } else { - cell.contentEditable = true; - } - cell.setAttribute("id", `r-${rowNum}-${i}`); - tr.appendChild(cell); + cell.appendChild(span); + cell.appendChild(dropDownDiv); + cell.setAttribute("class", "row-header"); + } else { + cell.contentEditable = true; } - return tr; - } catch (e) { - console.log("Failed to create table row in the Table Editor View, error message: " + e); + cell.setAttribute("id", `r-${rowNum}-${i}`); + tr.appendChild(cell); } - }, - - /** - * createTableBody - Given a table element, add table rows - * - * @param {HTMLElement} tableBody A table HTML Element - */ - createTableBody: function(tableBody) { - try { - for (let rowNum = 1; rowNum <= this.rowCount; rowNum++) { - tableBody.appendChild(this.createTableBodyRow(rowNum)); - } - } catch (e) { - console.log("Failed to create table body in the Table Editor View, error message: " + e); + return tr; + } catch (e) { + console.log( + "Failed to create table row in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * createTableBody - Given a table element, add table rows + * + * @param {HTMLElement} tableBody A table HTML Element + */ + createTableBody: function (tableBody) { + try { + for (let rowNum = 1; rowNum <= this.rowCount; rowNum++) { + tableBody.appendChild(this.createTableBodyRow(rowNum)); } - }, - - /** - * addRow - Utility function to add row - * - * @param {number} currentRow The row number at which to add a new row - * @param {string} direction Can be "top" or "bottom", indicating whether to new row should be above or below the current row - */ - addRow: function(currentRow, direction) { - try { - let data = this.getData(); - const colCount = data[0].length; - const newRow = new Array(colCount).fill(""); - if (direction === "top") { - data.splice(currentRow, 0, newRow); - } else if (direction === "bottom") { - data.splice(currentRow + 1, 0, newRow); - } - this.rowCount++; - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log("Failed to add row in the Table Editor View, error message: " + e); + } catch (e) { + console.log( + "Failed to create table body in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * addRow - Utility function to add row + * + * @param {number} currentRow The row number at which to add a new row + * @param {string} direction Can be "top" or "bottom", indicating whether to new row should be above or below the current row + */ + addRow: function (currentRow, direction) { + try { + let data = this.getData(); + const colCount = data[0].length; + const newRow = new Array(colCount).fill(""); + if (direction === "top") { + data.splice(currentRow, 0, newRow); + } else if (direction === "bottom") { + data.splice(currentRow + 1, 0, newRow); } - }, - - /** - * deleteRow - Utility function to delete row - * - * @param {number} currentRow The row number to delete - */ - deleteRow: function(currentRow) { - try { - let data = this.getData(); - // Don't allow deletion of the last row - if (data.length <= 2) { - this.resetData(); - return; - } - data.splice(currentRow, 1); - this.rowCount--; - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log("Failed to delete row in the Table Editor View, error message: " + e); + this.rowCount++; + this.saveData(data); + this.createSpreadsheet(); + } catch (e) { + console.log( + "Failed to add row in the Table Editor View, error message: " + e, + ); + } + }, + + /** + * deleteRow - Utility function to delete row + * + * @param {number} currentRow The row number to delete + */ + deleteRow: function (currentRow) { + try { + let data = this.getData(); + // Don't allow deletion of the last row + if (data.length <= 2) { + this.resetData(); + return; } - }, - - /** - * addColumn - Utility function to add columns - * - * @param {number} currentCol The column number at which to add a new column - * @param {string} direction Can be "left" or "right", indicating whether to new column should be to the left or right of the current column - */ - addColumn: function(currentCol, direction) { - try { - let data = this.getData(); - for (let i = 0; i <= this.rowCount; i++) { - if (direction === "left") { - data[i].splice(currentCol, 0, ""); - } else if (direction === "right") { - data[i].splice(currentCol + 1, 0, ""); - } + data.splice(currentRow, 1); + this.rowCount--; + this.saveData(data); + this.createSpreadsheet(); + } catch (e) { + console.log( + "Failed to delete row in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * addColumn - Utility function to add columns + * + * @param {number} currentCol The column number at which to add a new column + * @param {string} direction Can be "left" or "right", indicating whether to new column should be to the left or right of the current column + */ + addColumn: function (currentCol, direction) { + try { + let data = this.getData(); + for (let i = 0; i <= this.rowCount; i++) { + if (direction === "left") { + data[i].splice(currentCol, 0, ""); + } else if (direction === "right") { + data[i].splice(currentCol + 1, 0, ""); } - this.colCount++; - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log("Failed to add column in the Table Editor View, error message: " + e); } - }, - - /** - * deleteColumn - Utility function to delete column - * - * @param {number} currentCol The number of the column to delete - */ - deleteColumn: function(currentCol) { - try { - let data = this.getData(); - // Don't allow deletion of the last column - if (data[0].length <= 2) { - this.resetData(); - return; - } - for (let i = 0; i <= this.rowCount; i++) { - data[i].splice(currentCol, 1); - } - this.colCount--; - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log("Failed to delete column in the Table Editor View, error message: " + e); + this.colCount++; + this.saveData(data); + this.createSpreadsheet(); + } catch (e) { + console.log( + "Failed to add column in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * deleteColumn - Utility function to delete column + * + * @param {number} currentCol The number of the column to delete + */ + deleteColumn: function (currentCol) { + try { + let data = this.getData(); + // Don't allow deletion of the last column + if (data[0].length <= 2) { + this.resetData(); + return; } - }, - - /** - * sortColumn - Utility function to sort columns - * - * @param {number} currentCol The column number of the column to delete - */ - sortColumn: function(currentCol) { - try { - let spreadSheetData = this.getData(); - let data = spreadSheetData.slice(1); - let headers = spreadSheetData.slice(0, 1)[0]; - if (!data.some(a => a[currentCol] !== "")) return; - if (this.sortingHistory.has(currentCol)) { - const sortOrder = this.sortingHistory.get(currentCol); - switch (sortOrder) { - case "desc": - data.sort(this.ascSort.bind(this, currentCol)); - this.sortingHistory.set(currentCol, "asc"); - break; - case "asc": - data.sort(this.dscSort.bind(this, currentCol)); - this.sortingHistory.set(currentCol, "desc"); - break; - } - } else { - data.sort(this.ascSort.bind(this, currentCol)); - this.sortingHistory.set(currentCol, "asc"); - } - data.splice(0, 0, headers); - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log("Failed to sort column in the Table Editor View, error message: " + e); + for (let i = 0; i <= this.rowCount; i++) { + data[i].splice(currentCol, 1); } - }, - - /** - * ascSort - Compare Functions for sorting - ascending - * - * @param {number} currentCol The number of the column to sort - * @param {*} a One of two items to compare - * @param {*} b The second of two items to compare - * @return {number} A number indicating the order to place a vs b in the list. It it returns less than zero, then a will be placed before b in the list. - */ - ascSort: function(currentCol, a, b) { - try { - let _a = a[currentCol]; - let _b = b[currentCol]; - if (_a === "") return 1; - if (_b === "") return -1; - - // Check for strings and numbers - if (isNaN(_a) || isNaN(_b)) { - _a = _a.toUpperCase(); - _b = _b.toUpperCase(); - if (_a < _b) return -1; - if (_a > _b) return 1; - return 0; + this.colCount--; + this.saveData(data); + this.createSpreadsheet(); + } catch (e) { + console.log( + "Failed to delete column in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * sortColumn - Utility function to sort columns + * + * @param {number} currentCol The column number of the column to delete + */ + sortColumn: function (currentCol) { + try { + let spreadSheetData = this.getData(); + let data = spreadSheetData.slice(1); + let headers = spreadSheetData.slice(0, 1)[0]; + if (!data.some((a) => a[currentCol] !== "")) return; + if (this.sortingHistory.has(currentCol)) { + const sortOrder = this.sortingHistory.get(currentCol); + switch (sortOrder) { + case "desc": + data.sort(this.ascSort.bind(this, currentCol)); + this.sortingHistory.set(currentCol, "asc"); + break; + case "asc": + data.sort(this.dscSort.bind(this, currentCol)); + this.sortingHistory.set(currentCol, "desc"); + break; } - return _a - _b; - } catch (e) { - console.log("The ascending compare function in Table Editor View failed, error message: " + e); + } else { + data.sort(this.ascSort.bind(this, currentCol)); + this.sortingHistory.set(currentCol, "asc"); + } + data.splice(0, 0, headers); + this.saveData(data); + this.createSpreadsheet(); + } catch (e) { + console.log( + "Failed to sort column in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * ascSort - Compare Functions for sorting - ascending + * + * @param {number} currentCol The number of the column to sort + * @param {*} a One of two items to compare + * @param {*} b The second of two items to compare + * @return {number} A number indicating the order to place a vs b in the list. It it returns less than zero, then a will be placed before b in the list. + */ + ascSort: function (currentCol, a, b) { + try { + let _a = a[currentCol]; + let _b = b[currentCol]; + if (_a === "") return 1; + if (_b === "") return -1; + + // Check for strings and numbers + if (isNaN(_a) || isNaN(_b)) { + _a = _a.toUpperCase(); + _b = _b.toUpperCase(); + if (_a < _b) return -1; + if (_a > _b) return 1; return 0; } - }, - - /** - * dscSort - Descending compare function - * - * @param {number} currentCol The number of the column to sort - * @param {*} a One of two items to compare - * @param {*} b The second of two items to compare - * @return {number} A number indicating the order to place a vs b in the list. It it returns less than zero, then a will be placed before b in the list. - */ - dscSort: function(currentCol, a, b) { - try { - let _a = a[currentCol]; - let _b = b[currentCol]; - if (_a === "") return 1; - if (_b === "") return -1; - - // Check for strings and numbers - if (isNaN(_a) || isNaN(_b)) { - _a = _a.toUpperCase(); - _b = _b.toUpperCase(); - if (_a < _b) return 1; - if (_a > _b) return -1; - return 0; - } - return _b - _a; - } catch (e) { - console.log("The descending compare function in Table Editor View failed, error message: " + e); + return _a - _b; + } catch (e) { + console.log( + "The ascending compare function in Table Editor View failed, error message: " + + e, + ); + return 0; + } + }, + + /** + * dscSort - Descending compare function + * + * @param {number} currentCol The number of the column to sort + * @param {*} a One of two items to compare + * @param {*} b The second of two items to compare + * @return {number} A number indicating the order to place a vs b in the list. It it returns less than zero, then a will be placed before b in the list. + */ + dscSort: function (currentCol, a, b) { + try { + let _a = a[currentCol]; + let _b = b[currentCol]; + if (_a === "") return 1; + if (_b === "") return -1; + + // Check for strings and numbers + if (isNaN(_a) || isNaN(_b)) { + _a = _a.toUpperCase(); + _b = _b.toUpperCase(); + if (_a < _b) return 1; + if (_a > _b) return -1; return 0; } - }, - - - /** - * convertToMarkdown - Returns the table data as markdown - * - * @return {string} The markdownified table as string - */ - getMarkdown: function() { - try { - // Ensure there are at least two dashes below the table header, - // i.e. use | -- | not | - | - // Showdown requries this to avoid ambiguous markdown. - const minStringLength = function(s) { - l = s.length <= 1 ? 2 : s.length; - return l - } - // Get the current table data - var tableData = this.getData(); - // Remove the empty column that we use for row numbers first - if (this.hasEmptyCol1(tableData)) { - for (let i = 0; i <= (tableData.length - 1); i++) { - tableData[i].splice(0, 1); - } + return _b - _a; + } catch (e) { + console.log( + "The descending compare function in Table Editor View failed, error message: " + + e, + ); + return 0; + } + }, + + /** + * convertToMarkdown - Returns the table data as markdown + * + * @return {string} The markdownified table as string + */ + getMarkdown: function () { + try { + // Ensure there are at least two dashes below the table header, + // i.e. use | -- | not | - | + // Showdown requries this to avoid ambiguous markdown. + const minStringLength = function (s) { + l = s.length <= 1 ? 2 : s.length; + return l; + }; + // Get the current table data + var tableData = this.getData(); + // Remove the empty column that we use for row numbers first + if (this.hasEmptyCol1(tableData)) { + for (let i = 0; i <= tableData.length - 1; i++) { + tableData[i].splice(0, 1); } - // Convert json data to markdown, for options see https://github.com/wooorm/markdown-table - // TODO: Add alignment information that we will store in view as an array - // Include in markdownTableFromJson() options like this - align: ['l', 'c', 'r'] - var markdown = markdownTableFromJson(tableData, { - stringLength: minStringLength - }); - // Add a new line to the end - return markdown + "\n"; - } catch (e) { - console.log("Failed to convert json to markdown in the Table Editor View, error message: " + e); - return ""; } - }, - - /** - * getJSONfromMarkdown - Converts a given markdown table string to JSON. - * - * @param {string} markdown description - * @return {Array} The markdown table as an array of arrays, where the header is the first array and each row is an array that follows. - */ - getJSONfromMarkdown: function(markdown) { - try { - parsedMarkdown = markdownTableToJson(markdown); - if (!parsedMarkdown) return; - // TODO: Add alignment information to the view, returned as parsedMarkdown.align - return parsedMarkdown.table; - } catch (e) { - console.log("Failed to parse markdown in the Table Editor View, error message: " + e); - return []; - } - }, - - /** - * hasEmptyCol1 - Checks whether the first column is empty. - * - * @param {Object} data The table data in the form of an array of arrays - * @return {boolean} returns true if the first column is empty, false if at least one cell in the first column contains a value - */ - hasEmptyCol1: function(data) { - try { - var firstColEmpty = true; - // Check if the first item in each row is blank - for (let i = 0; i <= (data.length - 1); i++) { - if (data[i][0] != "") { - firstColEmpty = false; - break; - } + // Convert json data to markdown, for options see https://github.com/wooorm/markdown-table + // TODO: Add alignment information that we will store in view as an array + // Include in markdownTableFromJson() options like this - align: ['l', 'c', 'r'] + var markdown = markdownTableFromJson(tableData, { + stringLength: minStringLength, + }); + // Add a new line to the end + return markdown + "\n"; + } catch (e) { + console.log( + "Failed to convert json to markdown in the Table Editor View, error message: " + + e, + ); + return ""; + } + }, + + /** + * getJSONfromMarkdown - Converts a given markdown table string to JSON. + * + * @param {string} markdown description + * @return {Array} The markdown table as an array of arrays, where the header is the first array and each row is an array that follows. + */ + getJSONfromMarkdown: function (markdown) { + try { + parsedMarkdown = markdownTableToJson(markdown); + if (!parsedMarkdown) return; + // TODO: Add alignment information to the view, returned as parsedMarkdown.align + return parsedMarkdown.table; + } catch (e) { + console.log( + "Failed to parse markdown in the Table Editor View, error message: " + + e, + ); + return []; + } + }, + + /** + * hasEmptyCol1 - Checks whether the first column is empty. + * + * @param {Object} data The table data in the form of an array of arrays + * @return {boolean} returns true if the first column is empty, false if at least one cell in the first column contains a value + */ + hasEmptyCol1: function (data) { + try { + var firstColEmpty = true; + // Check if the first item in each row is blank + for (let i = 0; i <= data.length - 1; i++) { + if (data[i][0] != "") { + firstColEmpty = false; + break; } - return firstColEmpty; - } catch (e) { - console.log("Failed to detect if there's an empty first column in the Table Editor View. Assuming the first column has data, but this could cause some issues. Error message: " + e); - return false; } - }, - - /** - * closeDropdown - Close the dropdown menu if the user clicks outside of it - * - * @param {type} e The event that triggered this function - */ - closeDropdown: function(e) { - try { - if (!e.target.matches(".dropbtn") || !e) { - var dropdowns = document.getElementsByClassName("dropdown-content"); - var i; - for (i = 0; i < dropdowns.length; i++) { - var openDropdown = dropdowns[i]; - if (openDropdown.classList.contains("show")) { - openDropdown.classList.remove("show"); - } + return firstColEmpty; + } catch (e) { + console.log( + "Failed to detect if there's an empty first column in the Table Editor View. Assuming the first column has data, but this could cause some issues. Error message: " + + e, + ); + return false; + } + }, + + /** + * closeDropdown - Close the dropdown menu if the user clicks outside of it + * + * @param {type} e The event that triggered this function + */ + closeDropdown: function (e) { + try { + if (!e.target.matches(".dropbtn") || !e) { + var dropdowns = document.getElementsByClassName("dropdown-content"); + var i; + for (i = 0; i < dropdowns.length; i++) { + var openDropdown = dropdowns[i]; + if (openDropdown.classList.contains("show")) { + openDropdown.classList.remove("show"); } } - } catch (e) { - console.log("Failed to close a dropdown menu in the Table Editor View, error message: " + e); } - }, - - /** - * handleHeadersClick - Called when the table header is clicked. Depending - * on what is clicked, shows or hides the dropdown menus in the header, - * or calls one of the functions listed in the menu (e.g. delete column). - * - * @param {event} e The event that triggered this function - */ - handleHeadersClick: function(e) { - try { - var view = this; - if (e.target) { - - var classes = e.target.classList; - - if (classes.contains("column-header-span")) { - // If the header element is clicked... - } else if (classes.contains("dropbtn")) { - const idArr = e.target.id.split("-"); - document - .getElementById(`col-dropdown-${idArr[2]}`) - .classList.toggle("show"); - } else if (classes.contains("col-dropdown-option")) { - - const index = e.target.parentNode.id.split("-")[2]; - - if (classes.contains("col-insert-left")) { - view.addColumn(index, "left"); - } else if (classes.contains("col-insert-right")) { - view.addColumn(index, "right"); - } else if (classes.contains("col-sort")) { - view.sortColumn(index); - } else if (classes.contains("col-delete")) { - view.deleteColumn(index); - } + } catch (e) { + console.log( + "Failed to close a dropdown menu in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * handleHeadersClick - Called when the table header is clicked. Depending + * on what is clicked, shows or hides the dropdown menus in the header, + * or calls one of the functions listed in the menu (e.g. delete column). + * + * @param {event} e The event that triggered this function + */ + handleHeadersClick: function (e) { + try { + var view = this; + if (e.target) { + var classes = e.target.classList; + + if (classes.contains("column-header-span")) { + // If the header element is clicked... + } else if (classes.contains("dropbtn")) { + const idArr = e.target.id.split("-"); + document + .getElementById(`col-dropdown-${idArr[2]}`) + .classList.toggle("show"); + } else if (classes.contains("col-dropdown-option")) { + const index = e.target.parentNode.id.split("-")[2]; + + if (classes.contains("col-insert-left")) { + view.addColumn(index, "left"); + } else if (classes.contains("col-insert-right")) { + view.addColumn(index, "right"); + } else if (classes.contains("col-sort")) { + view.sortColumn(index); + } else if (classes.contains("col-delete")) { + view.deleteColumn(index); } - } - } catch (e) { - console.log("Failed to handle a click in the table header in the Table Editor View, error message: " + e); } - }, - - /** - * handleHeadersClick - Called when the table body is clicked. Depending - * on what is clicked, shows or hides the dropdown menus in the body, - * or calls one of the functions listed in the menu (e.g. delete row). - * - * @param {type} e description - * @return {type} description - */ - handleBodyClick: function(e) { - try { - var view = this; - if (e.target) { - - var classes = e.target.classList; - - if (classes.contains("dropbtn")) { - const idArr = e.target.id.split("-"); - view.$el.find(`#row-dropdown-${idArr[2]}`)[0] - .classList.toggle("show"); - } else if (classes.contains("row-dropdown-option")) { - const index = parseInt(e.target.parentNode.id.split("-"))[2]; - if (classes.contains("row-insert-top")) { - view.addRow(index, "top"); - } - if (classes.contains("row-insert-bottom")) { - view.addRow(index, "bottom"); - } - if (classes.contains("row-delete")) { - view.deleteRow(index); - } + } catch (e) { + console.log( + "Failed to handle a click in the table header in the Table Editor View, error message: " + + e, + ); + } + }, + + /** + * handleHeadersClick - Called when the table body is clicked. Depending + * on what is clicked, shows or hides the dropdown menus in the body, + * or calls one of the functions listed in the menu (e.g. delete row). + * + * @param {type} e description + * @return {type} description + */ + handleBodyClick: function (e) { + try { + var view = this; + if (e.target) { + var classes = e.target.classList; + + if (classes.contains("dropbtn")) { + const idArr = e.target.id.split("-"); + view.$el + .find(`#row-dropdown-${idArr[2]}`)[0] + .classList.toggle("show"); + } else if (classes.contains("row-dropdown-option")) { + const index = parseInt(e.target.parentNode.id.split("-"))[2]; + if (classes.contains("row-insert-top")) { + view.addRow(index, "top"); + } + if (classes.contains("row-insert-bottom")) { + view.addRow(index, "bottom"); + } + if (classes.contains("row-delete")) { + view.deleteRow(index); } } - } catch (e) { - console.log("Failed to handle a click in the table body in the Table Editor View, error message: " + e); } + } catch (e) { + console.log( + "Failed to handle a click in the table body in the Table Editor View, error message: " + + e, + ); } + }, + }, + ); - }); - - return TableEditorView; - - }); + return TableEditorView; +});
diff --git a/docs/docs/src_js_views_UserGroupView.js.html b/docs/docs/src_js_views_UserGroupView.js.html index 307a972ea..d19041553 100644 --- a/docs/docs/src_js_views_UserGroupView.js.html +++ b/docs/docs/src_js_views_UserGroupView.js.html @@ -44,285 +44,354 @@

Source: src/js/views/UserGroupView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'collections/UserGroup', 'views/GroupListView', 'text!templates/userGroup.html', 'text!templates/alert.html'],
-	function($, _, Backbone, UserGroup, GroupListView, Template, AlertTemplate) {
-		'use strict';
-
-		/**
-		 * @class UserGroupView
-		 * @classdesc A subview that displays group management. View controls creation of groups and addition
-		 * of members to groups
-		 * @classcategory Views
-		 * @screenshot views/UserGroupView.png
-		 * @extends Backbone.View
-		 */
-		var UserGroupView = Backbone.View.extend(
-			/** @lends UserGroupView.prototype */ {
-
-			tagName: "div",
-			className: "span8 subsection",
-			attributes: {'data-section': 'groups'},
-			type: "UserGroupView",
-			events: {
-				"blur #add-group-name"    : "checkGroupName",
-				"click #add-group-submit" : "createGroup"
-			},
-
-			template: _.template(Template),
-			alertTemplate: _.template(AlertTemplate),
-
-			initialize: function(options){
-				if((typeof options == "undefined"))
-					var options = {};
-				this.model = options.model;
-				this.subviews = new Array();
-			},
-
-			render: function(){
-				this.$el.html(this.template());
-				this.insertCreateGroupForm();
-				this.listenTo(this.model, "change:isMemberOf", this.getGroups);
-				this.getGroups();
-				this.delegateEvents();
-				return this;
-			},
-
-				/**
-				 * Creates a view of a list of groups that the User is a member of
-				 *
-				 * @param {UserGroup} userGroup A user group model
-				 * @param {Object} viewOptions an object of options for the view
-				 */
-				createGroupList: function(userGroup, viewOptions) {
-				//Only create a list for new groups that aren't yet on the page
-				var existingGroupLists = _.where(this.subviews, {type: "GroupListView"});
-				if(existingGroupLists)
-					var groupIds = _.pluck(existingGroupLists, "groupId");
-				if(groupIds && (_.contains(groupIds, userGroup.groupId)))
-					return;
-
-				//Create a list of the view options
-				if(typeof viewOptions == "object")
-					viewOptions.collection = userGroup;
-				else
-					viewOptions = { collection: userGroup };
-
-				//Create the view and save it as a subview
-				var groupView = new GroupListView(viewOptions);
-				this.subviews.push(groupView);
-
-				//Collapse the views if need be
-				if((userGroup.get("isMemberOf") && (userGroup.get("isMemberOf").length > 3)) || (userGroup.length > 3))
-					groupView.collapseMemberList();
-
-				//Finally, render it and return
-				return groupView.render().el;
-			},
-
-				/**
-				 * Gets the groups the this user is a part of and creates a UserGroup collection for each
-				 */
-				getGroups: function(){
-				var view = this,
-					groups = [],
-					model = this.model;
-				//Create a group Collection for each group this user is a member of
-				_.each(_.sortBy(model.get("isMemberOf"), "name"), function(group){
-					var userGroup = new UserGroup([model], group);
-					groups.push(userGroup);
-
-					view.listenTo(userGroup, "sync", function(){
-						var list = this.createGroupList(userGroup);
-						this.$("#group-list-container").append(list);
-					});
-					userGroup.getGroup();
-				});
-			},
-
-				/**
-				 * Inserts a new form for this user to create a new group.
-				 * The form container is grabbed from the template
-				 */
-				insertCreateGroupForm: function(){
-				//Reset the form
-				$("#add-group-form-container").find("input[type='text']").val("").removeClass("has-error");
-				$("#group-name-notification-container").empty().removeClass("notification success error");
-
-				//Create a pending group that is stored locally until the user submits it
-				this.pendingGroup = new UserGroup([this.model], { pending: true });
-				var groupView = new GroupListView({ collection: this.pendingGroup });
-				this.subviews.push(groupView)
-				groupView.setElement(this.$("#add-group-container .member-list"));
-				groupView.render();
-			},
-
-				/**
-				 * Returns a container that includes a view of the user's group membership
-				 * @param {UserGroup[]} groups An array of UserGroup models
-				 * @param {String} listContainer An html string template of a container that will get appended
-				 * @returns {String} HTML string filled with groups information
-				 */
-				insertMembership: function(groups, listContainer){
-				var	model  = this.model,
-					list   = $(document.createElement("ul")).addClass("list-group member-list"),
-					listHeader = $(document.createElement("h5")).addClass("list-group-item list-group-header").text("Member of " + groups.length + " groups")
-
-				_.each(groups, function(group, i){
-					var name = group.name || "Group",
-						listItem = $(document.createElement("li")).addClass("list-group-item"),
-						groupLink = group.groupId? $(document.createElement("a")).attr("href", MetacatUI.root + "/profile/" + group.groupId).text(name).appendTo(listItem) : "<a></a>";
-
-					$(list).append(listItem);
-				});
-
-				if(this.model.get("username") == MetacatUI.appUserModel.get("username")){
-					var link = $(document.createElement("a")).attr("href", MetacatUI.root + "/profile/" + MetacatUI.appUserModel.get("username") + "/s=settings/s=groups").text("Create New Group"),
-						icon = $(document.createElement("i")).addClass("icon icon-on-left icon-plus"),
-						listItem = $(document.createElement("li")).addClass("list-group-item create-group").append( $(link).prepend(icon) );
-
-					$(list).append(listItem);
-				}
-
-				listContainer.html(list);
-				list.before(listHeader);
-				return listContainer;
-			},
-
-				/**
-				 * Will send a request for info about this user and their groups, and redraw the group lists.
-				 * Will also reset the "Create New Group" form
-				 */
-				refreshGroupLists: function(){
-				this.insertCreateGroupForm();
-				this.model.getInfo();
-			},
-
-
-				/**
-				 * Gets the group name the user has entered and attempts to get this group from the server
-				 * If no group is found, then the group name is marked as available. Otherwise an error msg is displayed
-				 * @param {Event} e
-				 */
-				checkGroupName: function(e){
-				if(!e || !e.target) return;
-
-				var view = this,
-					$notification = $("#group-name-notification-container"),
-					$input = $(e.target);
-
-				//Get the name typed in by the user
-				var name = $input.val().trim();
-				if(!name) return;
-
-				this.listenToOnce(this.pendingGroup, "nameChecked", function(collection){
-					//If the group name/id is available, then display so
-					if(collection.nameAvailable){
-						var icon = $(document.createElement("i")).addClass("icon icon-ok"),
-							message = "The name " + collection.name + " is available",
-							container = $(document.createElement("div")).addClass("notification success");
-
-						$notification.html($(container).append(icon, message));
-						$input.removeClass("has-error");
-					}
-					else{
-						var icon = $(document.createElement("i")).addClass("icon icon-remove"),
-							message = "The name " + collection.name + " is already taken",
-							container = $(document.createElement("div")).addClass("notification error");
-
-						$notification.html($(container).append(icon, message));
-						$input.addClass("has-error");
-					}
-
-				});
-
-				this.pendingGroup.checkName(name);
-			},
-
-				/**
-				 * Syncs the pending group with the server
-				 * @param {Event} e
-				 */
-				createGroup: function(e){
-				e.preventDefault();
-
-				//If there is no name specified, give warning
-				if(!this.pendingGroup.name){
-					var $notification = $("#group-name-notification-container"),
-						$input = $("#add-group-name");
-
-					var icon = $(document.createElement("i")).addClass("icon icon-exclamation"),
-						message = "You must enter a group name",
-						container = $(document.createElement("div")).addClass("notification error");
-
-					$notification.html($(container).append(icon, message));
-					$input.addClass("has-error");
-
-					return;
-				}
-				//If this name is not available, exit
-				else if(this.pendingGroup.nameAvailable == false) return;
-
-				var view = this,
-					group = this.pendingGroup;
-				var success = function(data){
-					view.showAlert("Success! Your group has been saved. View it <a href='" + MetacatUI.root + "/profile/" + group.groupId + "'>here</a>", "alert-success", "#add-group-alert-container");
-					view.refreshGroupLists();
-				}
-				var error = function(xhr){
-					var response = xhr? $.parseHTML(xhr.responseText) : null,
-						description = "";
-					if(response && response.length)
-						description = $(response).find("description").text();
-
-					if(description) description = "(" + description + ").";
-					else description = "";
-
-					view.showAlert("Your group could not be created. " + description + " Please try again.", "alert-error", "#add-group-alert-container")
-				}
-
-				//Create it!
-				if(!this.pendingGroup.save(success, error))
-					error();
-			},
-
-				/**
-				 * Displays an alert message to the user
-				 * @param {String} msg Message of the alert
-				 * @param {String} classes A class tag
-				 * @param {String} container The container tag
-				 */
-				showAlert: function(msg, classes, container) {
-				if(!classes)
-					var classes = 'alert-success';
-				if(!container || !$(container).length)
-					var container = this.$el;
-
-				//Remove any alerts that are already in this container
-				if($(container).children(".alert-container").length > 0)
-					$(container).children(".alert-container").remove();
-
-				$(container).prepend(
-					this.alertTemplate({
-						msg: msg,
-						classes: classes
-					})
-				);
-			},
-
-				/**
-				 * Closes the view and clears any subviews
-				 */
-				onClose: function() {
-				this.$el.html("");
-				this.stopListening(this.model)
-				this.subviews = new Array();
-			},
-
-		});
-
-		return UserGroupView;
-	});
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/UserGroup",
+  "views/GroupListView",
+  "text!templates/userGroup.html",
+  "text!templates/alert.html",
+], function (
+  $,
+  _,
+  Backbone,
+  UserGroup,
+  GroupListView,
+  Template,
+  AlertTemplate,
+) {
+  "use strict";
+
+  /**
+   * @class UserGroupView
+   * @classdesc A subview that displays group management. View controls creation of groups and addition
+   * of members to groups
+   * @classcategory Views
+   * @screenshot views/UserGroupView.png
+   * @extends Backbone.View
+   */
+  var UserGroupView = Backbone.View.extend(
+    /** @lends UserGroupView.prototype */ {
+      tagName: "div",
+      className: "span8 subsection",
+      attributes: { "data-section": "groups" },
+      type: "UserGroupView",
+      events: {
+        "blur #add-group-name": "checkGroupName",
+        "click #add-group-submit": "createGroup",
+      },
+
+      template: _.template(Template),
+      alertTemplate: _.template(AlertTemplate),
+
+      initialize: function (options) {
+        if (typeof options == "undefined") var options = {};
+        this.model = options.model;
+        this.subviews = new Array();
+      },
+
+      render: function () {
+        this.$el.html(this.template());
+        this.insertCreateGroupForm();
+        this.listenTo(this.model, "change:isMemberOf", this.getGroups);
+        this.getGroups();
+        this.delegateEvents();
+        return this;
+      },
+
+      /**
+       * Creates a view of a list of groups that the User is a member of
+       *
+       * @param {UserGroup} userGroup A user group model
+       * @param {Object} viewOptions an object of options for the view
+       */
+      createGroupList: function (userGroup, viewOptions) {
+        //Only create a list for new groups that aren't yet on the page
+        var existingGroupLists = _.where(this.subviews, {
+          type: "GroupListView",
+        });
+        if (existingGroupLists)
+          var groupIds = _.pluck(existingGroupLists, "groupId");
+        if (groupIds && _.contains(groupIds, userGroup.groupId)) return;
+
+        //Create a list of the view options
+        if (typeof viewOptions == "object") viewOptions.collection = userGroup;
+        else viewOptions = { collection: userGroup };
+
+        //Create the view and save it as a subview
+        var groupView = new GroupListView(viewOptions);
+        this.subviews.push(groupView);
+
+        //Collapse the views if need be
+        if (
+          (userGroup.get("isMemberOf") &&
+            userGroup.get("isMemberOf").length > 3) ||
+          userGroup.length > 3
+        )
+          groupView.collapseMemberList();
+
+        //Finally, render it and return
+        return groupView.render().el;
+      },
+
+      /**
+       * Gets the groups the this user is a part of and creates a UserGroup collection for each
+       */
+      getGroups: function () {
+        var view = this,
+          groups = [],
+          model = this.model;
+        //Create a group Collection for each group this user is a member of
+        _.each(_.sortBy(model.get("isMemberOf"), "name"), function (group) {
+          var userGroup = new UserGroup([model], group);
+          groups.push(userGroup);
+
+          view.listenTo(userGroup, "sync", function () {
+            var list = this.createGroupList(userGroup);
+            this.$("#group-list-container").append(list);
+          });
+          userGroup.getGroup();
+        });
+      },
+
+      /**
+       * Inserts a new form for this user to create a new group.
+       * The form container is grabbed from the template
+       */
+      insertCreateGroupForm: function () {
+        //Reset the form
+        $("#add-group-form-container")
+          .find("input[type='text']")
+          .val("")
+          .removeClass("has-error");
+        $("#group-name-notification-container")
+          .empty()
+          .removeClass("notification success error");
+
+        //Create a pending group that is stored locally until the user submits it
+        this.pendingGroup = new UserGroup([this.model], { pending: true });
+        var groupView = new GroupListView({ collection: this.pendingGroup });
+        this.subviews.push(groupView);
+        groupView.setElement(this.$("#add-group-container .member-list"));
+        groupView.render();
+      },
+
+      /**
+       * Returns a container that includes a view of the user's group membership
+       * @param {UserGroup[]} groups An array of UserGroup models
+       * @param {String} listContainer An html string template of a container that will get appended
+       * @returns {String} HTML string filled with groups information
+       */
+      insertMembership: function (groups, listContainer) {
+        var model = this.model,
+          list = $(document.createElement("ul")).addClass(
+            "list-group member-list",
+          ),
+          listHeader = $(document.createElement("h5"))
+            .addClass("list-group-item list-group-header")
+            .text("Member of " + groups.length + " groups");
+
+        _.each(groups, function (group, i) {
+          var name = group.name || "Group",
+            listItem = $(document.createElement("li")).addClass(
+              "list-group-item",
+            ),
+            groupLink = group.groupId
+              ? $(document.createElement("a"))
+                  .attr("href", MetacatUI.root + "/profile/" + group.groupId)
+                  .text(name)
+                  .appendTo(listItem)
+              : "<a></a>";
+
+          $(list).append(listItem);
+        });
+
+        if (
+          this.model.get("username") == MetacatUI.appUserModel.get("username")
+        ) {
+          var link = $(document.createElement("a"))
+              .attr(
+                "href",
+                MetacatUI.root +
+                  "/profile/" +
+                  MetacatUI.appUserModel.get("username") +
+                  "/s=settings/s=groups",
+              )
+              .text("Create New Group"),
+            icon = $(document.createElement("i")).addClass(
+              "icon icon-on-left icon-plus",
+            ),
+            listItem = $(document.createElement("li"))
+              .addClass("list-group-item create-group")
+              .append($(link).prepend(icon));
+
+          $(list).append(listItem);
+        }
+
+        listContainer.html(list);
+        list.before(listHeader);
+        return listContainer;
+      },
+
+      /**
+       * Will send a request for info about this user and their groups, and redraw the group lists.
+       * Will also reset the "Create New Group" form
+       */
+      refreshGroupLists: function () {
+        this.insertCreateGroupForm();
+        this.model.getInfo();
+      },
+
+      /**
+       * Gets the group name the user has entered and attempts to get this group from the server
+       * If no group is found, then the group name is marked as available. Otherwise an error msg is displayed
+       * @param {Event} e
+       */
+      checkGroupName: function (e) {
+        if (!e || !e.target) return;
+
+        var view = this,
+          $notification = $("#group-name-notification-container"),
+          $input = $(e.target);
+
+        //Get the name typed in by the user
+        var name = $input.val().trim();
+        if (!name) return;
+
+        this.listenToOnce(
+          this.pendingGroup,
+          "nameChecked",
+          function (collection) {
+            //If the group name/id is available, then display so
+            if (collection.nameAvailable) {
+              var icon = $(document.createElement("i")).addClass(
+                  "icon icon-ok",
+                ),
+                message = "The name " + collection.name + " is available",
+                container = $(document.createElement("div")).addClass(
+                  "notification success",
+                );
+
+              $notification.html($(container).append(icon, message));
+              $input.removeClass("has-error");
+            } else {
+              var icon = $(document.createElement("i")).addClass(
+                  "icon icon-remove",
+                ),
+                message = "The name " + collection.name + " is already taken",
+                container = $(document.createElement("div")).addClass(
+                  "notification error",
+                );
+
+              $notification.html($(container).append(icon, message));
+              $input.addClass("has-error");
+            }
+          },
+        );
+
+        this.pendingGroup.checkName(name);
+      },
+
+      /**
+       * Syncs the pending group with the server
+       * @param {Event} e
+       */
+      createGroup: function (e) {
+        e.preventDefault();
+
+        //If there is no name specified, give warning
+        if (!this.pendingGroup.name) {
+          var $notification = $("#group-name-notification-container"),
+            $input = $("#add-group-name");
+
+          var icon = $(document.createElement("i")).addClass(
+              "icon icon-exclamation",
+            ),
+            message = "You must enter a group name",
+            container = $(document.createElement("div")).addClass(
+              "notification error",
+            );
+
+          $notification.html($(container).append(icon, message));
+          $input.addClass("has-error");
+
+          return;
+        }
+        //If this name is not available, exit
+        else if (this.pendingGroup.nameAvailable == false) return;
+
+        var view = this,
+          group = this.pendingGroup;
+        var success = function (data) {
+          view.showAlert(
+            "Success! Your group has been saved. View it <a href='" +
+              MetacatUI.root +
+              "/profile/" +
+              group.groupId +
+              "'>here</a>",
+            "alert-success",
+            "#add-group-alert-container",
+          );
+          view.refreshGroupLists();
+        };
+        var error = function (xhr) {
+          var response = xhr ? $.parseHTML(xhr.responseText) : null,
+            description = "";
+          if (response && response.length)
+            description = $(response).find("description").text();
+
+          if (description) description = "(" + description + ").";
+          else description = "";
+
+          view.showAlert(
+            "Your group could not be created. " +
+              description +
+              " Please try again.",
+            "alert-error",
+            "#add-group-alert-container",
+          );
+        };
+
+        //Create it!
+        if (!this.pendingGroup.save(success, error)) error();
+      },
+
+      /**
+       * Displays an alert message to the user
+       * @param {String} msg Message of the alert
+       * @param {String} classes A class tag
+       * @param {String} container The container tag
+       */
+      showAlert: function (msg, classes, container) {
+        if (!classes) var classes = "alert-success";
+        if (!container || !$(container).length) var container = this.$el;
+
+        //Remove any alerts that are already in this container
+        if ($(container).children(".alert-container").length > 0)
+          $(container).children(".alert-container").remove();
+
+        $(container).prepend(
+          this.alertTemplate({
+            msg: msg,
+            classes: classes,
+          }),
+        );
+      },
+
+      /**
+       * Closes the view and clears any subviews
+       */
+      onClose: function () {
+        this.$el.html("");
+        this.stopListening(this.model);
+        this.subviews = new Array();
+      },
+    },
+  );
+
+  return UserGroupView;
+});
 
diff --git a/docs/docs/src_js_views_UserView.js.html b/docs/docs/src_js_views_UserView.js.html index a4be68ec6..2d01e9947 100644 --- a/docs/docs/src_js_views_UserView.js.html +++ b/docs/docs/src_js_views_UserView.js.html @@ -44,1189 +44,1501 @@

Source: src/js/views/UserView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone', 'clipboard',
-        'collections/UserGroup',
-    		'models/UserModel',
-        "models/Stats",
-		'views/SignInView', 'views/StatsView', 'views/DataCatalogView',
-		'views/UserGroupView',
-        'text!templates/userProfile.html', 'text!templates/alert.html', 'text!templates/loading.html',
-        'text!templates/userProfileMenu.html', 'text!templates/userSettings.html', 'text!templates/noResults.html'],
-	function($, _, Backbone, Clipboard,
-    UserGroup,
-    UserModel, Stats,
-    SignInView, StatsView, DataCatalogView, UserGroupView,
-    userProfileTemplate, AlertTemplate, LoadingTemplate,
-    ProfileMenuTemplate, SettingsTemplate, NoResultsTemplate) {
-	'use strict';
-
-	/**
-	 * @class UserView
-	 * @classdesc A major view that displays a public profile for the user and a settings page for the logged-in user
-	 * to manage their account info, groups, identities, and API tokens.
-     * @classcategory Views
-     * @screenshot views/UserView.png
-	 * @extends Backbone.View
-	 */
-	var UserView = Backbone.View.extend(
-    /** @lends UserView.prototype */{
-
-		el: '#Content',
-
-		//Templates
-		profileTemplate:  _.template(userProfileTemplate),
-		alertTemplate:    _.template(AlertTemplate),
-		loadingTemplate:  _.template(LoadingTemplate),
-		settingsTemplate: _.template(SettingsTemplate),
-		menuTemplate:     _.template(ProfileMenuTemplate),
-		noResultsTemplate: _.template(NoResultsTemplate),
-
-    /**
-    * A jQuery selector for the element that the PortalListView should be inserted into
-    * @type {string}
-    */
-    portalListContainer: ".my-portals-container",
-
-		events: {
-			"click .section-link"          : "switchToSection",
-			"click .subsection-link"       : "switchToSubSection",
-			"click .token-generator"       : "getToken",
-			"click #mod-save-btn"		   : "saveUser",
-			"click #map-request-btn"	   : "sendMapRequest",
-			"click .remove-identity-btn"   : "removeMap",
-			"click .confirm-request-btn"   : "confirmMapRequest",
-			"click .reject-request-btn"	   : "rejectMapRequest",
-			"click [highlight-subsection]" : "highlightSubSection",
-			"keypress #add-group-name"     : "preventSubmit",
-			"click .token-tab" 			   : "switchTabs"
-		},
-
-		initialize: function(){
-			this.subviews = new Array();
-		},
-
-		//------------------------------------------ Rendering the main parts of the view ------------------------------------------------//
-		render: function (options) {
-			//Don't render anything if the user profiles are turned off
-			if( MetacatUI.appModel.get("enableUserProfiles") === false ){
-				return;
-			}
-
-			this.stopListening();
-			if(this.model) this.model.stopListening();
-
-      //Create a Stats model
-      this.statsModel = new Stats();
-
-			this.activeSection = (options && options.section)? options.section : "profile";
-			this.activeSubSection = (options && options.subsection)? options.subsection : "";
-			this.username = (options && options.username)? options.username : undefined;
-
-			//Add the container element for our profile sections
-			this.sectionHolder = $(document.createElement("section")).addClass("user-view-section");
-			this.$el.html(this.sectionHolder);
-
-			//Show the loading sign first
-			//$(this.sectionHolder).html(this.loadingTemplate());
-			this.$el.show();
-
-			// set the header type
-			MetacatUI.appModel.set('headerType', 'default');
-
-			//Render the user profile only after the app user's info has been checked
-			//This prevents the app from rendering the profile before the login process has completed - which would
-			//cause this profile to render twice (first before the user is logged in then again after they log in)
-			if(MetacatUI.appUserModel.get("checked")) this.renderUser();
-			else MetacatUI.appUserModel.on("change:checked", this.renderUser, this);
-
-			return this;
-		},
-
-		/**
-		 * Update the window location path to route to /portals path
-		 * @param {string} username - Short identifier for the member node
-		*/
-		forwardToPortals: function(username){
-
-			var pathName      = decodeURIComponent(window.location.pathname)
-								.substring(MetacatUI.root.length)
-								// remove trailing forward slash if one exists in path
-								.replace(/\/$/, "");
-
-			// Routes the /profile/{node-id} to /portals/{node-id}
-			var pathRE = new RegExp("\\/profile(\\/[^\\/]*)?$", "i");
-			var newPathName = pathName.replace(pathRE, "") + "/" +
-							MetacatUI.appModel.get("portalTermPlural") + "/" + username;
-
-			// Update the window location
-			MetacatUI.uiRouter.navigate( newPathName, { trigger: true, replace: true } );
-			return;
-		},
-
-		renderUser: function(){
-
-
-			this.model = MetacatUI.appUserModel;
-
-			var username = MetacatUI.appModel.get("profileUsername") || view.username,
-				currentUser = MetacatUI.appUserModel.get("username") || "";
-
-			if(username.toUpperCase() == currentUser.toUpperCase()){ //Case-insensitive matching of usernames
-				this.model = MetacatUI.appUserModel;
-				this.model.set("type", "user");
-
-				//If the user is logged in, display the settings options
-				if(this.model.get("loggedIn")){
-					this.insertMenu();
-					this.renderProfile();
-					this.renderSettings();
-					this.resetSections();
-				}
-			}
-
-			//If this isn't the currently-logged in user, then let's find out more info about this account
-			else{
-				//Create a UserModel with the username given
-				this.model = new UserModel({
-					username: username
-				});
-
-				//Is this a member node?
-				if(MetacatUI.nodeModel.get("checked") && this.model.isNode()){
-					this.model.saveAsNode();
-					this.model.set("nodeInfo", _.find(MetacatUI.nodeModel.get("members"), function(nodeModel) {
-						return nodeModel.identifier.toLowerCase() == "urn:node:" + username.toLowerCase();
-					  }));
-					this.forwardToPortals(username);
-					return;
-				}
-				//If the node model hasn't been checked yet
-				else if(!MetacatUI.nodeModel.get("checked")){
-					var user = this.model,
-						view = this;
-					this.listenTo(MetacatUI.nodeModel, "change:checked", function(){
-						if(user.isNode())
-							view.render();
-					});
-				}
-
-				//When we get the infomration about this account, then crender the profile
-				this.model.once("change:checked", this.renderProfile, this);
-				this.model.once("change:checked", this.resetSections, this);
-				//Get the info
-				this.model.getInfo();
-			}
-
-			//When the model is reset, refresh the page
-			this.listenTo(this.model, "reset", this.render);
-
-		},
-
-		renderProfile: function(){
-
-			//Insert the template first
-			var profileEl = $.parseHTML(this.profileTemplate({
-				type: this.model.get("type"),
-				logo: this.model.get("logo") || "",
-				description: this.model.get("description") || "",
-				user: this.model.toJSON()
-			}).trim());
-
-			//If the profile is being redrawn, then replace it
-			if(this.$profile && this.$profile.length){
-				//If the profile section is currently hidden, make sure we hide our new profile rendering too
-				if(!this.$profile.is(":visible"))
-					$(profileEl).hide();
-
-				this.$profile.replaceWith(profileEl);
-			}
-			//If this is a fresh rendering, then append it to the page and save it
-			else
-				this.sectionHolder.append(profileEl);
-
-			this.$profile = $(profileEl);
-
-			//If this user hasn't uploaded anything yet, display so
-			this.listenTo(this.statsModel, "change:totalCount", function(){
-				if(!this.statsModel.get("totalCount"))
-					this.noActivity();
-			});
-
-			//Insert the user data statistics
-			this.insertStats();
-
-			//Insert the user's basic information
-			this.listenTo(this.model, "change:fullName", this.insertUserInfo);
-			this.insertUserInfo();
-
-			var view = this;
-			//Listen to changes in the user's search terms
-			this.listenTo(this.model, "change:searchModel", this.renderProfile);
-
-
-			//Insert this user's data content
-			this.insertContent();
-
-			// create the UserGroupView to generate the membership list
-			// this is the first call to UserGroupView so we instantiate it here
-			var groupView = new UserGroupView({model: this.model});
-			this.subviews.push(groupView);
-			this.renderMembershipList();
-		},
-
-			renderMembershipList: function() {
-				//List the groups this user is in by creating usergroupview subview
-				//List the groups this user is in by creating usergroupview subview
-				var groupView = _.where(this.subviews, {type: "UserGroupView"}).at(0);
-
-				if(this.model.get("type") == "group"){
-					//Create the User Group collection
-					var options = {
-						name: this.model.get("fullName"),
-						groupId: this.model.get("username"),
-						rawData: this.model.get("rawData") || null
-					}
-					var userGroup = new UserGroup([], options);
-					//Create the group list and add it to the page
-					var viewOptions = { collapsable: false, showGroupName: false }
-					var groupList = groupView.createGroupList(userGroup, viewOptions);
-					this.$("#user-membership-container").html(groupList);
-				}
-				else{
-					var groups = _.sortBy(this.model.get("isMemberOf"), "name");
-					if(!groups.length){
-						this.$("#user-membership-header").hide();
-						return;
-					}
-					this.sectionHolder.append(groupView.insertMembership(groups, this.$("#user-membership-container")).html());
-				}
-			},
-
-			renderGroupsSection: function() {
-				var groupView = _.where(this.subviews, {type: "UserGroupView"}).at(0);
-				var container = this.$('#groups-container');
-				container.append(groupView.render().el)
-			},
-
-			renderSettings: function(){
-					//Don't render anything if the user profile settings are turned off
-		if( MetacatUI.appModel.get("enableUserProfileSettings") === false ){
-			return;
-		}
-
-		//Insert the template first
-		this.sectionHolder.append(this.settingsTemplate(this.model.toJSON()));
-		this.$settings = this.$("[data-section='settings']");
-
-		//Draw the group list
-		this.renderGroupsSection();
-
-		//Listen for the identity list
-		this.listenTo(this.model, "change:identities", this.insertIdentityList);
-		this.insertIdentityList();
-
-		//Listen for the pending list
-		this.listenTo(this.model, "change:pending", this.insertPendingList);
-		this.model.getPendingIdentities();
-
-		//Render the portals subsection
-		this.renderMyPortals();
-
-			//Listen for updates to person details
-			this.listenTo(this.model, "change:lastName change:firstName change:email change:registered", this.updateModForm);
-			this.updateModForm();
-
-			// init autocomplete fields
-			this.setUpAutocomplete();
-
-			//Get the token right away
-			this.getToken();
-		},
-
-		/*
-		 * Displays a menu for the user to switch between different views of the user profile
-		 */
-		insertMenu: function(){
-
-			//If the user is not logged in, then remove the menu
-			if(!MetacatUI.appUserModel.get("loggedIn")){
-				this.$(".nav").remove();
-				return;
-			}
-
-			//Otherwise, insert the menu
-			var menu = this.menuTemplate({
-				username: this.model.get("username")
-			});
-
-			this.$el.prepend(menu);
-		},
-
-		//------------------------------------------ Navigating sections of view ------------------------------------------------//
-		switchToSection: function(e, sectionName){
-
-			if(e) e.preventDefault();
-
-			//Hide all the sections first
-			$(this.sectionHolder).children().slideUp().removeClass(".active");
-
-			//Get the section name
-			if(!sectionName)
-				var sectionName = $(e.target).attr("data-section");
-
-			//Display the specified section
-			var activeSection = this.$(".section[data-section='" + sectionName + "']");
-			if(!activeSection.length) activeSection = this.$(".section[data-section='profile']");
-			$(activeSection).addClass("active").slideDown();
-
-			//Change the navigation tabs
-			this.$(".nav-tab").removeClass("active");
-			$(".nav-tab[data-section='" + sectionName + "']").addClass("active");
-
-			//Find all the subsections, if there are any
-			if($(activeSection).find(".subsection").length > 0){
-				//Find any item classified as "active"
-				var activeItem = $(activeSection).find(".active");
-				if(activeItem.length > 0){
-					//Find the data section this active item is referring to
-					if($(activeItem).children("[data-subsection]").length > 0){
-						//Get the section name
-						var subsectionName = $(activeItem).find("[data-subsection]").first().attr("data-subsection");
-						//If we found a section name, find the subsection element and display it
-						if(subsectionName) this.switchToSubSection(null, subsectionName);
-					}
-					else
-						this.switchToSubSection(null, $(activeSection).children("[data-section]").first().attr("data-section"));
-				}
-			}
-		},
-
-		switchToSubSection: function(e, subsectionName){
-			if(e){
-				e.preventDefault();
-			    var subsectionName = $(e.target).attr("data-section");
-          if( !subsectionName ){
-            subsectionName = $(e.target).parents("[data-section]").first().attr("data-section");
-          }
-			}
-
-			//Mark its links as active
-			$(".section.active").find(".subsection-link").removeClass("active");
-			$(".section.active").find(".subsection-link[data-section='" + subsectionName + "']").addClass("active");
-
-			//Hide all the other sections
-			$(".section.active").find(".subsection").hide();
-			$(".section.active").find(".subsection[data-section='" + subsectionName + "']").show();
-		},
-
-		resetSections: function(){
-			//Hide all the sections first, then display the section specified in the URL (or the default)
-			this.$(".subsection, .section").hide();
-			this.switchToSection(null, this.activeSection);
-
-			//Show the subsection
-			if(this.activeSubSection)
-				this.switchToSubSection(null, this.activeSubSection);
-		},
-
-		highlightSubSection: function(e, subsectionName){
-			if(e) e.preventDefault();
-
-			if(!subsectionName && e){
-				//Get the subsection name
-				var subsectionName = $(e.target).attr("highlight-subsection");
-				if(!subsectionName) return;
-			}
-			else if(!subsectionName && !e) return false;
-
-			//Find the subsection
-			var subsection = this.$(".subsection[data-section='" + subsectionName + "']");
-			if(!subsection.length) subsection = this.$("[data-subsection='add-account']");
-			if(!subsection.length) return;
-
-			//Visually highlight the subsection
-			subsection.addClass("highlight");
-			MetacatUI.appView.scrollTo(subsection);
-			//Wait about a second and then remove the highlight style
-			window.setTimeout(function(){ subsection.removeClass("highlight"); }, 1500);
-		},
-
-		//------------------------------------------ Inserting public profile UI elements ------------------------------------------------//
-		insertStats: function(){
-			if(this.model.noActivity && this.statsView){
-				this.statsView.$el.addClass("no-activity");
-				this.$("#total-download-wrapper, section.downloads").hide();
-				return;
-			}
-
-			var username = this.model.get("username"),
-				view = this;
-
-			//Insert a couple stats into the profile
-			this.listenToOnce(this.statsModel, "change:firstUpload", this.insertFirstUpload);
-
-			this.listenToOnce(this.statsModel, "change:totalCount", function(){
-				view.$("#total-upload-container").text(MetacatUI.appView.commaSeparateNumber(view.statsModel.get("totalCount")));
-			});
-
-			//Create a base query for the statistics
-			var statsSearchModel = this.model.get("searchModel").clone();
-			statsSearchModel.set("exclude", [], {silent: true}).set("formatType", [], {silent: true});
-			this.statsModel.set("query", statsSearchModel.getQuery());
-      this.statsModel.set("isSystemMetadataQuery", true);
-			this.statsModel.set("searchModel", statsSearchModel);
-
-			//Create the description for this profile
-			var description;
-
-			switch(this.model.get("type")){
-				case "node":
-					description = "A summary of all datasets from the " + this.model.get("fullName") + " repository";
-					break;
-				case "group":
-					description = "A summary of all datasets from the " + this.model.get("fullName") + " group";
-					break;
-				case "user":
-					description = "A summary of all datasets from " + this.model.get("fullName");
-					break;
-				default:
-					description = "";
-					break;
-			}
-
-			//Render the Stats View for this person
-			this.statsView = new StatsView({
-				title: "Statistics and Figures",
-				description: description,
-				userType: "user",
-				el: this.$("#user-stats"),
-				model: this.statsModel
-			});
-			this.subviews.push(this.statsView);
-			this.statsView.render();
-			if(this.model.noActivity)
-				this.statsView.$el.addClass("no-activity");
-
-		},
-
-		/*
-		 * Insert the name of the user
-		 */
-		insertUserInfo: function(){
-
-			//Don't try to insert anything if we haven't gotten all the user info yet
-			if(!this.model.get("fullName")) return;
-
-			//Insert the name into this page
-			var usernameLink = $(document.createElement("a")).attr("href", MetacatUI.root + "/profile/" + this.model.get("username")).text(this.model.get("fullName"));
-			this.$(".insert-fullname").append(usernameLink);
-
-			//Insert the username
-			if(this.model.get("type") != "node"){
-				if(!this.model.get("usernameReadable")) this.model.createReadableUsername();
-				this.$(".insert-username").text(this.model.get("usernameReadable"));
-			}
-			else{
-				$("#username-wrapper").hide();
-			}
-
-			//Show or hide ORCID logo
-			if(this.model.isOrcid())
-				this.$(".show-orcid").show();
-			else
-				this.$(".show-orcid").hide();
-
-			//Show the email
-			if(this.model.get("email")){
-				this.$(".email-wrapper").show();
-				var parts = this.model.get("email").split("@");
-				this.$(".email-container").attr("data-user", parts[0]);
-				this.$(".email-container").attr("data-domain", parts[1]);
-			}
-			else
-				this.$(".email-wrapper").hide();
-
-		},
-
-		// Creates an HTML element to display in front of the user identity/subject.
-		// Only used for the ORCID logo right now
-		createIdPrefix: function(){
-			if(this.model.isOrcid())
-				return $(document.createElement("img")).attr("src", MetacatUI.root + "/img/orcid_64x64.png").addClass("orcid-logo");
-			else
-				return "";
-		},
-
-		/*
-		 * Insert the first year of contribution for this user
-		 */
-		insertFirstUpload: function(){
-			if(this.model.noActivity || !this.statsModel.get("firstUpload")){
-				this.$("#first-upload-container, #first-upload-year-container").hide();
-				return;
-			}
-
-			// Get the first upload or first operational date
-			if(this.model.get("type") == "node"){
-
-				//Get the member node object
-				var node = _.findWhere(MetacatUI.nodeModel.get("members"), {identifier: "urn:node:" + this.model.get("username") });
-
-				//If there is no memberSince date, then hide this statistic and exit
-				if( !node.memberSince ){
-					this.$("#first-upload-container, #first-upload-year-container").hide();
-					return;
-				}
-				else{
-					var firstUpload = node.memberSince? new Date(node.memberSince.substring(0, node.memberSince.indexOf("T"))) : new Date();
-				}
-
-			}
-			else{
-				var	firstUpload = new Date(this.statsModel.get("firstUpload"));
-			}
-
-			// Construct the first upload date sentence
-			var	monthNames = [ "January", "February", "March", "April", "May", "June",
-				                 "July", "August", "September", "October", "November", "December" ],
-				m = monthNames[firstUpload.getUTCMonth()],
-				y = firstUpload.getUTCFullYear(),
-				d = firstUpload.getUTCDate();
-
-			//For Member Nodes, start all dates at July 2012, the beginning of DataONE
-			if(this.model.get("type") == "node"){
-				this.$("#first-upload-container").text("DataONE Member Node since " + y);
-			}
-			else
-				this.$("#first-upload-container").text("Contributor since " + m + " " + d + ", " + y);
-
-			//Construct the time-elapsed sentence
-			var now = new Date(),
-				msElapsed = now - firstUpload,
-				years = msElapsed / 31556952000,
-				months = msElapsed / 2629746000,
-				weeks = msElapsed / 604800000,
-				days = msElapsed / 86400000,
-				time = "";
-
-			//If one week or less, express in days
-			if(weeks <= 1){
-				time = (Math.round(days) || 1) + " day";
-				if(days > 1.5) time += "s";
-			}
-			//If one month or less, express in weeks
-			else if(months < 1){
-				time = (Math.round(weeks) || 1) + " week";
-				if(weeks > 1.5) time += "s";
-			}
-			//If less than 12 months, express in months
-			else if(months <= 11.5){
-				time = (Math.round(months) || 1) + " month";
-				if(months > 1.5) time += "s";
-			}
-			//If one year or more, express in years and months
-			else{
-				var yearsOnly = (Math.floor(years) || 1),
-					monthsOnly = Math.round(years % 1 * 12);
-
-				if(monthsOnly == 12){
-					yearsOnly += 1;
-					monthsOnly = 0;
-				}
-
-				time = yearsOnly + " year";
-				if(yearsOnly > 1) time += "s";
-
-				if(monthsOnly)
-					time += ", " + monthsOnly + " month";
-				if(monthsOnly > 1) time += "s";
-			}
-
-			this.$("#first-upload-year-container").text(time);
-		},
-
-
-		/*
-		 * Insert a list of this user's content
-		 */
-		insertContent: function(){
-			if(this.model.noActivity){
-				this.$("#data-list").html(this.noResultsTemplate({
-					fullName: this.model.get("fullName"),
-					username: ((this.model == MetacatUI.appUserModel) && MetacatUI.appUserModel.get("loggedIn"))? this.model.get("username") : null
-				}));
-				return;
-			}
-
-			var view = new DataCatalogView({
-				el            : this.$("#data-list")[0],
-				searchModel   : this.model.get("searchModel"),
-				searchResults : this.model.get("searchResults"),
-				mode          : "list",
-				isSubView     : true,
-				filters       : false
-			});
-			this.subviews.push(view);
-			view.render();
-			view.$el.addClass("list-only");
-			view.$(".auto-height").removeClass("auto-height").css("height", "auto");
-			$("#metacatui-app").removeClass("DataCatalog mapMode");
-		},
-
-		/*
-		 * When this user has not uploaded any content, render the profile differently
-		 */
-		noActivity: function(){
-			this.model.noActivity = true;
-			this.insertContent();
-			this.insertFirstUpload();
-			this.insertStats();
-		},
-
-		//------------------------------------------------ Identities/Accounts -------------------------------------------------------//
-			/*
-             * Sends a new identity map request and displays notifications about the result
-             */
-		sendMapRequest: function(e) {
-			e.preventDefault();
-
-			//Get the identity entered into the input
-			var equivalentIdentity = this.$("#map-request-field").val();
-			if (!equivalentIdentity || equivalentIdentity.length < 1) {
-				return;
-			}
-			//Clear the text input
-			this.$("#map-request-field").val("");
-
-			//Show notifications after the identity map request is a success or failure
-			var viewRef = this,
-				success = function(){
-					var message = "An account map request has been sent to <a href=" + MetacatUI.root + "'/profile/" + equivalentIdentity + "'>" + equivalentIdentity + "</a>" +
-					  "<h4>Next step:</h4><p>Sign In with this other account and approve this request.</p>"
-					viewRef.showAlert(message, null, "#request-alert-container");
-				},
-				error = function(xhr){
-					var errorMessage = xhr.responseText;
-
-					if( xhr.responseText.indexOf("Request already issued") > -1 ){
-							viewRef.showAlert("<p>You have already sent a request to map this account to " + equivalentIdentity +
-							".</p> <h4>Next Step:</h4><p> Sign In with your " + equivalentIdentity + " account and approve the request.</p>",
-							'alert-info', "#request-alert-container");
-					}
-					else{
-
-						//Make a more understandable error message when the account isn't found
-						if(xhr.responseText.indexOf("LDAP: error code 32 - No Such Object") > -1){
-							xhr.responseText = "The username " + equivalentIdentity + " does not exist in our system."
-						}
-
-					viewRef.showAlert(xhr.responseText, 'alert-error', "#request-alert-container");
-					}
-				};
-
-			//Send it
-			this.model.addMap(equivalentIdentity, success, error);
-		},
-
-		/*
-		 * Removes a confirmed identity map request and displays notifications about the result
-		 */
-		removeMap: function(e) {
-			e.preventDefault();
-
-			var equivalentIdentity = $(e.target).parents("a").attr("data-identity");
-			if(!equivalentIdentity) return;
-
-			var viewRef = this,
-				success = function(){
-					viewRef.showAlert("Success! Your account is no longer associated with the user " + equivalentIdentity, "alert-success", "#identity-alert-container");
-				},
-				error = function(xhr, textStatus, error){
-					viewRef.showAlert("Something went wrong: " + xhr.responseText, 'alert-error', "#identity-alert-container");
-				};
-
-			this.model.removeMap(equivalentIdentity, success, error);
-		},
-
-		/*
-		 * Confirms an identity map request that was initiated from another user, and displays notifications about the result
-		 */
-		confirmMapRequest: function(e) {
-			var model = this.model;
-
-			e.preventDefault();
-			var otherUsername = $(e.target).parents("a").attr("data-identity"),
-				mapRequestEl = $(e.target).parents(".pending.identity");
-
-			var viewRef = this;
-
-			var success = function(data, textStatus, xhr) {
-				viewRef.showAlert("Success! Your account is now linked with the username " + otherUsername, "alert-success", "#pending-alert-container");
-
-				mapRequestEl.remove();
-			}
-			var error = function(xhr, textStatus, error) {
-				viewRef.showAlert(xhr.responseText, 'alert-error', "#pending-alert-container");
-			}
-
-			//Confirm this map request
-			this.model.confirmMapRequest(otherUsername, success, error);
-		},
-
-		/*
-		 * Rejects an identity map request that was initiated by another user, and displays notifications about the result
-		 */
-		rejectMapRequest: function(e) {
-			e.preventDefault();
-
-			var equivalentIdentity = $(e.target).parents("a").attr("data-identity"),
-				mapRequestEl = $(e.target).parents(".pending.identity");
-
-			if(!equivalentIdentity) return;
-
-			var viewRef = this,
-				success = function(data){
-					viewRef.showAlert("Removed mapping request for " + equivalentIdentity, "alert-success", "#pending-alert-container");
-					$(mapRequestEl).remove();
-				},
-				error = function(xhr, textStatus, error){
-					viewRef.showAlert(xhr.responseText, 'alert-error', "#pending-alert-container");
-				};
-
-			this.model.denyMapRequest(equivalentIdentity, success, error);
-		},
-
-		insertIdentityList: function(){
-			var identities = this.model.get("identities");
-
-			//Remove the equivalentIdentities list if it was drawn already so we don't do it twice
-			this.$("#identity-list-container").empty();
-
-			if(!identities) return;
-
-			//Create the list element
-			if(identities.length < 1){
-				var identityList = $(document.createElement("p")).text("You haven't linked to another account yet. Send a request below.");
-			}
-			else
-				var identityList = $(document.createElement("ul")).addClass("list-identity").attr("id", "identity-list");
-
-			var view = this;
-			//Create a list item for each identity
-			_.each(identities, function(identity, i){
-				var listItem = view.createUserListItem(identity, { confirmed: true });
-
-				//When/if the info from the equivalent identities is retrieved, update the item
-				view.listenToOnce(identity, "change:fullName", function(identity){
-					var newListItem = view.createUserListItem(identity, {confirmed: true});
-					listItem.replaceWith(newListItem);
-				});
-
-				$(identityList).append(listItem);
-			});
-
-			//Add to the page
-			//$(identityList).find(".collapsed").hide();
-			this.$("#identity-list-container").append(identityList);
-		},
-
-		insertPendingList: function(){
-			var pending = this.model.get("pending");
-
-			//Remove the equivalentIdentities list if it was drawn already so we don't do it twice
-			this.$("#pending-list-container").empty();
-
-			//Create the list element
-			if (pending.length < 1){
-				this.$("[data-subsection='pending-accounts']").hide();
-				return;
-			}
-			else{
-				this.$("[data-subsection='pending-accounts']").show();
-				this.$("#pending-list-container").prepend($(document.createElement("p")).text("You have " + pending.length + " new request to map accounts. If these requests are from you, accept them below. If you do not recognize a username, reject the request."));
-				var pendingList = $(document.createElement("ul")).addClass("list-identity").attr("id", "pending-list");
-				var pendingCount = $(document.createElement("span")).addClass("badge").attr("id", "pending-count").text(pending.length);
-				this.$("#pending-list-heading").append(pendingCount);
-			}
-
-			//Create a list item for each pending id
-			var view = this;
-			_.each(pending, function(pendingUser, i){
-				var listItem = view.createUserListItem(pendingUser, {pending: true});
-				$(pendingList).append(listItem);
-
-				if(pendingUser.isOrcid()){
-					view.listenToOnce(pendingUser, "change:fullName", function(pendingUser){
-						var newListItem = view.createUserListItem(pendingUser, {pending: true});
-						listItem.replaceWith(newListItem);
-					});
-				}
-			});
-
-			//Add to the page
-			this.$("#pending-list-container").append(pendingList);
-		},
-
-		createUserListItem: function(user, options){
-			var pending = false,
-				confirmed = false;
-
-			if(options && options.pending)
-				pending = true;
-			if(options && options.confirmed)
-				confirmed = true;
-
-			var username = user.get("username"),
-			    fullName = user.get("fullName") || username;
-
-			var listItem = $(document.createElement("li")).addClass("list-group-item identity"),
-				link     = $(document.createElement("a")).attr("href", MetacatUI.root + "/profile/" + username).attr("data-identity", username).text(fullName),
-				details  = $(document.createElement("span")).addClass("subtle details").text(username);
-
-			listItem.append(link, details);
-
-			if(pending){
-				var acceptIcon = $(document.createElement("i")).addClass("icon icon-ok icon-large icon-positive tooltip-this").attr("data-title", "Accept Request").attr("data-trigger", "hover").attr("data-placement", "top"),
-					rejectIcon = $(document.createElement("i")).addClass("icon icon-remove icon-large icon-negative tooltip-this").attr("data-title", "Reject Request").attr("data-trigger", "hover").attr("data-placement", "top"),
-					confirm = $(document.createElement("a")).attr("href", "#").addClass('confirm-request-btn').attr("data-identity", username).append(acceptIcon),
-					reject = $(document.createElement("a")).attr("href", "#").addClass("reject-request-btn").attr("data-identity", username).append(rejectIcon);
-
-				listItem.prepend(confirm, reject).addClass("pending");
-			}
-			else if(confirmed){
-				var removeIcon = $(document.createElement("i")).addClass("icon icon-remove icon-large icon-negative"),
-					remove = $(document.createElement("a")).attr("href", "#").addClass("remove-identity-btn").attr("data-identity", username).append(removeIcon);
-				$(remove).tooltip({
-					trigger: "hover",
-					placement: "top",
-					title: "Remove equivalent account"
-				});
-				listItem.prepend(remove.append(removeIcon));
-			}
-
-			if(user.isOrcid()){
-				details.prepend(this.createIdPrefix(), " ORCID: ");
-			}
-			else
-				details.prepend(" Username: ");
-
-			return listItem;
-		},
-
-		updateModForm: function() {
-			this.$("#mod-givenName").val(this.model.get("firstName"));
-			this.$("#mod-familyName").val(this.model.get("lastName"));
-			this.$("#mod-email").val(this.model.get("email"));
-
-			if(!this.model.get("email")){
-				this.$("#mod-email").parent(".form-group").addClass("has-warning");
-				this.$("#mod-email").parent(".form-group").find(".help-block").text("Please provide an email address.");
-			}
-			else{
-				this.$("#mod-email").parent(".form-group").removeClass("has-warning");
-				this.$("#mod-email").parent(".form-group").find(".help-block").text("");
-			}
-
-			if (this.model.get("registered")) {
-				this.$("#registered-user-container").show();
-			} else {
-				this.$("#registered-user-container").hide();
-			}
-		},
-
-		/*
-		 * Gets the user account settings, updates the UserModel and saves this new info to the server
-		 */
-		saveUser: function(e) {
-
-			e.preventDefault();
-
-			var view = this,
-				container = this.$('[data-subsection="edit-account"] .content') || $(e.target).parent();
-
-			var success = function(data){
-				$(container).find(".loading").detach();
-				$(container).children().show();
-				view.showAlert("Success! Your profile has been updated.", 'alert-success', container);
-			}
-			var error = function(data){
-				$(container).find(".loading").detach();
-				$(container).children().show();
-				var msg = (data && data.responseText) ? data.responseText : "Sorry, updating your profile failed. Please try again.";
-				if(!data.responseText)
-					view.showAlert(msg, 'alert-error', container);
-			}
-
-			//Get info entered into form
-			var givenName = this.$("#mod-givenName").val();
-			var familyName = this.$("#mod-familyName").val();
-			var email = this.$("#mod-email").val();
-
-			//Update the model
-			this.model.set("firstName", givenName);
-			this.model.set("lastName", familyName);
-			this.model.set("email", email);
-
-			//Loading icon
-			$(container).children().hide();
-			$(container).prepend(this.loadingTemplate());
-
-			//Send the update
-			this.model.update(success, error);
-
-		},
-
-		//---------------------------------- Token -----------------------------------------//
-		getToken: function(){
-			var model = this.model;
-
-			//Show loading sign
-			this.$("#token-generator-container").html(this.loadingTemplate());
-
-			//When the token is retrieved, then show it
-			this.listenToOnce(this.model, "change:token", this.showToken);
-
-			//Get the token from the CN
-			this.model.getToken(function(data, textStatus, xhr){
-				model.getTokenExpiration();
-				model.set("token", data);
-				model.trigger("change:token");
-			});
-		},
-
-		showToken: function(){
-			var token = this.model.get("token");
-
-			if(!token || !this.model.get("loggedIn"))
-				return;
-
-			var expires    = this.model.get("expires"),
-				rTokenName = (MetacatUI.appModel.get("d1CNBaseUrl").indexOf("cn.dataone.org") > -1)? "dataone_token" : "dataone_test_token",
-				rToken = 'options(' + rTokenName +' = "' + token + '")',
-				matlabToken = "import org.dataone.client.run.RunManager; mgr = RunManager.getInstance(); mgr.configuration.authentication_token = '" + token + "';",
-				tokenInput = $(document.createElement("textarea")).attr("type", "text").attr("rows", "5").addClass("token copy").text(token),
-				copyButton = $(document.createElement("a")).addClass("btn btn-primary copy").text("Copy").attr("data-clipboard-text", token),
-				copyRButton = $(document.createElement("a")).addClass("btn btn-primary copy").text("Copy").attr("data-clipboard-text", rToken),
-				copyMatlabButton = $(document.createElement("a")).addClass("btn btn-primary copy").text("Copy").attr("data-clipboard-text", matlabToken),
-				successIcon = $(document.createElement("i")).addClass("icon icon-ok"),
-		  		copySuccess = $(document.createElement("div")).addClass("notification success copy-success hidden").append(successIcon, " Copied!"),
-		  		expirationMsg = expires? "<strong>Note:</strong> Your authentication token expires on " + expires.toLocaleDateString() + " at " + expires.toLocaleTimeString() : "",
-		  		usernameMsg = "<div class='footnote'>Your user identity: ",
-		  		usernamePrefix = this.createIdPrefix(),
-		  		tabs = $(document.createElement("ul")).addClass("nav nav-tabs")
-		  				.append($(document.createElement("li")).addClass("active")
-		  						.append( $(document.createElement("a")).attr("href", "#token-code-panel").addClass("token-tab").text("Token") ))
-		  				.append($(document.createElement("li"))
-		  						.append( $(document.createElement("a")).attr("href", "#r-token-code-panel").addClass("token-tab").text("Token for DataONE R") ))
-		  				.append($(document.createElement("li"))
-		  						.append( $(document.createElement("a")).attr("href", "#matlab-token-code-panel").addClass("token-tab").text("Token for Matlab DataONE Toolbox") )),
-		  		tokenRInput = $(document.createElement("textarea")).attr("type", "text").attr("rows", "5").addClass("token copy").text(rToken),
-		  		tokenRText = $(document.createElement("p")).text("Copy this code snippet to use your token with the DataONE R package."),
-		  		tokenMatlabInput = $(document.createElement("textarea")).attr("type", "text").attr("rows", "5").addClass("token copy").text(matlabToken),
-		  		tokenMatlabText = $(document.createElement("p")).text("Copy this code snippet to use your token with the Matlab DataONE toolbox."),
-		  		tokenInputContain = $(document.createElement("div")).attr("id", "token-code-panel").addClass("tab-panel active").append(tokenInput, copyButton, copySuccess),
-		  		rTokenInputContain = $(document.createElement("div")).attr("id", "r-token-code-panel").addClass("tab-panel").append(tokenRText, 	tokenRInput, copyRButton, copySuccess.clone()).addClass("hidden"),
-		  		matlabTokenInputContain = $(document.createElement("div")).attr("id", "matlab-token-code-panel").addClass("tab-panel").append(tokenMatlabText, 	tokenMatlabInput, copyMatlabButton, copySuccess.clone()).addClass("hidden");
-
-			if(typeof usernamePrefix == "object")
-				usernameMsg += usernamePrefix[0].outerHTML;
-			else if(typeof usernamePrefix == "string")
-				usernameMsg += usernamePrefix;
-
-			usernameMsg += this.model.get("username") + "</div>";
-
-			var	successMessage = $.parseHTML(this.alertTemplate({
-					msg: 'Copy your authentication token: <br/> ' + expirationMsg + usernameMsg,
-					classes: "alert-success",
-					containerClasses: "well"
-				}));
-			$(successMessage).append(tabs, tokenInputContain, rTokenInputContain, matlabTokenInputContain);
-			this.$("#token-generator-container").html(successMessage);
-
-			$(".token-tab").tab();
-
-			//Create clickable "Copy" buttons to copy text (e.g. token) to the user's clipboard
-			var clipboard = new Clipboard(".copy");
-
-				clipboard.on("success", function(e){
-				$(".copy-success").show().delay(3000).fadeOut();
-				});
-
-				clipboard.on("error", function(e){
-					var textarea = $(e.trigger).parent().children("textarea.token");
-					textarea.trigger("focus");
-					textarea.tooltip({
-						title: "Press Ctrl+c to copy",
-						placement: "bottom"
-					});
-					textarea.tooltip("show");
-
-				});
-		},
-
-		setUpAutocomplete: function() {
-			var input = this.$(".account-autocomplete");
-			if(!input || !input.length) return;
-
-			// look up registered identities
-			$(input).hoverAutocomplete({
-				source: function (request, response) {
-		            var term = $.ui.autocomplete.escapeRegex(request.term);
-
-		            var list = [];
-
-		            //Ids/Usernames that we want to ignore in the autocompelte
-		            var ignoreEquivIds = ($(this.element).attr("id") == "map-request-field"),
-		            	ignoreIds = ignoreEquivIds? MetacatUI.appUserModel.get("identitiesUsernames") : [];
-		            ignoreIds.push(MetacatUI.appUserModel.get("username").toLowerCase());
-
-		            var url = MetacatUI.appModel.get("accountsUrl") + "?query=" + encodeURIComponent(term);
-					var requestSettings = {
-						url: url,
-						success: function(data, textStatus, xhr) {
-							_.each($(data).find("person"), function(person, i){
-								var item = {};
-								item.value = $(person).find("subject").text();
-
-								//Don't display yourself in the autocomplete dropdown (prevents users from adding themselves as an equivalent identity or group member)
-								//Also don't display your equivalent identities in the autocomplete
-								if(_.contains(ignoreIds, item.value.toLowerCase())) return;
-
-								item.label = $(person).find("fullName").text() || ($(person).find("givenName").text() + " " + $(person).find("familyName").text());
-								list.push(item);
-							});
-
-				            response(list);
-
-						}
-					}
-					$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
-
-					//Send an ORCID search when the search string gets long enough
-					if(request.term.length > 3)
-						MetacatUI.appLookupModel.orcidSearch(request, response, false, ignoreIds);
-		        },
-				select: function(e, ui) {
-					e.preventDefault();
-
-					// set the text field
-					$(e.target).val(ui.item.value);
-					$(e.target).parents("form").find("input[name='fullName']").val(ui.item.label);
-				},
-				position: {
-					my: "left top",
-					at: "left bottom",
-					collision: "none"
-				}
-			});
-
-		},
-
-    /**
-    * Renders a list of portals that this user is an owner of.
-    */
-    renderMyPortals: function(){
-
-      //If my portals has been disabled, don't render the list
-      if( MetacatUI.appModel.get("showMyPortals") === false ){
-        return;
-      }
-
-      var view = this;
-
-      //If Bookkeeper services are enabled, render the Portals via a PortalUsagesView,
-      // which queries Bookkeeper for portal Usages
-      if( MetacatUI.appModel.get("enableBookkeeperServices") ){
-        require(['views/portals/PortalUsagesView'], function(PortalUsagesView){
-          var portalListView = new PortalUsagesView();
-          //Render the Portal list view and insert it in the page
-          portalListView.render();
-          view.$(view.portalListContainer)
-              .html(portalListView.el);
-        });
-      }
-      //If Bookkeeper services are disabled, render the Portals via a PortalListView,
-      // which queries Solr for portal docs
-      else{
-        require(['views/portals/PortalListView'], function(PortalListView){
-          //Create a PortalListView
-          var portalListView = new PortalListView();
-          //Render the Portal list view and insert it in the page
-          portalListView.render();
-          view.$(view.portalListContainer)
-              .html(portalListView.el);
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "clipboard",
+  "collections/UserGroup",
+  "models/UserModel",
+  "models/Stats",
+  "views/SignInView",
+  "views/StatsView",
+  "views/DataCatalogView",
+  "views/UserGroupView",
+  "text!templates/userProfile.html",
+  "text!templates/alert.html",
+  "text!templates/loading.html",
+  "text!templates/userProfileMenu.html",
+  "text!templates/userSettings.html",
+  "text!templates/noResults.html",
+], function (
+  $,
+  _,
+  Backbone,
+  Clipboard,
+  UserGroup,
+  UserModel,
+  Stats,
+  SignInView,
+  StatsView,
+  DataCatalogView,
+  UserGroupView,
+  userProfileTemplate,
+  AlertTemplate,
+  LoadingTemplate,
+  ProfileMenuTemplate,
+  SettingsTemplate,
+  NoResultsTemplate,
+) {
+  "use strict";
+
+  /**
+   * @class UserView
+   * @classdesc A major view that displays a public profile for the user and a settings page for the logged-in user
+   * to manage their account info, groups, identities, and API tokens.
+   * @classcategory Views
+   * @screenshot views/UserView.png
+   * @extends Backbone.View
+   */
+  var UserView = Backbone.View.extend(
+    /** @lends UserView.prototype */ {
+      el: "#Content",
+
+      //Templates
+      profileTemplate: _.template(userProfileTemplate),
+      alertTemplate: _.template(AlertTemplate),
+      loadingTemplate: _.template(LoadingTemplate),
+      settingsTemplate: _.template(SettingsTemplate),
+      menuTemplate: _.template(ProfileMenuTemplate),
+      noResultsTemplate: _.template(NoResultsTemplate),
+
+      /**
+       * A jQuery selector for the element that the PortalListView should be inserted into
+       * @type {string}
+       */
+      portalListContainer: ".my-portals-container",
+
+      events: {
+        "click .section-link": "switchToSection",
+        "click .subsection-link": "switchToSubSection",
+        "click .token-generator": "getToken",
+        "click #mod-save-btn": "saveUser",
+        "click #map-request-btn": "sendMapRequest",
+        "click .remove-identity-btn": "removeMap",
+        "click .confirm-request-btn": "confirmMapRequest",
+        "click .reject-request-btn": "rejectMapRequest",
+        "click [highlight-subsection]": "highlightSubSection",
+        "keypress #add-group-name": "preventSubmit",
+        "click .token-tab": "switchTabs",
+      },
+
+      initialize: function () {
+        this.subviews = new Array();
+      },
+
+      //------------------------------------------ Rendering the main parts of the view ------------------------------------------------//
+      render: function (options) {
+        //Don't render anything if the user profiles are turned off
+        if (MetacatUI.appModel.get("enableUserProfiles") === false) {
+          return;
+        }
+
+        this.stopListening();
+        if (this.model) this.model.stopListening();
+
+        //Create a Stats model
+        this.statsModel = new Stats();
+
+        this.activeSection =
+          options && options.section ? options.section : "profile";
+        this.activeSubSection =
+          options && options.subsection ? options.subsection : "";
+        this.username =
+          options && options.username ? options.username : undefined;
+
+        //Add the container element for our profile sections
+        this.sectionHolder = $(document.createElement("section")).addClass(
+          "user-view-section",
+        );
+        this.$el.html(this.sectionHolder);
+
+        //Show the loading sign first
+        //$(this.sectionHolder).html(this.loadingTemplate());
+        this.$el.show();
+
+        // set the header type
+        MetacatUI.appModel.set("headerType", "default");
+
+        //Render the user profile only after the app user's info has been checked
+        //This prevents the app from rendering the profile before the login process has completed - which would
+        //cause this profile to render twice (first before the user is logged in then again after they log in)
+        if (MetacatUI.appUserModel.get("checked")) this.renderUser();
+        else MetacatUI.appUserModel.on("change:checked", this.renderUser, this);
+
+        return this;
+      },
+
+      /**
+       * Update the window location path to route to /portals path
+       * @param {string} username - Short identifier for the member node
+       */
+      forwardToPortals: function (username) {
+        var pathName = decodeURIComponent(window.location.pathname)
+          .substring(MetacatUI.root.length)
+          // remove trailing forward slash if one exists in path
+          .replace(/\/$/, "");
+
+        // Routes the /profile/{node-id} to /portals/{node-id}
+        var pathRE = new RegExp("\\/profile(\\/[^\\/]*)?$", "i");
+        var newPathName =
+          pathName.replace(pathRE, "") +
+          "/" +
+          MetacatUI.appModel.get("portalTermPlural") +
+          "/" +
+          username;
+
+        // Update the window location
+        MetacatUI.uiRouter.navigate(newPathName, {
+          trigger: true,
+          replace: true,
         });
-      }
-
-    },
-
-		//---------------------------------- Misc. and Utilities -----------------------------------------//
-
-		showAlert: function(msg, classes, container) {
-			if(!classes)
-				var classes = 'alert-success';
-			if(!container || !$(container).length)
-				var container = this.$el;
-
-			//Remove any alerts that are already in this container
-			if($(container).children(".alert-container").length > 0)
-				$(container).children(".alert-container").remove();
-
-			$(container).prepend(
-					this.alertTemplate({
-						msg: msg,
-						classes: classes
-					})
-			);
-		},
-
-		switchTabs: function(e){
-			e.preventDefault();
-			$(e.target).tab('show');
-			this.$(".tab-panel").hide();
-			this.$(".tab-panel" + $(e.target).attr("href")).show();
-			this.$("#token-generator-container .copy-button").attr("data-clipboard-text")
-
-		},
-
-		preventSubmit: function(e){
-			if(e.keyCode != 13) return;
+        return;
+      },
+
+      renderUser: function () {
+        this.model = MetacatUI.appUserModel;
+
+        var username =
+            MetacatUI.appModel.get("profileUsername") || view.username,
+          currentUser = MetacatUI.appUserModel.get("username") || "";
+
+        if (username.toUpperCase() == currentUser.toUpperCase()) {
+          //Case-insensitive matching of usernames
+          this.model = MetacatUI.appUserModel;
+          this.model.set("type", "user");
+
+          //If the user is logged in, display the settings options
+          if (this.model.get("loggedIn")) {
+            this.insertMenu();
+            this.renderProfile();
+            this.renderSettings();
+            this.resetSections();
+          }
+        }
+
+        //If this isn't the currently-logged in user, then let's find out more info about this account
+        else {
+          //Create a UserModel with the username given
+          this.model = new UserModel({
+            username: username,
+          });
+
+          //Is this a member node?
+          if (MetacatUI.nodeModel.get("checked") && this.model.isNode()) {
+            this.model.saveAsNode();
+            this.model.set(
+              "nodeInfo",
+              _.find(MetacatUI.nodeModel.get("members"), function (nodeModel) {
+                return (
+                  nodeModel.identifier.toLowerCase() ==
+                  "urn:node:" + username.toLowerCase()
+                );
+              }),
+            );
+            this.forwardToPortals(username);
+            return;
+          }
+          //If the node model hasn't been checked yet
+          else if (!MetacatUI.nodeModel.get("checked")) {
+            var user = this.model,
+              view = this;
+            this.listenTo(MetacatUI.nodeModel, "change:checked", function () {
+              if (user.isNode()) view.render();
+            });
+          }
 
-			e.preventDefault();
-		},
+          //When we get the infomration about this account, then crender the profile
+          this.model.once("change:checked", this.renderProfile, this);
+          this.model.once("change:checked", this.resetSections, this);
+          //Get the info
+          this.model.getInfo();
+        }
+
+        //When the model is reset, refresh the page
+        this.listenTo(this.model, "reset", this.render);
+      },
+
+      renderProfile: function () {
+        //Insert the template first
+        var profileEl = $.parseHTML(
+          this.profileTemplate({
+            type: this.model.get("type"),
+            logo: this.model.get("logo") || "",
+            description: this.model.get("description") || "",
+            user: this.model.toJSON(),
+          }).trim(),
+        );
+
+        //If the profile is being redrawn, then replace it
+        if (this.$profile && this.$profile.length) {
+          //If the profile section is currently hidden, make sure we hide our new profile rendering too
+          if (!this.$profile.is(":visible")) $(profileEl).hide();
+
+          this.$profile.replaceWith(profileEl);
+        }
+        //If this is a fresh rendering, then append it to the page and save it
+        else this.sectionHolder.append(profileEl);
+
+        this.$profile = $(profileEl);
+
+        //If this user hasn't uploaded anything yet, display so
+        this.listenTo(this.statsModel, "change:totalCount", function () {
+          if (!this.statsModel.get("totalCount")) this.noActivity();
+        });
 
-		onClose: function () {
-			//Clear the template
-			this.$el.html("");
+        //Insert the user data statistics
+        this.insertStats();
+
+        //Insert the user's basic information
+        this.listenTo(this.model, "change:fullName", this.insertUserInfo);
+        this.insertUserInfo();
+
+        var view = this;
+        //Listen to changes in the user's search terms
+        this.listenTo(this.model, "change:searchModel", this.renderProfile);
+
+        //Insert this user's data content
+        this.insertContent();
+
+        // create the UserGroupView to generate the membership list
+        // this is the first call to UserGroupView so we instantiate it here
+        var groupView = new UserGroupView({ model: this.model });
+        this.subviews.push(groupView);
+        this.renderMembershipList();
+      },
+
+      renderMembershipList: function () {
+        //List the groups this user is in by creating usergroupview subview
+        //List the groups this user is in by creating usergroupview subview
+        var groupView = _.where(this.subviews, { type: "UserGroupView" }).at(0);
+
+        if (this.model.get("type") == "group") {
+          //Create the User Group collection
+          var options = {
+            name: this.model.get("fullName"),
+            groupId: this.model.get("username"),
+            rawData: this.model.get("rawData") || null,
+          };
+          var userGroup = new UserGroup([], options);
+          //Create the group list and add it to the page
+          var viewOptions = { collapsable: false, showGroupName: false };
+          var groupList = groupView.createGroupList(userGroup, viewOptions);
+          this.$("#user-membership-container").html(groupList);
+        } else {
+          var groups = _.sortBy(this.model.get("isMemberOf"), "name");
+          if (!groups.length) {
+            this.$("#user-membership-header").hide();
+            return;
+          }
+          this.sectionHolder.append(
+            groupView
+              .insertMembership(groups, this.$("#user-membership-container"))
+              .html(),
+          );
+        }
+      },
+
+      renderGroupsSection: function () {
+        var groupView = _.where(this.subviews, { type: "UserGroupView" }).at(0);
+        var container = this.$("#groups-container");
+        container.append(groupView.render().el);
+      },
+
+      renderSettings: function () {
+        //Don't render anything if the user profile settings are turned off
+        if (MetacatUI.appModel.get("enableUserProfileSettings") === false) {
+          return;
+        }
+
+        //Insert the template first
+        this.sectionHolder.append(this.settingsTemplate(this.model.toJSON()));
+        this.$settings = this.$("[data-section='settings']");
+
+        //Draw the group list
+        this.renderGroupsSection();
+
+        //Listen for the identity list
+        this.listenTo(this.model, "change:identities", this.insertIdentityList);
+        this.insertIdentityList();
+
+        //Listen for the pending list
+        this.listenTo(this.model, "change:pending", this.insertPendingList);
+        this.model.getPendingIdentities();
+
+        //Render the portals subsection
+        this.renderMyPortals();
+
+        //Listen for updates to person details
+        this.listenTo(
+          this.model,
+          "change:lastName change:firstName change:email change:registered",
+          this.updateModForm,
+        );
+        this.updateModForm();
+
+        // init autocomplete fields
+        this.setUpAutocomplete();
+
+        //Get the token right away
+        this.getToken();
+      },
+
+      /*
+       * Displays a menu for the user to switch between different views of the user profile
+       */
+      insertMenu: function () {
+        //If the user is not logged in, then remove the menu
+        if (!MetacatUI.appUserModel.get("loggedIn")) {
+          this.$(".nav").remove();
+          return;
+        }
+
+        //Otherwise, insert the menu
+        var menu = this.menuTemplate({
+          username: this.model.get("username"),
+        });
 
-			//Reset the active section and subsection
-			this.activeSection = "profile";
-			this.activeSubSection = "";
+        this.$el.prepend(menu);
+      },
+
+      //------------------------------------------ Navigating sections of view ------------------------------------------------//
+      switchToSection: function (e, sectionName) {
+        if (e) e.preventDefault();
+
+        //Hide all the sections first
+        $(this.sectionHolder).children().slideUp().removeClass(".active");
+
+        //Get the section name
+        if (!sectionName) var sectionName = $(e.target).attr("data-section");
+
+        //Display the specified section
+        var activeSection = this.$(
+          ".section[data-section='" + sectionName + "']",
+        );
+        if (!activeSection.length)
+          activeSection = this.$(".section[data-section='profile']");
+        $(activeSection).addClass("active").slideDown();
+
+        //Change the navigation tabs
+        this.$(".nav-tab").removeClass("active");
+        $(".nav-tab[data-section='" + sectionName + "']").addClass("active");
+
+        //Find all the subsections, if there are any
+        if ($(activeSection).find(".subsection").length > 0) {
+          //Find any item classified as "active"
+          var activeItem = $(activeSection).find(".active");
+          if (activeItem.length > 0) {
+            //Find the data section this active item is referring to
+            if ($(activeItem).children("[data-subsection]").length > 0) {
+              //Get the section name
+              var subsectionName = $(activeItem)
+                .find("[data-subsection]")
+                .first()
+                .attr("data-subsection");
+              //If we found a section name, find the subsection element and display it
+              if (subsectionName) this.switchToSubSection(null, subsectionName);
+            } else
+              this.switchToSubSection(
+                null,
+                $(activeSection)
+                  .children("[data-section]")
+                  .first()
+                  .attr("data-section"),
+              );
+          }
+        }
+      },
+
+      switchToSubSection: function (e, subsectionName) {
+        if (e) {
+          e.preventDefault();
+          var subsectionName = $(e.target).attr("data-section");
+          if (!subsectionName) {
+            subsectionName = $(e.target)
+              .parents("[data-section]")
+              .first()
+              .attr("data-section");
+          }
+        }
+
+        //Mark its links as active
+        $(".section.active").find(".subsection-link").removeClass("active");
+        $(".section.active")
+          .find(".subsection-link[data-section='" + subsectionName + "']")
+          .addClass("active");
+
+        //Hide all the other sections
+        $(".section.active").find(".subsection").hide();
+        $(".section.active")
+          .find(".subsection[data-section='" + subsectionName + "']")
+          .show();
+      },
+
+      resetSections: function () {
+        //Hide all the sections first, then display the section specified in the URL (or the default)
+        this.$(".subsection, .section").hide();
+        this.switchToSection(null, this.activeSection);
+
+        //Show the subsection
+        if (this.activeSubSection)
+          this.switchToSubSection(null, this.activeSubSection);
+      },
+
+      highlightSubSection: function (e, subsectionName) {
+        if (e) e.preventDefault();
+
+        if (!subsectionName && e) {
+          //Get the subsection name
+          var subsectionName = $(e.target).attr("highlight-subsection");
+          if (!subsectionName) return;
+        } else if (!subsectionName && !e) return false;
+
+        //Find the subsection
+        var subsection = this.$(
+          ".subsection[data-section='" + subsectionName + "']",
+        );
+        if (!subsection.length)
+          subsection = this.$("[data-subsection='add-account']");
+        if (!subsection.length) return;
+
+        //Visually highlight the subsection
+        subsection.addClass("highlight");
+        MetacatUI.appView.scrollTo(subsection);
+        //Wait about a second and then remove the highlight style
+        window.setTimeout(function () {
+          subsection.removeClass("highlight");
+        }, 1500);
+      },
+
+      //------------------------------------------ Inserting public profile UI elements ------------------------------------------------//
+      insertStats: function () {
+        if (this.model.noActivity && this.statsView) {
+          this.statsView.$el.addClass("no-activity");
+          this.$("#total-download-wrapper, section.downloads").hide();
+          return;
+        }
+
+        var username = this.model.get("username"),
+          view = this;
+
+        //Insert a couple stats into the profile
+        this.listenToOnce(
+          this.statsModel,
+          "change:firstUpload",
+          this.insertFirstUpload,
+        );
+
+        this.listenToOnce(this.statsModel, "change:totalCount", function () {
+          view
+            .$("#total-upload-container")
+            .text(
+              MetacatUI.appView.commaSeparateNumber(
+                view.statsModel.get("totalCount"),
+              ),
+            );
+        });
 
-      //Reset the model
-      if( this.model ){
-			  this.model.noActivity = null;
-        this.stopListening(this.model);
-      }
+        //Create a base query for the statistics
+        var statsSearchModel = this.model.get("searchModel").clone();
+        statsSearchModel
+          .set("exclude", [], { silent: true })
+          .set("formatType", [], { silent: true });
+        this.statsModel.set("query", statsSearchModel.getQuery());
+        this.statsModel.set("isSystemMetadataQuery", true);
+        this.statsModel.set("searchModel", statsSearchModel);
+
+        //Create the description for this profile
+        var description;
+
+        switch (this.model.get("type")) {
+          case "node":
+            description =
+              "A summary of all datasets from the " +
+              this.model.get("fullName") +
+              " repository";
+            break;
+          case "group":
+            description =
+              "A summary of all datasets from the " +
+              this.model.get("fullName") +
+              " group";
+            break;
+          case "user":
+            description =
+              "A summary of all datasets from " + this.model.get("fullName");
+            break;
+          default:
+            description = "";
+            break;
+        }
+
+        //Render the Stats View for this person
+        this.statsView = new StatsView({
+          title: "Statistics and Figures",
+          description: description,
+          userType: "user",
+          el: this.$("#user-stats"),
+          model: this.statsModel,
+        });
+        this.subviews.push(this.statsView);
+        this.statsView.render();
+        if (this.model.noActivity) this.statsView.$el.addClass("no-activity");
+      },
+
+      /*
+       * Insert the name of the user
+       */
+      insertUserInfo: function () {
+        //Don't try to insert anything if we haven't gotten all the user info yet
+        if (!this.model.get("fullName")) return;
+
+        //Insert the name into this page
+        var usernameLink = $(document.createElement("a"))
+          .attr(
+            "href",
+            MetacatUI.root + "/profile/" + this.model.get("username"),
+          )
+          .text(this.model.get("fullName"));
+        this.$(".insert-fullname").append(usernameLink);
+
+        //Insert the username
+        if (this.model.get("type") != "node") {
+          if (!this.model.get("usernameReadable"))
+            this.model.createReadableUsername();
+          this.$(".insert-username").text(this.model.get("usernameReadable"));
+        } else {
+          $("#username-wrapper").hide();
+        }
+
+        //Show or hide ORCID logo
+        if (this.model.isOrcid()) this.$(".show-orcid").show();
+        else this.$(".show-orcid").hide();
+
+        //Show the email
+        if (this.model.get("email")) {
+          this.$(".email-wrapper").show();
+          var parts = this.model.get("email").split("@");
+          this.$(".email-container").attr("data-user", parts[0]);
+          this.$(".email-container").attr("data-domain", parts[1]);
+        } else this.$(".email-wrapper").hide();
+      },
+
+      // Creates an HTML element to display in front of the user identity/subject.
+      // Only used for the ORCID logo right now
+      createIdPrefix: function () {
+        if (this.model.isOrcid())
+          return $(document.createElement("img"))
+            .attr("src", MetacatUI.root + "/img/orcid_64x64.png")
+            .addClass("orcid-logo");
+        else return "";
+      },
+
+      /*
+       * Insert the first year of contribution for this user
+       */
+      insertFirstUpload: function () {
+        if (this.model.noActivity || !this.statsModel.get("firstUpload")) {
+          this.$(
+            "#first-upload-container, #first-upload-year-container",
+          ).hide();
+          return;
+        }
+
+        // Get the first upload or first operational date
+        if (this.model.get("type") == "node") {
+          //Get the member node object
+          var node = _.findWhere(MetacatUI.nodeModel.get("members"), {
+            identifier: "urn:node:" + this.model.get("username"),
+          });
+
+          //If there is no memberSince date, then hide this statistic and exit
+          if (!node.memberSince) {
+            this.$(
+              "#first-upload-container, #first-upload-year-container",
+            ).hide();
+            return;
+          } else {
+            var firstUpload = node.memberSince
+              ? new Date(
+                  node.memberSince.substring(0, node.memberSince.indexOf("T")),
+                )
+              : new Date();
+          }
+        } else {
+          var firstUpload = new Date(this.statsModel.get("firstUpload"));
+        }
+
+        // Construct the first upload date sentence
+        var monthNames = [
+            "January",
+            "February",
+            "March",
+            "April",
+            "May",
+            "June",
+            "July",
+            "August",
+            "September",
+            "October",
+            "November",
+            "December",
+          ],
+          m = monthNames[firstUpload.getUTCMonth()],
+          y = firstUpload.getUTCFullYear(),
+          d = firstUpload.getUTCDate();
+
+        //For Member Nodes, start all dates at July 2012, the beginning of DataONE
+        if (this.model.get("type") == "node") {
+          this.$("#first-upload-container").text(
+            "DataONE Member Node since " + y,
+          );
+        } else
+          this.$("#first-upload-container").text(
+            "Contributor since " + m + " " + d + ", " + y,
+          );
+
+        //Construct the time-elapsed sentence
+        var now = new Date(),
+          msElapsed = now - firstUpload,
+          years = msElapsed / 31556952000,
+          months = msElapsed / 2629746000,
+          weeks = msElapsed / 604800000,
+          days = msElapsed / 86400000,
+          time = "";
+
+        //If one week or less, express in days
+        if (weeks <= 1) {
+          time = (Math.round(days) || 1) + " day";
+          if (days > 1.5) time += "s";
+        }
+        //If one month or less, express in weeks
+        else if (months < 1) {
+          time = (Math.round(weeks) || 1) + " week";
+          if (weeks > 1.5) time += "s";
+        }
+        //If less than 12 months, express in months
+        else if (months <= 11.5) {
+          time = (Math.round(months) || 1) + " month";
+          if (months > 1.5) time += "s";
+        }
+        //If one year or more, express in years and months
+        else {
+          var yearsOnly = Math.floor(years) || 1,
+            monthsOnly = Math.round((years % 1) * 12);
+
+          if (monthsOnly == 12) {
+            yearsOnly += 1;
+            monthsOnly = 0;
+          }
 
-			//Remove saved elements
-			this.$profile = null;
+          time = yearsOnly + " year";
+          if (yearsOnly > 1) time += "s";
+
+          if (monthsOnly) time += ", " + monthsOnly + " month";
+          if (monthsOnly > 1) time += "s";
+        }
+
+        this.$("#first-upload-year-container").text(time);
+      },
+
+      /*
+       * Insert a list of this user's content
+       */
+      insertContent: function () {
+        if (this.model.noActivity) {
+          this.$("#data-list").html(
+            this.noResultsTemplate({
+              fullName: this.model.get("fullName"),
+              username:
+                this.model == MetacatUI.appUserModel &&
+                MetacatUI.appUserModel.get("loggedIn")
+                  ? this.model.get("username")
+                  : null,
+            }),
+          );
+          return;
+        }
+
+        var view = new DataCatalogView({
+          el: this.$("#data-list")[0],
+          searchModel: this.model.get("searchModel"),
+          searchResults: this.model.get("searchResults"),
+          mode: "list",
+          isSubView: true,
+          filters: false,
+        });
+        this.subviews.push(view);
+        view.render();
+        view.$el.addClass("list-only");
+        view.$(".auto-height").removeClass("auto-height").css("height", "auto");
+        $("#metacatui-app").removeClass("DataCatalog mapMode");
+      },
+
+      /*
+       * When this user has not uploaded any content, render the profile differently
+       */
+      noActivity: function () {
+        this.model.noActivity = true;
+        this.insertContent();
+        this.insertFirstUpload();
+        this.insertStats();
+      },
+
+      //------------------------------------------------ Identities/Accounts -------------------------------------------------------//
+      /*
+       * Sends a new identity map request and displays notifications about the result
+       */
+      sendMapRequest: function (e) {
+        e.preventDefault();
+
+        //Get the identity entered into the input
+        var equivalentIdentity = this.$("#map-request-field").val();
+        if (!equivalentIdentity || equivalentIdentity.length < 1) {
+          return;
+        }
+        //Clear the text input
+        this.$("#map-request-field").val("");
+
+        //Show notifications after the identity map request is a success or failure
+        var viewRef = this,
+          success = function () {
+            var message =
+              "An account map request has been sent to <a href=" +
+              MetacatUI.root +
+              "'/profile/" +
+              equivalentIdentity +
+              "'>" +
+              equivalentIdentity +
+              "</a>" +
+              "<h4>Next step:</h4><p>Sign In with this other account and approve this request.</p>";
+            viewRef.showAlert(message, null, "#request-alert-container");
+          },
+          error = function (xhr) {
+            var errorMessage = xhr.responseText;
+
+            if (xhr.responseText.indexOf("Request already issued") > -1) {
+              viewRef.showAlert(
+                "<p>You have already sent a request to map this account to " +
+                  equivalentIdentity +
+                  ".</p> <h4>Next Step:</h4><p> Sign In with your " +
+                  equivalentIdentity +
+                  " account and approve the request.</p>",
+                "alert-info",
+                "#request-alert-container",
+              );
+            } else {
+              //Make a more understandable error message when the account isn't found
+              if (
+                xhr.responseText.indexOf(
+                  "LDAP: error code 32 - No Such Object",
+                ) > -1
+              ) {
+                xhr.responseText =
+                  "The username " +
+                  equivalentIdentity +
+                  " does not exist in our system.";
+              }
+
+              viewRef.showAlert(
+                xhr.responseText,
+                "alert-error",
+                "#request-alert-container",
+              );
+            }
+          };
+
+        //Send it
+        this.model.addMap(equivalentIdentity, success, error);
+      },
+
+      /*
+       * Removes a confirmed identity map request and displays notifications about the result
+       */
+      removeMap: function (e) {
+        e.preventDefault();
+
+        var equivalentIdentity = $(e.target).parents("a").attr("data-identity");
+        if (!equivalentIdentity) return;
+
+        var viewRef = this,
+          success = function () {
+            viewRef.showAlert(
+              "Success! Your account is no longer associated with the user " +
+                equivalentIdentity,
+              "alert-success",
+              "#identity-alert-container",
+            );
+          },
+          error = function (xhr, textStatus, error) {
+            viewRef.showAlert(
+              "Something went wrong: " + xhr.responseText,
+              "alert-error",
+              "#identity-alert-container",
+            );
+          };
+
+        this.model.removeMap(equivalentIdentity, success, error);
+      },
+
+      /*
+       * Confirms an identity map request that was initiated from another user, and displays notifications about the result
+       */
+      confirmMapRequest: function (e) {
+        var model = this.model;
+
+        e.preventDefault();
+        var otherUsername = $(e.target).parents("a").attr("data-identity"),
+          mapRequestEl = $(e.target).parents(".pending.identity");
+
+        var viewRef = this;
+
+        var success = function (data, textStatus, xhr) {
+          viewRef.showAlert(
+            "Success! Your account is now linked with the username " +
+              otherUsername,
+            "alert-success",
+            "#pending-alert-container",
+          );
+
+          mapRequestEl.remove();
+        };
+        var error = function (xhr, textStatus, error) {
+          viewRef.showAlert(
+            xhr.responseText,
+            "alert-error",
+            "#pending-alert-container",
+          );
+        };
+
+        //Confirm this map request
+        this.model.confirmMapRequest(otherUsername, success, error);
+      },
+
+      /*
+       * Rejects an identity map request that was initiated by another user, and displays notifications about the result
+       */
+      rejectMapRequest: function (e) {
+        e.preventDefault();
+
+        var equivalentIdentity = $(e.target).parents("a").attr("data-identity"),
+          mapRequestEl = $(e.target).parents(".pending.identity");
+
+        if (!equivalentIdentity) return;
+
+        var viewRef = this,
+          success = function (data) {
+            viewRef.showAlert(
+              "Removed mapping request for " + equivalentIdentity,
+              "alert-success",
+              "#pending-alert-container",
+            );
+            $(mapRequestEl).remove();
+          },
+          error = function (xhr, textStatus, error) {
+            viewRef.showAlert(
+              xhr.responseText,
+              "alert-error",
+              "#pending-alert-container",
+            );
+          };
+
+        this.model.denyMapRequest(equivalentIdentity, success, error);
+      },
+
+      insertIdentityList: function () {
+        var identities = this.model.get("identities");
+
+        //Remove the equivalentIdentities list if it was drawn already so we don't do it twice
+        this.$("#identity-list-container").empty();
+
+        if (!identities) return;
+
+        //Create the list element
+        if (identities.length < 1) {
+          var identityList = $(document.createElement("p")).text(
+            "You haven't linked to another account yet. Send a request below.",
+          );
+        } else
+          var identityList = $(document.createElement("ul"))
+            .addClass("list-identity")
+            .attr("id", "identity-list");
+
+        var view = this;
+        //Create a list item for each identity
+        _.each(identities, function (identity, i) {
+          var listItem = view.createUserListItem(identity, { confirmed: true });
+
+          //When/if the info from the equivalent identities is retrieved, update the item
+          view.listenToOnce(identity, "change:fullName", function (identity) {
+            var newListItem = view.createUserListItem(identity, {
+              confirmed: true,
+            });
+            listItem.replaceWith(newListItem);
+          });
+
+          $(identityList).append(listItem);
+        });
 
-			//Stop listening to changes in models
-			this.stopListening(this.statsModel);
-			this.stopListening(MetacatUI.appUserModel);
+        //Add to the page
+        //$(identityList).find(".collapsed").hide();
+        this.$("#identity-list-container").append(identityList);
+      },
+
+      insertPendingList: function () {
+        var pending = this.model.get("pending");
+
+        //Remove the equivalentIdentities list if it was drawn already so we don't do it twice
+        this.$("#pending-list-container").empty();
+
+        //Create the list element
+        if (pending.length < 1) {
+          this.$("[data-subsection='pending-accounts']").hide();
+          return;
+        } else {
+          this.$("[data-subsection='pending-accounts']").show();
+          this.$("#pending-list-container").prepend(
+            $(document.createElement("p")).text(
+              "You have " +
+                pending.length +
+                " new request to map accounts. If these requests are from you, accept them below. If you do not recognize a username, reject the request.",
+            ),
+          );
+          var pendingList = $(document.createElement("ul"))
+            .addClass("list-identity")
+            .attr("id", "pending-list");
+          var pendingCount = $(document.createElement("span"))
+            .addClass("badge")
+            .attr("id", "pending-count")
+            .text(pending.length);
+          this.$("#pending-list-heading").append(pendingCount);
+        }
+
+        //Create a list item for each pending id
+        var view = this;
+        _.each(pending, function (pendingUser, i) {
+          var listItem = view.createUserListItem(pendingUser, {
+            pending: true,
+          });
+          $(pendingList).append(listItem);
+
+          if (pendingUser.isOrcid()) {
+            view.listenToOnce(
+              pendingUser,
+              "change:fullName",
+              function (pendingUser) {
+                var newListItem = view.createUserListItem(pendingUser, {
+                  pending: true,
+                });
+                listItem.replaceWith(newListItem);
+              },
+            );
+          }
+        });
 
-			//Close the subviews
-			_.each(this.subviews, function(view){
-				view.onClose();
-			});
-			this.subviews = new Array();
-		}
+        //Add to the page
+        this.$("#pending-list-container").append(pendingList);
+      },
+
+      createUserListItem: function (user, options) {
+        var pending = false,
+          confirmed = false;
+
+        if (options && options.pending) pending = true;
+        if (options && options.confirmed) confirmed = true;
+
+        var username = user.get("username"),
+          fullName = user.get("fullName") || username;
+
+        var listItem = $(document.createElement("li")).addClass(
+            "list-group-item identity",
+          ),
+          link = $(document.createElement("a"))
+            .attr("href", MetacatUI.root + "/profile/" + username)
+            .attr("data-identity", username)
+            .text(fullName),
+          details = $(document.createElement("span"))
+            .addClass("subtle details")
+            .text(username);
+
+        listItem.append(link, details);
+
+        if (pending) {
+          var acceptIcon = $(document.createElement("i"))
+              .addClass("icon icon-ok icon-large icon-positive tooltip-this")
+              .attr("data-title", "Accept Request")
+              .attr("data-trigger", "hover")
+              .attr("data-placement", "top"),
+            rejectIcon = $(document.createElement("i"))
+              .addClass(
+                "icon icon-remove icon-large icon-negative tooltip-this",
+              )
+              .attr("data-title", "Reject Request")
+              .attr("data-trigger", "hover")
+              .attr("data-placement", "top"),
+            confirm = $(document.createElement("a"))
+              .attr("href", "#")
+              .addClass("confirm-request-btn")
+              .attr("data-identity", username)
+              .append(acceptIcon),
+            reject = $(document.createElement("a"))
+              .attr("href", "#")
+              .addClass("reject-request-btn")
+              .attr("data-identity", username)
+              .append(rejectIcon);
+
+          listItem.prepend(confirm, reject).addClass("pending");
+        } else if (confirmed) {
+          var removeIcon = $(document.createElement("i")).addClass(
+              "icon icon-remove icon-large icon-negative",
+            ),
+            remove = $(document.createElement("a"))
+              .attr("href", "#")
+              .addClass("remove-identity-btn")
+              .attr("data-identity", username)
+              .append(removeIcon);
+          $(remove).tooltip({
+            trigger: "hover",
+            placement: "top",
+            title: "Remove equivalent account",
+          });
+          listItem.prepend(remove.append(removeIcon));
+        }
+
+        if (user.isOrcid()) {
+          details.prepend(this.createIdPrefix(), " ORCID: ");
+        } else details.prepend(" Username: ");
+
+        return listItem;
+      },
+
+      updateModForm: function () {
+        this.$("#mod-givenName").val(this.model.get("firstName"));
+        this.$("#mod-familyName").val(this.model.get("lastName"));
+        this.$("#mod-email").val(this.model.get("email"));
+
+        if (!this.model.get("email")) {
+          this.$("#mod-email").parent(".form-group").addClass("has-warning");
+          this.$("#mod-email")
+            .parent(".form-group")
+            .find(".help-block")
+            .text("Please provide an email address.");
+        } else {
+          this.$("#mod-email").parent(".form-group").removeClass("has-warning");
+          this.$("#mod-email")
+            .parent(".form-group")
+            .find(".help-block")
+            .text("");
+        }
+
+        if (this.model.get("registered")) {
+          this.$("#registered-user-container").show();
+        } else {
+          this.$("#registered-user-container").hide();
+        }
+      },
+
+      /*
+       * Gets the user account settings, updates the UserModel and saves this new info to the server
+       */
+      saveUser: function (e) {
+        e.preventDefault();
+
+        var view = this,
+          container =
+            this.$('[data-subsection="edit-account"] .content') ||
+            $(e.target).parent();
+
+        var success = function (data) {
+          $(container).find(".loading").detach();
+          $(container).children().show();
+          view.showAlert(
+            "Success! Your profile has been updated.",
+            "alert-success",
+            container,
+          );
+        };
+        var error = function (data) {
+          $(container).find(".loading").detach();
+          $(container).children().show();
+          var msg =
+            data && data.responseText
+              ? data.responseText
+              : "Sorry, updating your profile failed. Please try again.";
+          if (!data.responseText) view.showAlert(msg, "alert-error", container);
+        };
+
+        //Get info entered into form
+        var givenName = this.$("#mod-givenName").val();
+        var familyName = this.$("#mod-familyName").val();
+        var email = this.$("#mod-email").val();
+
+        //Update the model
+        this.model.set("firstName", givenName);
+        this.model.set("lastName", familyName);
+        this.model.set("email", email);
+
+        //Loading icon
+        $(container).children().hide();
+        $(container).prepend(this.loadingTemplate());
+
+        //Send the update
+        this.model.update(success, error);
+      },
+
+      //---------------------------------- Token -----------------------------------------//
+      getToken: function () {
+        var model = this.model;
+
+        //Show loading sign
+        this.$("#token-generator-container").html(this.loadingTemplate());
+
+        //When the token is retrieved, then show it
+        this.listenToOnce(this.model, "change:token", this.showToken);
+
+        //Get the token from the CN
+        this.model.getToken(function (data, textStatus, xhr) {
+          model.getTokenExpiration();
+          model.set("token", data);
+          model.trigger("change:token");
+        });
+      },
+
+      showToken: function () {
+        var token = this.model.get("token");
+
+        if (!token || !this.model.get("loggedIn")) return;
+
+        var expires = this.model.get("expires"),
+          rTokenName =
+            MetacatUI.appModel.get("d1CNBaseUrl").indexOf("cn.dataone.org") > -1
+              ? "dataone_token"
+              : "dataone_test_token",
+          rToken = "options(" + rTokenName + ' = "' + token + '")',
+          matlabToken =
+            "import org.dataone.client.run.RunManager; mgr = RunManager.getInstance(); mgr.configuration.authentication_token = '" +
+            token +
+            "';",
+          tokenInput = $(document.createElement("textarea"))
+            .attr("type", "text")
+            .attr("rows", "5")
+            .addClass("token copy")
+            .text(token),
+          copyButton = $(document.createElement("a"))
+            .addClass("btn btn-primary copy")
+            .text("Copy")
+            .attr("data-clipboard-text", token),
+          copyRButton = $(document.createElement("a"))
+            .addClass("btn btn-primary copy")
+            .text("Copy")
+            .attr("data-clipboard-text", rToken),
+          copyMatlabButton = $(document.createElement("a"))
+            .addClass("btn btn-primary copy")
+            .text("Copy")
+            .attr("data-clipboard-text", matlabToken),
+          successIcon = $(document.createElement("i")).addClass("icon icon-ok"),
+          copySuccess = $(document.createElement("div"))
+            .addClass("notification success copy-success hidden")
+            .append(successIcon, " Copied!"),
+          expirationMsg = expires
+            ? "<strong>Note:</strong> Your authentication token expires on " +
+              expires.toLocaleDateString() +
+              " at " +
+              expires.toLocaleTimeString()
+            : "",
+          usernameMsg = "<div class='footnote'>Your user identity: ",
+          usernamePrefix = this.createIdPrefix(),
+          tabs = $(document.createElement("ul"))
+            .addClass("nav nav-tabs")
+            .append(
+              $(document.createElement("li"))
+                .addClass("active")
+                .append(
+                  $(document.createElement("a"))
+                    .attr("href", "#token-code-panel")
+                    .addClass("token-tab")
+                    .text("Token"),
+                ),
+            )
+            .append(
+              $(document.createElement("li")).append(
+                $(document.createElement("a"))
+                  .attr("href", "#r-token-code-panel")
+                  .addClass("token-tab")
+                  .text("Token for DataONE R"),
+              ),
+            )
+            .append(
+              $(document.createElement("li")).append(
+                $(document.createElement("a"))
+                  .attr("href", "#matlab-token-code-panel")
+                  .addClass("token-tab")
+                  .text("Token for Matlab DataONE Toolbox"),
+              ),
+            ),
+          tokenRInput = $(document.createElement("textarea"))
+            .attr("type", "text")
+            .attr("rows", "5")
+            .addClass("token copy")
+            .text(rToken),
+          tokenRText = $(document.createElement("p")).text(
+            "Copy this code snippet to use your token with the DataONE R package.",
+          ),
+          tokenMatlabInput = $(document.createElement("textarea"))
+            .attr("type", "text")
+            .attr("rows", "5")
+            .addClass("token copy")
+            .text(matlabToken),
+          tokenMatlabText = $(document.createElement("p")).text(
+            "Copy this code snippet to use your token with the Matlab DataONE toolbox.",
+          ),
+          tokenInputContain = $(document.createElement("div"))
+            .attr("id", "token-code-panel")
+            .addClass("tab-panel active")
+            .append(tokenInput, copyButton, copySuccess),
+          rTokenInputContain = $(document.createElement("div"))
+            .attr("id", "r-token-code-panel")
+            .addClass("tab-panel")
+            .append(tokenRText, tokenRInput, copyRButton, copySuccess.clone())
+            .addClass("hidden"),
+          matlabTokenInputContain = $(document.createElement("div"))
+            .attr("id", "matlab-token-code-panel")
+            .addClass("tab-panel")
+            .append(
+              tokenMatlabText,
+              tokenMatlabInput,
+              copyMatlabButton,
+              copySuccess.clone(),
+            )
+            .addClass("hidden");
+
+        if (typeof usernamePrefix == "object")
+          usernameMsg += usernamePrefix[0].outerHTML;
+        else if (typeof usernamePrefix == "string")
+          usernameMsg += usernamePrefix;
+
+        usernameMsg += this.model.get("username") + "</div>";
+
+        var successMessage = $.parseHTML(
+          this.alertTemplate({
+            msg:
+              "Copy your authentication token: <br/> " +
+              expirationMsg +
+              usernameMsg,
+            classes: "alert-success",
+            containerClasses: "well",
+          }),
+        );
+        $(successMessage).append(
+          tabs,
+          tokenInputContain,
+          rTokenInputContain,
+          matlabTokenInputContain,
+        );
+        this.$("#token-generator-container").html(successMessage);
+
+        $(".token-tab").tab();
+
+        //Create clickable "Copy" buttons to copy text (e.g. token) to the user's clipboard
+        var clipboard = new Clipboard(".copy");
+
+        clipboard.on("success", function (e) {
+          $(".copy-success").show().delay(3000).fadeOut();
+        });
 
-	});
+        clipboard.on("error", function (e) {
+          var textarea = $(e.trigger).parent().children("textarea.token");
+          textarea.trigger("focus");
+          textarea.tooltip({
+            title: "Press Ctrl+c to copy",
+            placement: "bottom",
+          });
+          textarea.tooltip("show");
+        });
+      },
+
+      setUpAutocomplete: function () {
+        var input = this.$(".account-autocomplete");
+        if (!input || !input.length) return;
+
+        // look up registered identities
+        $(input).hoverAutocomplete({
+          source: function (request, response) {
+            var term = $.ui.autocomplete.escapeRegex(request.term);
+
+            var list = [];
+
+            //Ids/Usernames that we want to ignore in the autocompelte
+            var ignoreEquivIds =
+                $(this.element).attr("id") == "map-request-field",
+              ignoreIds = ignoreEquivIds
+                ? MetacatUI.appUserModel.get("identitiesUsernames")
+                : [];
+            ignoreIds.push(
+              MetacatUI.appUserModel.get("username").toLowerCase(),
+            );
+
+            var url =
+              MetacatUI.appModel.get("accountsUrl") +
+              "?query=" +
+              encodeURIComponent(term);
+            var requestSettings = {
+              url: url,
+              success: function (data, textStatus, xhr) {
+                _.each($(data).find("person"), function (person, i) {
+                  var item = {};
+                  item.value = $(person).find("subject").text();
+
+                  //Don't display yourself in the autocomplete dropdown (prevents users from adding themselves as an equivalent identity or group member)
+                  //Also don't display your equivalent identities in the autocomplete
+                  if (_.contains(ignoreIds, item.value.toLowerCase())) return;
+
+                  item.label =
+                    $(person).find("fullName").text() ||
+                    $(person).find("givenName").text() +
+                      " " +
+                      $(person).find("familyName").text();
+                  list.push(item);
+                });
+
+                response(list);
+              },
+            };
+            $.ajax(
+              _.extend(
+                requestSettings,
+                MetacatUI.appUserModel.createAjaxSettings(),
+              ),
+            );
+
+            //Send an ORCID search when the search string gets long enough
+            if (request.term.length > 3)
+              MetacatUI.appLookupModel.orcidSearch(
+                request,
+                response,
+                false,
+                ignoreIds,
+              );
+          },
+          select: function (e, ui) {
+            e.preventDefault();
+
+            // set the text field
+            $(e.target).val(ui.item.value);
+            $(e.target)
+              .parents("form")
+              .find("input[name='fullName']")
+              .val(ui.item.label);
+          },
+          position: {
+            my: "left top",
+            at: "left bottom",
+            collision: "none",
+          },
+        });
+      },
+
+      /**
+       * Renders a list of portals that this user is an owner of.
+       */
+      renderMyPortals: function () {
+        //If my portals has been disabled, don't render the list
+        if (MetacatUI.appModel.get("showMyPortals") === false) {
+          return;
+        }
+
+        var view = this;
+
+        //If Bookkeeper services are enabled, render the Portals via a PortalUsagesView,
+        // which queries Bookkeeper for portal Usages
+        if (MetacatUI.appModel.get("enableBookkeeperServices")) {
+          require(["views/portals/PortalUsagesView"], function (
+            PortalUsagesView,
+          ) {
+            var portalListView = new PortalUsagesView();
+            //Render the Portal list view and insert it in the page
+            portalListView.render();
+            view.$(view.portalListContainer).html(portalListView.el);
+          });
+        }
+        //If Bookkeeper services are disabled, render the Portals via a PortalListView,
+        // which queries Solr for portal docs
+        else {
+          require(["views/portals/PortalListView"], function (PortalListView) {
+            //Create a PortalListView
+            var portalListView = new PortalListView();
+            //Render the Portal list view and insert it in the page
+            portalListView.render();
+            view.$(view.portalListContainer).html(portalListView.el);
+          });
+        }
+      },
+
+      //---------------------------------- Misc. and Utilities -----------------------------------------//
+
+      showAlert: function (msg, classes, container) {
+        if (!classes) var classes = "alert-success";
+        if (!container || !$(container).length) var container = this.$el;
+
+        //Remove any alerts that are already in this container
+        if ($(container).children(".alert-container").length > 0)
+          $(container).children(".alert-container").remove();
+
+        $(container).prepend(
+          this.alertTemplate({
+            msg: msg,
+            classes: classes,
+          }),
+        );
+      },
+
+      switchTabs: function (e) {
+        e.preventDefault();
+        $(e.target).tab("show");
+        this.$(".tab-panel").hide();
+        this.$(".tab-panel" + $(e.target).attr("href")).show();
+        this.$("#token-generator-container .copy-button").attr(
+          "data-clipboard-text",
+        );
+      },
+
+      preventSubmit: function (e) {
+        if (e.keyCode != 13) return;
+
+        e.preventDefault();
+      },
+
+      onClose: function () {
+        //Clear the template
+        this.$el.html("");
+
+        //Reset the active section and subsection
+        this.activeSection = "profile";
+        this.activeSubSection = "";
+
+        //Reset the model
+        if (this.model) {
+          this.model.noActivity = null;
+          this.stopListening(this.model);
+        }
+
+        //Remove saved elements
+        this.$profile = null;
+
+        //Stop listening to changes in models
+        this.stopListening(this.statsModel);
+        this.stopListening(MetacatUI.appUserModel);
+
+        //Close the subviews
+        _.each(this.subviews, function (view) {
+          view.onClose();
+        });
+        this.subviews = new Array();
+      },
+    },
+  );
 
-	return UserView;
+  return UserView;
 });
 
diff --git a/docs/docs/src_js_views_citations_CitationModalView.js.html b/docs/docs/src_js_views_citations_CitationModalView.js.html index 13bd8d9b9..d932a65f9 100644 --- a/docs/docs/src_js_views_citations_CitationModalView.js.html +++ b/docs/docs/src_js_views_citations_CitationModalView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/citations/CitationModalView.js

-
/*global define */
-define([
+            
define([
   "jquery",
   "underscore",
   "backbone",
@@ -85,7 +84,7 @@ 

Source: src/js/views/citations/CitationModalView.js

* @type {Underscore.Template} */ innerButtonTemplate: _.template( - "<i class='icon <%=icon%> icon-on-left'></i> <%=text%>" + "<i class='icon <%=icon%> icon-on-left'></i> <%=text%>", ), /** @@ -215,7 +214,7 @@

Source: src/js/views/citations/CitationModalView.js

// Find the citation container this.citationContainer = this.el.querySelector( - "#" + ids.citationContainer + "#" + ids.citationContainer, ); // Find the citation button @@ -304,7 +303,7 @@

Source: src/js/views/citations/CitationModalView.js

textarea.trigger("focus"); textarea.tooltip("show"); }, - } + }, ); textarea.focusout(function () { textarea.animate({ width: "0px" }, function () { @@ -337,7 +336,7 @@

Source: src/js/views/citations/CitationModalView.js

onClose: function () { this.teardown(); }, - } + }, ); return CitationModalView; diff --git a/docs/docs/src_js_views_filters_BooleanFilterView.js.html b/docs/docs/src_js_views_filters_BooleanFilterView.js.html index fc5fc8e12..56eeb3956 100644 --- a/docs/docs/src_js_views_filters_BooleanFilterView.js.html +++ b/docs/docs/src_js_views_filters_BooleanFilterView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/filters/BooleanFilterView.js

-
/*global define */
-define([
+            
define([
   "jquery",
   "underscore",
   "backbone",
@@ -75,14 +74,13 @@ 

Source: src/js/views/filters/BooleanFilterView.js

/** * @inheritdoc */ - events: function(){ + events: function () { try { const events = FilterView.prototype.events.call(this); events["click input[type='checkbox']"] = "updateModel"; - return events - } - catch (e) { - console.log('Failed to create events for BooleanFilterView: ' + e); + return events; + } catch (e) { + console.log("Failed to create events for BooleanFilterView: " + e); return {}; } }, @@ -119,7 +117,7 @@

Source: src/js/views/filters/BooleanFilterView.js

//Update the checkbox based on the model value this.$("input[type='checkbox']").prop("checked", modelValue); }, - } + }, ); return BooleanFilterView; }); diff --git a/docs/docs/src_js_views_filters_ChoiceFilterView.js.html b/docs/docs/src_js_views_filters_ChoiceFilterView.js.html index ba48eaad4..0234276f8 100644 --- a/docs/docs/src_js_views_filters_ChoiceFilterView.js.html +++ b/docs/docs/src_js_views_filters_ChoiceFilterView.js.html @@ -44,503 +44,533 @@

Source: src/js/views/filters/ChoiceFilterView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone',
-        'models/filters/ChoiceFilter',
-        'views/filters/FilterView',
-        'text!templates/filters/choiceFilter.html'],
-  function($, _, Backbone, ChoiceFilter, FilterView, Template) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/filters/ChoiceFilter",
+  "views/filters/FilterView",
+  "text!templates/filters/choiceFilter.html",
+], function ($, _, Backbone, ChoiceFilter, FilterView, Template) {
+  "use strict";
 
   /**
-  * @class ChoiceFilterView
-  * @classdesc Render a view of a single ChoiceFilter model
-  * @classcategory Views/Filters
-  * @extends FilterView
-  */
+   * @class ChoiceFilterView
+   * @classdesc Render a view of a single ChoiceFilter model
+   * @classcategory Views/Filters
+   * @extends FilterView
+   */
   var ChoiceFilterView = FilterView.extend(
-    /** @lends ChoiceFilterView.prototype */{
-
-    /**
-    * A ChoiceFilter model to be rendered in this view
-    * @type {ChoiceFilter} */
-    model: null,
-
-    /**
-     * @inheritdoc
-     */
-    modelClass: ChoiceFilter,
-
-    className: "filter choice",
-
-    template: _.template(Template),
-
-    /**
-    * When this view is in "uiBuilder" mode, the class name for the handles on each choice
-    * row that the user can click and drag to re-order
-    * @type {string}
-    */
-    choiceHandleClass: "handle",
-
-    /**
-     * The class to add to the element that a user should click to remove a choice
-     * value and label when this view is in "uiEditor" mode
-     * @since 2.17.0
-     * @type {string}
-     */
-    removeChoiceClass: "remove-choice",
-
-    /**
-     * A function that creates and returns the Backbone events object.
-     * @return {Object} Returns a Backbone events object
-     */
-    events: function () {
-      try {
-        var events = FilterView.prototype.events.call(this);
-        events["change select"] = "handleChange";
-        var removeClass = "." + this.removeChoiceClass;
-        events["click " + removeClass] = "removeChoice";
-        events["mouseover " + removeClass] = "previewRemoveChoice";
-        events["mouseout " + removeClass] = "previewRemoveChoice";
-        return events
-      }
-      catch (error) {
-        console.log( 'There was an error creating the events object for a ChoiceFilterView' +
-          ' Error details: ' + error );
-      }
-    },
-
-    render: function () {
-
-      var view = this;
-
-      // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
-      FilterView.prototype.render.call(this)
-
-      var placeHolderText = this.model.get("placeholder");
-      var select = this.$("select");
-
-      if(this.mode === "uiBuilder"){
-
-        // If this is the filter view where the user can edit the filter UI options,
-        // like the label, placeholder text, and choices, then render the inputs
-        // for these options.
-
-        // The ignore-changes class prevents the editor footer from showing on keypress
-        var placeholderInput = $(
-          '<input placeholder="placeholder" class="' + this.uiInputClass +
-          ' placeholder ignore-changes" data-category="placeholder" value="' +
-          (placeHolderText ? placeHolderText : '') +'" />'
-        );
-        // Replace the select element with the placeholder text element
-        placeholderInput.insertAfter(select);
-
-        // Create the interface for a user to edit the value-label choice options
-        var choicesEditor = this.createChoicesEditor();
-        view.$el.append(choicesEditor);
-
-      } else {
-        // For regular search filter views, or the edit filter view, render the dropdown
-        // interface
-
-        //Create the placeholder text for the dropdown menu
-
-        //If placeholder text is already provided in the model, use it
-        //If not, create placeholder text using the model label
-        if (!placeHolderText){
-          if (this.model.get("label")){
-            //If the label starts with a vowel, use "an"
-            var vowels = ["a", "e", "i", "o", "u"];
-            if (_.contains(vowels, this.model.get("label").toLowerCase().charAt(0))) {
-              placeHolderText = "Choose an " + this.model.get("label");
-            }
-            //Otherwise use "a"
-            else {
-              placeHolderText = "Choose a " + this.model.get("label");
-            }
-          }
-
+    /** @lends ChoiceFilterView.prototype */ {
+      /**
+       * A ChoiceFilter model to be rendered in this view
+       * @type {ChoiceFilter} */
+      model: null,
+
+      /**
+       * @inheritdoc
+       */
+      modelClass: ChoiceFilter,
+
+      className: "filter choice",
+
+      template: _.template(Template),
+
+      /**
+       * When this view is in "uiBuilder" mode, the class name for the handles on each choice
+       * row that the user can click and drag to re-order
+       * @type {string}
+       */
+      choiceHandleClass: "handle",
+
+      /**
+       * The class to add to the element that a user should click to remove a choice
+       * value and label when this view is in "uiEditor" mode
+       * @since 2.17.0
+       * @type {string}
+       */
+      removeChoiceClass: "remove-choice",
+
+      /**
+       * A function that creates and returns the Backbone events object.
+       * @return {Object} Returns a Backbone events object
+       */
+      events: function () {
+        try {
+          var events = FilterView.prototype.events.call(this);
+          events["change select"] = "handleChange";
+          var removeClass = "." + this.removeChoiceClass;
+          events["click " + removeClass] = "removeChoice";
+          events["mouseover " + removeClass] = "previewRemoveChoice";
+          events["mouseout " + removeClass] = "previewRemoveChoice";
+          return events;
+        } catch (error) {
+          console.log(
+            "There was an error creating the events object for a ChoiceFilterView" +
+              " Error details: " +
+              error,
+          );
         }
+      },
 
-        //Create the default option
-        var defaultOption = $(document.createElement("option"))
-                              .attr("value", "")
-                              .text( placeHolderText );
-        select.append(defaultOption);
-
-        //Create an option element for each choice listen in the model
-        _.each( this.model.get("choices"), function(choice){
-          select.append( $(document.createElement("option"))
-                          .attr("value", choice.value)
-                          .text(choice.label) );
-        }, this );
-
-        //When the ChoiceFilter is changed, update the choice list in the UI
-        this.listenTo(this.model, "change:values", this.updateChoices);
-        this.listenTo(this.model, "remove", this.updateChoices);
-      }
-
-
-    },
-
-    /**
-     * Create the set of inputs where a use can select label-value pairs for the regular
-     * choice filter view
-     * @since 2.17.0
-     */
-    createChoicesEditor: function(){
-
-      try {
-
+      render: function () {
         var view = this;
-        this.choicesEditor = $("<div class='choices-editor'></div>");
-        var choicesEditorText = $("<p class='modal-instructions'>Allow people to select from the following search terms</p>");
-        var choiceEditorError = $("<p class='notification error' data-category='choices' style='display: none'></p>");
-        var labelContainer = $("<div class='choice-editor unsortable'></div>");
 
-        this.choicesEditor.append(choicesEditorText, choiceEditorError, labelContainer)
-
-        labelContainer.append("<p class='ui-builder-container-text choice-label subtle'>Enter the text to display</p>")
-        labelContainer.append("<p class='ui-builder-container-text choice-value subtle'>Enter the text to search for</p>")
+        // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
+        FilterView.prototype.render.call(this);
+
+        var placeHolderText = this.model.get("placeholder");
+        var select = this.$("select");
+
+        if (this.mode === "uiBuilder") {
+          // If this is the filter view where the user can edit the filter UI options,
+          // like the label, placeholder text, and choices, then render the inputs
+          // for these options.
+
+          // The ignore-changes class prevents the editor footer from showing on keypress
+          var placeholderInput = $(
+            '<input placeholder="placeholder" class="' +
+              this.uiInputClass +
+              ' placeholder ignore-changes" data-category="placeholder" value="' +
+              (placeHolderText ? placeHolderText : "") +
+              '" />',
+          );
+          // Replace the select element with the placeholder text element
+          placeholderInput.insertAfter(select);
+
+          // Create the interface for a user to edit the value-label choice options
+          var choicesEditor = this.createChoicesEditor();
+          view.$el.append(choicesEditor);
+        } else {
+          // For regular search filter views, or the edit filter view, render the dropdown
+          // interface
+
+          //Create the placeholder text for the dropdown menu
+
+          //If placeholder text is already provided in the model, use it
+          //If not, create placeholder text using the model label
+          if (!placeHolderText) {
+            if (this.model.get("label")) {
+              //If the label starts with a vowel, use "an"
+              var vowels = ["a", "e", "i", "o", "u"];
+              if (
+                _.contains(
+                  vowels,
+                  this.model.get("label").toLowerCase().charAt(0),
+                )
+              ) {
+                placeHolderText = "Choose an " + this.model.get("label");
+              }
+              //Otherwise use "a"
+              else {
+                placeHolderText = "Choose a " + this.model.get("label");
+              }
+            }
+          }
 
-        _.each(this.model.get("choices"), function (choice) {
-          var choiceEditorEl = this.createChoiceEditor(choice);
-          this.choicesEditor.append(choiceEditorEl)
-        }, this);
-
-        // Create a blank choice at the end
-        this.addEmptyChoiceEditor();
-
-        // Initialize choice drag and drop to re-order functionality
-        require(['sortable'], function(Sortable){
-          Sortable.create(view.choicesEditor[0], {
-            direction: 'vertical',
-            easing: "cubic-bezier(1, 0, 0, 1)",
-            animation: 200,
-            handle: "." + view.choiceHandleClass,
-            draggable: ".choice-editor:not(.unsortable)",
-            onUpdate: function (evt) {
-              // When the choice order is changed, update the filter model
-              view.updateModelChoices()
+          //Create the default option
+          var defaultOption = $(document.createElement("option"))
+            .attr("value", "")
+            .text(placeHolderText);
+          select.append(defaultOption);
+
+          //Create an option element for each choice listen in the model
+          _.each(
+            this.model.get("choices"),
+            function (choice) {
+              select.append(
+                $(document.createElement("option"))
+                  .attr("value", choice.value)
+                  .text(choice.label),
+              );
             },
-          })
-        })
-
-        return this.choicesEditor
-      }
-      catch (error) {
-        console.log( 'There was an error creating choices editor in a ChoiceFilterView' +
-          ' Error details: ' + error );
-      }
+            this,
+          );
 
-    },
-
-    /**
-     * Create a row where a user can input a value and label for a single choice.
-     * @since 2.17.0
-     */
-    createChoiceEditor: function(choice){
-      try {
-        if (!choice) {
-          return
+          //When the ChoiceFilter is changed, update the choice list in the UI
+          this.listenTo(this.model, "change:values", this.updateChoices);
+          this.listenTo(this.model, "remove", this.updateChoices);
         }
-
-        var view = this;
-
-        // Create the choice container
-        var choiceContainer = $("<div class='choice-editor'></div>");
-
-        // Create the click and drag handle
-        var handle = $('<span class="' + view.choiceHandleClass + '">' +
-            '<i class= "icon icon-ellipsis-vertical" ></i>' +
-            '<i class="icon icon-ellipsis-vertical"></i>' +
-          '</span >'
-        );
-        choiceContainer.append(handle);
-
-        // Create inputs for "value" and "label", insert them in the container
-        for (const [attrName, attrValue] of Object.entries(choice)) {
-          var inputEl = $('<input>').attr({
-            // The ignore-changes class prevents the editor footer from showing on keypress
-            class: 'ignore-changes choice-input choice-' + attrName,
-            value: attrValue,
-            "data-category": attrName
-          })
-          // Update the values in the model when the user focuses out of an input
-          inputEl.on("blur", function(){
-            view.updateModelChoices.call(view)
-          })
-          choiceContainer.append(inputEl);
+      },
+
+      /**
+       * Create the set of inputs where a use can select label-value pairs for the regular
+       * choice filter view
+       * @since 2.17.0
+       */
+      createChoicesEditor: function () {
+        try {
+          var view = this;
+          this.choicesEditor = $("<div class='choices-editor'></div>");
+          var choicesEditorText = $(
+            "<p class='modal-instructions'>Allow people to select from the following search terms</p>",
+          );
+          var choiceEditorError = $(
+            "<p class='notification error' data-category='choices' style='display: none'></p>",
+          );
+          var labelContainer = $(
+            "<div class='choice-editor unsortable'></div>",
+          );
+
+          this.choicesEditor.append(
+            choicesEditorText,
+            choiceEditorError,
+            labelContainer,
+          );
+
+          labelContainer.append(
+            "<p class='ui-builder-container-text choice-label subtle'>Enter the text to display</p>",
+          );
+          labelContainer.append(
+            "<p class='ui-builder-container-text choice-value subtle'>Enter the text to search for</p>",
+          );
+
+          _.each(
+            this.model.get("choices"),
+            function (choice) {
+              var choiceEditorEl = this.createChoiceEditor(choice);
+              this.choicesEditor.append(choiceEditorEl);
+            },
+            this,
+          );
+
+          // Create a blank choice at the end
+          this.addEmptyChoiceEditor();
+
+          // Initialize choice drag and drop to re-order functionality
+          require(["sortable"], function (Sortable) {
+            Sortable.create(view.choicesEditor[0], {
+              direction: "vertical",
+              easing: "cubic-bezier(1, 0, 0, 1)",
+              animation: 200,
+              handle: "." + view.choiceHandleClass,
+              draggable: ".choice-editor:not(.unsortable)",
+              onUpdate: function (evt) {
+                // When the choice order is changed, update the filter model
+                view.updateModelChoices();
+              },
+            });
+          });
+
+          return this.choicesEditor;
+        } catch (error) {
+          console.log(
+            "There was an error creating choices editor in a ChoiceFilterView" +
+              " Error details: " +
+              error,
+          );
         }
+      },
+
+      /**
+       * Create a row where a user can input a value and label for a single choice.
+       * @since 2.17.0
+       */
+      createChoiceEditor: function (choice) {
+        try {
+          if (!choice) {
+            return;
+          }
 
-        // Create the remove "X" button. Save references to the parent choice container so
-        // that we can remove it from the view when the button is clicked
-        var removeButton = $(
-          "<i class='icon icon-remove " +
-          this.removeChoiceClass +
-          "' title='Remove this choice'></i>"
-        ).data({
-          choiceEl: choiceContainer
-        });
-
-        // Insert the remove button into the choice container
-        choiceContainer.append(removeButton);
-        return choiceContainer
-      }
-      catch (error) {
-        console.log( 'There was an error  ChoiceFilterView' +
-          ' Error details: ' + error );
-      }
-
-    },
+          var view = this;
+
+          // Create the choice container
+          var choiceContainer = $("<div class='choice-editor'></div>");
+
+          // Create the click and drag handle
+          var handle = $(
+            '<span class="' +
+              view.choiceHandleClass +
+              '">' +
+              '<i class= "icon icon-ellipsis-vertical" ></i>' +
+              '<i class="icon icon-ellipsis-vertical"></i>' +
+              "</span >",
+          );
+          choiceContainer.append(handle);
+
+          // Create inputs for "value" and "label", insert them in the container
+          for (const [attrName, attrValue] of Object.entries(choice)) {
+            var inputEl = $("<input>").attr({
+              // The ignore-changes class prevents the editor footer from showing on keypress
+              class: "ignore-changes choice-input choice-" + attrName,
+              value: attrValue,
+              "data-category": attrName,
+            });
+            // Update the values in the model when the user focuses out of an input
+            inputEl.on("blur", function () {
+              view.updateModelChoices.call(view);
+            });
+            choiceContainer.append(inputEl);
+          }
 
-    /**
-     * Create an empty choice editor row
-     * @since 2.17.0
-     */
-    addEmptyChoiceEditor: function () {
-      try {
-        var view = this;
-        var choice = {
-          label: "",
-          value: ""
+          // Create the remove "X" button. Save references to the parent choice container so
+          // that we can remove it from the view when the button is clicked
+          var removeButton = $(
+            "<i class='icon icon-remove " +
+              this.removeChoiceClass +
+              "' title='Remove this choice'></i>",
+          ).data({
+            choiceEl: choiceContainer,
+          });
+
+          // Insert the remove button into the choice container
+          choiceContainer.append(removeButton);
+          return choiceContainer;
+        } catch (error) {
+          console.log(
+            "There was an error  ChoiceFilterView" + " Error details: " + error,
+          );
         }
-        var choiceEditorEl = this.createChoiceEditor(choice);
-        this.choicesEditor.append(choiceEditorEl)
-
-        // Don't let users remove or sort the new choice entry fields until some text has
-        // been entered
-        var removeButton = choiceEditorEl.find("." + this.removeChoiceClass);
-        var handle = choiceEditorEl.find("." + this.choiceHandleClass);
-        removeButton.hide();
-        handle.hide();
-        choiceEditorEl.addClass("unsortable");
-        // The inputs for value and label
-        var inputs = choiceEditorEl.find("input");
-
-        var onInputChange = function () {
-          choiceEditorEl.removeClass("unsortable")
-          removeButton.show();
-          handle.show();
-          view.addEmptyChoiceEditor();
-          inputs.off("input", onInputChange);
+      },
+
+      /**
+       * Create an empty choice editor row
+       * @since 2.17.0
+       */
+      addEmptyChoiceEditor: function () {
+        try {
+          var view = this;
+          var choice = {
+            label: "",
+            value: "",
+          };
+          var choiceEditorEl = this.createChoiceEditor(choice);
+          this.choicesEditor.append(choiceEditorEl);
+
+          // Don't let users remove or sort the new choice entry fields until some text has
+          // been entered
+          var removeButton = choiceEditorEl.find("." + this.removeChoiceClass);
+          var handle = choiceEditorEl.find("." + this.choiceHandleClass);
+          removeButton.hide();
+          handle.hide();
+          choiceEditorEl.addClass("unsortable");
+          // The inputs for value and label
+          var inputs = choiceEditorEl.find("input");
+
+          var onInputChange = function () {
+            choiceEditorEl.removeClass("unsortable");
+            removeButton.show();
+            handle.show();
+            view.addEmptyChoiceEditor();
+            inputs.off("input", onInputChange);
+          };
+          inputs.on("input", onInputChange);
+        } catch (error) {
+          console.log(
+            "There was an error creating a choice editor in a ChoiceFilterView" +
+              " Error details: " +
+              error,
+          );
         }
-        inputs.on("input", onInputChange);
-      }
-      catch (error) {
-        console.log('There was an error creating a choice editor in a ChoiceFilterView' +
-          ' Error details: ' + error);
-      }
-    },
-
-    /**
-     * Indicate to the user that the choice value and label inputs will be removed when
-     * they hover over the remove button.
-     * @since 2.17.0
-     */
-    previewRemoveChoice: function (e) {
-      try {
-
-        var normalOpacity = 1.0,
+      },
+
+      /**
+       * Indicate to the user that the choice value and label inputs will be removed when
+       * they hover over the remove button.
+       * @since 2.17.0
+       */
+      previewRemoveChoice: function (e) {
+        try {
+          var normalOpacity = 1.0,
             previewOpacity = 0.2,
             speed = 120;
 
-        var removeEl = $(e.target);
-        var subElements = removeEl.data("choiceEl").children().not(removeEl);
+          var removeEl = $(e.target);
+          var subElements = removeEl.data("choiceEl").children().not(removeEl);
 
-        if(e.type === "mouseover"){
-          subElements.fadeTo(speed, previewOpacity)
-          $(removeEl).fadeTo(speed, normalOpacity)
-        }
-        if(e.type === "mouseout"){
-          subElements.fadeTo(speed, normalOpacity)
-          $(removeEl).fadeTo(speed, previewOpacity)
+          if (e.type === "mouseover") {
+            subElements.fadeTo(speed, previewOpacity);
+            $(removeEl).fadeTo(speed, normalOpacity);
+          }
+          if (e.type === "mouseout") {
+            subElements.fadeTo(speed, normalOpacity);
+            $(removeEl).fadeTo(speed, previewOpacity);
+          }
+        } catch (error) {
+          console.log(
+            "Error showing a preview of the removal of a Choice editor in a " +
+              "Choice Filter View, details: " +
+              error,
+          );
         }
-
-      } catch (error) {
-        console.log("Error showing a preview of the removal of a Choice editor in a " +
-          "Choice Filter View, details: " + error);
-      }
-    },
-
-    /**
-     * Remove a choice editor row and the corresponding label-value pair from the choice
-     * Filter Model (TODO)
-     * @since 2.17.0
-     * @param {Object} e The click event object
-     */
-    removeChoice: function(e){
-      try {
-        var choiceEl = $(e.target).data("choiceEl");
-
-        // See how many choice elements there are (subtract one because the label elements
-        // are within a choice-editor element)
-        var numChoices = this.$el.find(".choice-editor").length - 1
-
-        // Don't allow removing the last choice element. Empty the last element and hide the
-        // remove button instead.
-        if (numChoices <= 1) {
-          choiceEl.find("input").val('')
-        } else {
-          // Remove the choice editor element from the view, plus any listeners
-          choiceEl.off();
-          choiceEl.remove();
+      },
+
+      /**
+       * Remove a choice editor row and the corresponding label-value pair from the choice
+       * Filter Model (TODO)
+       * @since 2.17.0
+       * @param {Object} e The click event object
+       */
+      removeChoice: function (e) {
+        try {
+          var choiceEl = $(e.target).data("choiceEl");
+
+          // See how many choice elements there are (subtract one because the label elements
+          // are within a choice-editor element)
+          var numChoices = this.$el.find(".choice-editor").length - 1;
+
+          // Don't allow removing the last choice element. Empty the last element and hide the
+          // remove button instead.
+          if (numChoices <= 1) {
+            choiceEl.find("input").val("");
+          } else {
+            // Remove the choice editor element from the view, plus any listeners
+            choiceEl.off();
+            choiceEl.remove();
+          }
+          // Update the choices in the model
+          this.updateModelChoices();
+        } catch (error) {
+          console.log(
+            "There was an error removing a choice editor in the ChoiceFilterView" +
+              " Error details: " +
+              error,
+          );
         }
-        // Update the choices in the model
-        this.updateModelChoices();
-      }
-      catch (error) {
-        console.log( 'There was an error removing a choice editor in the ChoiceFilterView' +
-          ' Error details: ' + error );
-      }
-
-    },
-
-    /**
-     * Update the choices attribute in the choiceFilter model based on the values in the
-     * choices editor
-     * @since 2.17.0
-     */
-    updateModelChoices: function(){
-      try {
-
-        // The array of label-value pairs that will be set on the choiceFilter model.
-        var newChoices = [];
-
-        // Find each choice editor container, and find the values from the two inputs
-        // within.
-        this.$el.find(".choice-editor").each(function(){
-          var choiceEditor = $(this)
-          var valueEl = choiceEditor.find("[data-category='value']")
-          var labelEl = choiceEditor.find("[data-category='label']")
-          if (valueEl.length && labelEl.length ){
-            var newValue = valueEl[0].value
-            var newLabel = labelEl[0].value
-            // TODO: validate the label/value here and show error if choice is not
-            // complete.
-            if(!newValue && !newLabel){
-              // Don't add empty choices to the model
-              return
-            } else {
-              newChoices.push({
-                label: newLabel,
-                value: newValue
-              })
+      },
+
+      /**
+       * Update the choices attribute in the choiceFilter model based on the values in the
+       * choices editor
+       * @since 2.17.0
+       */
+      updateModelChoices: function () {
+        try {
+          // The array of label-value pairs that will be set on the choiceFilter model.
+          var newChoices = [];
+
+          // Find each choice editor container, and find the values from the two inputs
+          // within.
+          this.$el.find(".choice-editor").each(function () {
+            var choiceEditor = $(this);
+            var valueEl = choiceEditor.find("[data-category='value']");
+            var labelEl = choiceEditor.find("[data-category='label']");
+            if (valueEl.length && labelEl.length) {
+              var newValue = valueEl[0].value;
+              var newLabel = labelEl[0].value;
+              // TODO: validate the label/value here and show error if choice is not
+              // complete.
+              if (!newValue && !newLabel) {
+                // Don't add empty choices to the model
+                return;
+              } else {
+                newChoices.push({
+                  label: newLabel,
+                  value: newValue,
+                });
+              }
             }
-          }
-        });
-        // Replace the choices in the model with the new array with new values
-        this.model.set("choices", newChoices);
-      }
-      catch (error) {
-        console.log( 'There was an error updating the choices in a ChoiceFilterView' +
-          ' Error details: ' + error );
-      }
-    },
-
-    /**
-    * Updates the value set on the ChoiceFilter Model associated with this view.
-    * The filter value is grabbed from the select element in this view.
-    *
-    */
-    updateModel: function(){
-
-      //Get the new value from the text input
-      var newValue = this.$("select").val();
-
-      //Get the current values array from the model
-      var currentValue = this.model.get("values");
-
-      //If the ChoiceFilter allows multiple values to be added,
-      // add the new choice to the values array
-      if( this.model.get("chooseMultiple") ){
-
-        //Duplicate the current values array
-        var newValuesArray = currentValue.slice(0);
-
-        //Add the new value to the array
-        newValuesArray.push(newValue);
-
-        //Set the new values array on the model
-        this.model.set("values", newValuesArray);
-
-      }
-      //If multiple choices are not allowed,
-      else{
-
-        //Replace the first index of the array with the new value
-        var newValuesArray = currentValue.slice(0);
-        newValuesArray[0] = newValue;
-
-        //Set the new values array on the model
-        this.model.set("values", newValuesArray);
-      }
-    },
-
-    /**
-    * Update the choices in the select dropdown menu based on which choices are
-    * currently selected
-    */
-    updateChoices: function(){
-
-      //Enable all the choices
-      this.$("option").prop("disabled", false);
-
-      //Get the currently-selected choices
-      var selectedChoices = this.model.get("values");
-
-      _.each(selectedChoices, function(choice){
-
-        //Find each choice in the dropdown menu and disable it
-        this.$("option[value='" + choice + "']").prop("disabled", true);
-
-      }, this);
-
-    },
-    
-    /**
-     * Show validation errors. This is used for filters that are in "UIBuilder" mode.
-     * @param {Object} errors The error messages associated with each attribute that has
-     * an error, passed from the Filter model validation function.
-     */
-    showValidationErrors: function (errors) {
-      try {
-
-        var view = this;
-        // Select the messages container for the choice error (added in the template)
-        var messageContainer = view.el.querySelector(".notification[data-category='choices']");
-
-        // Show errors for label, placeholder, etc (elements common to all FilterViews)
-        FilterView.prototype.showValidationErrors.call(this, errors);
-
-        // Show errors in the choices editor
-        var inputs = this.choicesEditor.find("input");
-
-        // Add error styling to all the choices inputs. Remove error styling (and input
-        // listeners) from all inputs when there is text in at least one of them
-        var handleInput = function () {
-          inputs.each(function (i, input) {
-            view.hideInputError(input, messageContainer)
-            input.removeEventListener('input', handleInput)
-          })
+          });
+          // Replace the choices in the model with the new array with new values
+          this.model.set("choices", newChoices);
+        } catch (error) {
+          console.log(
+            "There was an error updating the choices in a ChoiceFilterView" +
+              " Error details: " +
+              error,
+          );
         }
-        if (inputs.length) {
-          inputs.each(function (i, input) {
-            view.showInputError(input);
-            input.addEventListener('input', handleInput);
-          })
+      },
+
+      /**
+       * Updates the value set on the ChoiceFilter Model associated with this view.
+       * The filter value is grabbed from the select element in this view.
+       *
+       */
+      updateModel: function () {
+        //Get the new value from the text input
+        var newValue = this.$("select").val();
+
+        //Get the current values array from the model
+        var currentValue = this.model.get("values");
+
+        //If the ChoiceFilter allows multiple values to be added,
+        // add the new choice to the values array
+        if (this.model.get("chooseMultiple")) {
+          //Duplicate the current values array
+          var newValuesArray = currentValue.slice(0);
+
+          //Add the new value to the array
+          newValuesArray.push(newValue);
+
+          //Set the new values array on the model
+          this.model.set("values", newValuesArray);
         }
-
-      }
-      catch (error) {
-        console.log(
-          'There was an error showing validation errors in a FilterView' +
-          '. Error details: ' + error
+        //If multiple choices are not allowed,
+        else {
+          //Replace the first index of the array with the new value
+          var newValuesArray = currentValue.slice(0);
+          newValuesArray[0] = newValue;
+
+          //Set the new values array on the model
+          this.model.set("values", newValuesArray);
+        }
+      },
+
+      /**
+       * Update the choices in the select dropdown menu based on which choices are
+       * currently selected
+       */
+      updateChoices: function () {
+        //Enable all the choices
+        this.$("option").prop("disabled", false);
+
+        //Get the currently-selected choices
+        var selectedChoices = this.model.get("values");
+
+        _.each(
+          selectedChoices,
+          function (choice) {
+            //Find each choice in the dropdown menu and disable it
+            this.$("option[value='" + choice + "']").prop("disabled", true);
+          },
+          this,
         );
-      }
+      },
+
+      /**
+       * Show validation errors. This is used for filters that are in "UIBuilder" mode.
+       * @param {Object} errors The error messages associated with each attribute that has
+       * an error, passed from the Filter model validation function.
+       */
+      showValidationErrors: function (errors) {
+        try {
+          var view = this;
+          // Select the messages container for the choice error (added in the template)
+          var messageContainer = view.el.querySelector(
+            ".notification[data-category='choices']",
+          );
+
+          // Show errors for label, placeholder, etc (elements common to all FilterViews)
+          FilterView.prototype.showValidationErrors.call(this, errors);
+
+          // Show errors in the choices editor
+          var inputs = this.choicesEditor.find("input");
+
+          // Add error styling to all the choices inputs. Remove error styling (and input
+          // listeners) from all inputs when there is text in at least one of them
+          var handleInput = function () {
+            inputs.each(function (i, input) {
+              view.hideInputError(input, messageContainer);
+              input.removeEventListener("input", handleInput);
+            });
+          };
+          if (inputs.length) {
+            inputs.each(function (i, input) {
+              view.showInputError(input);
+              input.addEventListener("input", handleInput);
+            });
+          }
+        } catch (error) {
+          console.log(
+            "There was an error showing validation errors in a FilterView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
     },
-
-  });
+  );
   return ChoiceFilterView;
 });
 
diff --git a/docs/docs/src_js_views_filters_DateFilterView.js.html b/docs/docs/src_js_views_filters_DateFilterView.js.html index 504e8e586..e6e5baf41 100644 --- a/docs/docs/src_js_views_filters_DateFilterView.js.html +++ b/docs/docs/src_js_views_filters_DateFilterView.js.html @@ -44,173 +44,179 @@

Source: src/js/views/filters/DateFilterView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone',
-        'models/filters/DateFilter',
-        'views/filters/FilterView',
-        'text!templates/filters/dateFilter.html'],
-  function($, _, Backbone, DateFilter, FilterView, Template) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/filters/DateFilter",
+  "views/filters/FilterView",
+  "text!templates/filters/dateFilter.html",
+], function ($, _, Backbone, DateFilter, FilterView, Template) {
+  "use strict";
 
   /**
-  * @class DateFilterView
-  * @classdesc Render a view of a single DateFilter model
-  * @classcategory Views/Filters
-  * @extends FilterView
-  */
+   * @class DateFilterView
+   * @classdesc Render a view of a single DateFilter model
+   * @classcategory Views/Filters
+   * @extends FilterView
+   */
   var DateFilterView = FilterView.extend(
-    /** @lends DateFilterView.prototype */{
-
-    /**
-    * A DateFilter model to be rendered in this view
-    * @type {DateFilter} */
-    model: null,
-
-    /**
-     * @inheritdoc
-     */
-    modelClass: DateFilter,
-
-    className: "filter date",
-
-    template: _.template(Template),
-
-    /**
-     * @inheritdoc
-     */
-    events: function(){
-      try {
-        var events = FilterView.prototype.events.call(this);
-        events["change input.max"] = "updateYearRange";
-        events["change input.min"] = "updateYearRange";
-        return events
-      }
-      catch (error) {
-        console.log( 'There was an error creating the events object for a DateFilterView' +
-          ' Error details: ' + error );
-      }
-    },
-
-    render: function () {
+    /** @lends DateFilterView.prototype */ {
+      /**
+       * A DateFilter model to be rendered in this view
+       * @type {DateFilter} */
+      model: null,
+
+      /**
+       * @inheritdoc
+       */
+      modelClass: DateFilter,
+
+      className: "filter date",
+
+      template: _.template(Template),
+
+      /**
+       * @inheritdoc
+       */
+      events: function () {
+        try {
+          var events = FilterView.prototype.events.call(this);
+          events["change input.max"] = "updateYearRange";
+          events["change input.min"] = "updateYearRange";
+          return events;
+        } catch (error) {
+          console.log(
+            "There was an error creating the events object for a DateFilterView" +
+              " Error details: " +
+              error,
+          );
+        }
+      },
 
-      var view = this;
-      var templateVars = this.model.toJSON();
+      render: function () {
+        var view = this;
+        var templateVars = this.model.toJSON();
 
-      var model = this.model,
+        var model = this.model,
           min = model.get("min"),
           max = model.get("max"),
           rangeMin = model.get("rangeMin"),
           rangeMax = model.get("rangeMax");
 
-      if(!min && min !== 0){
-        templateVars.min = rangeMin
-      }
-      if(!max && max !== 0){
-        templateVars.max = rangeMax
-      }
-      if(templateVars.min < rangeMin){
-        templateVars.min = rangeMin
-      }
-      if(templateVars.max > rangeMax){
-        templateVars.max = rangeMax
-      }
-
-      // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
-      FilterView.prototype.render.call(this, templateVars);
-
-      //jQueryUI slider
-      this.$('.slider').slider({
+        if (!min && min !== 0) {
+          templateVars.min = rangeMin;
+        }
+        if (!max && max !== 0) {
+          templateVars.max = rangeMax;
+        }
+        if (templateVars.min < rangeMin) {
+          templateVars.min = rangeMin;
+        }
+        if (templateVars.max > rangeMax) {
+          templateVars.max = rangeMax;
+        }
+
+        // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
+        FilterView.prototype.render.call(this, templateVars);
+
+        //jQueryUI slider
+        this.$(".slider").slider({
           range: true,
           disabled: false,
-          min: this.model.get("rangeMin"),  //sets the minimum on the UI slider on initialization
-          max: this.model.get("rangeMax"),   //sets the maximum on the UI slider on initialization
-          values: [ this.model.get("min"), this.model.get("max") ], //where the left and right slider handles are
-          slide: function( event, ui ) {
+          min: this.model.get("rangeMin"), //sets the minimum on the UI slider on initialization
+          max: this.model.get("rangeMax"), //sets the maximum on the UI slider on initialization
+          values: [this.model.get("min"), this.model.get("max")], //where the left and right slider handles are
+          slide: function (event, ui) {
             // When the slider is changed, update the input values
-            view.$('input.min').val(ui.values[0]);
-            view.$('input.max').val(ui.values[1]);
+            view.$("input.min").val(ui.values[0]);
+            view.$("input.max").val(ui.values[1]);
           },
           stop: function (event, ui) {
-
             // When the slider is stopped, update the input values
-            view.$('input.min').val(ui.values[0]);
-            view.$('input.max').val(ui.values[1]);
+            view.$("input.min").val(ui.values[0]);
+            view.$("input.max").val(ui.values[1]);
 
             //Also update the DateFilter model
-            view.updateModel(ui.values[0], ui.values[1])
-          }
+            view.updateModel(ui.values[0], ui.values[1]);
+          },
         });
 
         //When the rangeReset event is triggered, reset the slider
         this.listenTo(view.model, "rangeReset", this.resetSlider);
-
       },
 
-    /**
-     * Override the base view which is triggered when the user types in the
-     * input and presses "Enter". The DateFilterView handles updating the model
-     * already and we do not want to clear the input value at any time.
-     */
-    handleChange: function () {
-      return
-    },
-
-    /**
-    * Updates the min and max values set on the Filter Model associated with this view.
-    * @param {number} min - The new minimum value
-    * @param {number} max - The new maximum value
-    * @since 2.17.0
-    */
-    updateModel: function(min, max){
-      try {
-        this.model.set({
-          min: min,
-          max: max
-        });
-      } catch (error) {
-        console.log("Error updating a DateFilter model from the DateFilter view. " +
-          "Error details: " + error);
-      }
-    },
-
-    /**
-    * Gets the min and max years from the number inputs and updates the DateFilter
-    *  model and the year UI slider.
-    * @param {Event} e - The event that triggered this callback function
-    */
-    updateYearRange : function(e) {
-
-      //Get the min and max values from the number inputs
-      var minVal = parseInt(this.$('input.min').val());
-      var maxVal = parseInt(this.$('input.max').val());
-
-      //Update the DateFilter model to match what is in the text inputs
-      this.model.set('min', minVal);
-      this.model.set('max', maxVal);
+      /**
+       * Override the base view which is triggered when the user types in the
+       * input and presses "Enter". The DateFilterView handles updating the model
+       * already and we do not want to clear the input value at any time.
+       */
+      handleChange: function () {
+        return;
+      },
 
-      //Update the UI slider to match the new min and max
-      this.$( ".slider" ).slider( "option", "values", [ minVal, maxVal ] );
+      /**
+       * Updates the min and max values set on the Filter Model associated with this view.
+       * @param {number} min - The new minimum value
+       * @param {number} max - The new maximum value
+       * @since 2.17.0
+       */
+      updateModel: function (min, max) {
+        try {
+          this.model.set({
+            min: min,
+            max: max,
+          });
+        } catch (error) {
+          console.log(
+            "Error updating a DateFilter model from the DateFilter view. " +
+              "Error details: " +
+              error,
+          );
+        }
+      },
 
-      //Track this event
-      MetacatUI.analytics?.trackEvent("portal search", "filter, Data Year", minVal + " to " + maxVal);
+      /**
+       * Gets the min and max years from the number inputs and updates the DateFilter
+       *  model and the year UI slider.
+       * @param {Event} e - The event that triggered this callback function
+       */
+      updateYearRange: function (e) {
+        //Get the min and max values from the number inputs
+        var minVal = parseInt(this.$("input.min").val());
+        var maxVal = parseInt(this.$("input.max").val());
+
+        //Update the DateFilter model to match what is in the text inputs
+        this.model.set("min", minVal);
+        this.model.set("max", maxVal);
+
+        //Update the UI slider to match the new min and max
+        this.$(".slider").slider("option", "values", [minVal, maxVal]);
+
+        //Track this event
+        MetacatUI.analytics?.trackEvent(
+          "portal search",
+          "filter, Data Year",
+          minVal + " to " + maxVal,
+        );
+      },
 
+      /**
+       * Resets the slider to the default values
+       */
+      resetSlider: function () {
+        //Set the min and max values on the slider widget
+        this.$(".slider").slider("option", "values", [
+          this.model.get("rangeMin"),
+          this.model.get("rangeMax"),
+        ]);
+
+        //Reset the min and max values
+        this.$("input.min").val(this.model.get("rangeMin"));
+        this.$("input.max").val(this.model.get("rangeMax"));
+      },
     },
-
-    /**
-    * Resets the slider to the default values
-    */
-    resetSlider: function(){
-
-      //Set the min and max values on the slider widget
-      this.$( ".slider" ).slider( "option", "values", [ this.model.get("rangeMin"), this.model.get("rangeMax") ] );
-
-      //Reset the min and max values
-      this.$('input.min').val( this.model.get("rangeMin") );
-      this.$('input.max').val( this.model.get("rangeMax") );
-
-    }
-
-  });
+  );
   return DateFilterView;
 });
 
diff --git a/docs/docs/src_js_views_filters_FilterEditorView.js.html b/docs/docs/src_js_views_filters_FilterEditorView.js.html index c61b5e86c..bd6d9b138 100644 --- a/docs/docs/src_js_views_filters_FilterEditorView.js.html +++ b/docs/docs/src_js_views_filters_FilterEditorView.js.html @@ -44,1159 +44,1222 @@

Source: src/js/views/filters/FilterEditorView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone',
-  'models/filters/Filter',
-  'models/filters/ChoiceFilter',
-  'models/filters/DateFilter',
-  'models/filters/ToggleFilter',
-  'collections/queryFields/QueryFields',
-  'views/searchSelect/QueryFieldSelectView',
-  'views/filters/FilterView',
-  'views/filters/ChoiceFilterView',
-  'views/filters/DateFilterView',
-  'views/filters/ToggleFilterView',
-  'text!templates/filters/filterEditor.html'
-],
-  function ($, _, Backbone, Filter, ChoiceFilter, DateFilter, ToggleFilter, QueryFields,
-    QueryFieldSelect, FilterView, ChoiceFilterView, DateFilterView, ToggleFilterView,
-    Template) {
-    'use strict';
-
-    /**
-    * @class FilterEditorView
-    * @classdesc Creates a view of an editor for a custom search filter
-    * @classcategory Views/Filters
-    * @screenshot views/filters/FilterEditorView.png
-    * @since 2.17.0
-    * @name FilterEditorView
-    * @extends Backbone.View
-    * @constructor
-    */
-    var FilterEditorView = Backbone.View.extend(
-    /** @lends FilterEditorView.prototype */{
-
-        /**
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/filters/Filter",
+  "models/filters/ChoiceFilter",
+  "models/filters/DateFilter",
+  "models/filters/ToggleFilter",
+  "collections/queryFields/QueryFields",
+  "views/searchSelect/QueryFieldSelectView",
+  "views/filters/FilterView",
+  "views/filters/ChoiceFilterView",
+  "views/filters/DateFilterView",
+  "views/filters/ToggleFilterView",
+  "text!templates/filters/filterEditor.html",
+], function (
+  $,
+  _,
+  Backbone,
+  Filter,
+  ChoiceFilter,
+  DateFilter,
+  ToggleFilter,
+  QueryFields,
+  QueryFieldSelect,
+  FilterView,
+  ChoiceFilterView,
+  DateFilterView,
+  ToggleFilterView,
+  Template,
+) {
+  "use strict";
+
+  /**
+   * @class FilterEditorView
+   * @classdesc Creates a view of an editor for a custom search filter
+   * @classcategory Views/Filters
+   * @screenshot views/filters/FilterEditorView.png
+   * @since 2.17.0
+   * @name FilterEditorView
+   * @extends Backbone.View
+   * @constructor
+   */
+  var FilterEditorView = Backbone.View.extend(
+    /** @lends FilterEditorView.prototype */ {
+      /**
          * A Filter model to be rendered and edited in this view. The Filter model must be
          * part of a Filters collection.
          //  TODO: Add support for boolean and number filters
          * @type {Filter|ChoiceFilter|DateFilter|ToggleFilter}
          */
-        model: null,
-
-        /**
-         * If rendering an editor for a brand new Filter model, provide the Filters
-         * collection instead of the Filter model. A new model will be created and, if the
-         * user clicks save, it will be added to this Filters collection.
-         * @type {Filters}
-         */
-        collection: null,
-
-        /**
-         * A reference to the PortalEditorView
-         * @type {PortalEditorView}
-         */
-        editorView: undefined,
-
-        /**
-         * Set to true if rendering an editor for a brand new Filter model that is not yet
-         * part of a Filters collection. If isNew is set to true, then the view requires a
-         * Filters model set to the view's collection property. A model will be created.
-         */
-        isNew: false,
-
-        /**
-         * The HTML classes to use for this view's element
-         * @type {string}
-         */
-        className: "filter-editor",
-
-        /**
-         * References to the template for this view. HTML files are converted to
-         * Underscore.js templates
-         * @type {Underscore.Template}
-         */
-        template: _.template(Template),
-
-        /**
-         * The classes to use for various elements in this view
-         * @type {Object}
-         * @property {string} fieldsContainer - the element in the template that
-         * will contain the input where a user can select metadata fields for the custom
-         * search filter.
-         * @property {string} editButton - The button a user clicks to start
-         * editing a search filter
-         * @property {string} cancelButton - the element in the template that a
-         * user clicks to undo any changes made to the filter and close the editing modal.
-         * @property {string} saveButton - the element in the template that a user
-         * clicks to add their filter changes to the parent Filters collection and close
-         * the editing modal.
-         * @property {string} deleteButton - the element in the template that a
-         * user clicks to remove the Filter model from the Filters collection
-         * @property {string} uiBuilderChoicesContainer - The container for the
-         * uiBuilderChoices and the associated instruction text
-         * @property {string} uiBuilderChoices - The container for each "button" a
-         * user can click to switch the filter type
-         * @property {string} uiBuilderChoice - The element that acts like a
-         * button that switches the filter type
-         * @property {string} uiBuilderChoiceActive - The class to add to a
-         * uiBuilderChoice buttons when that option is active/selected
-         * @property {string} uiBuilderLabel - The label that goes along with the
-         * uiBuilderChoice element
-         * @property {string} uiBuilderContainer - The element that will be turned
-         * into a carousel that switches between each UI Builder view when a user switches
-         * the filter type
-         * @property {string} modalInstructions - The class to add to the
-         * instruction text in the editing modal window
-         */
-        classes: {
-          fieldsContainer: "fields-container",
-          editButton: "edit-button",
-          cancelButton: "cancel-button",
-          saveButton: "save-button",
-          deleteButton: "delete-button",
-          uiBuilderChoicesContainer: "ui-builder-choices-container",
-          uiBuilderChoices: "ui-builder-choices",
-          uiBuilderChoice: "ui-builder-choice",
-          uiBuilderChoiceActive: "selected",
-          uiBuilderLabel: "ui-builder-choice-label",
-          uiBuilderContainer: "ui-builder-container",
-          modalInstructions: "modal-instructions",
-        },
-
-        /**
-         * Strings to use to display various messages to the user in this view
-         * @property {string} editButton - The text to show in the button a user clicks to
-         * open the editing modal window.
-         * @property {string} addFilterButton - The text to show in the button a user
-         * clicks to add a new search filter and open an editing modal window.
-         * @property {string} step1 - The instructions placed just before the fields input
-         * @property {string} step2 - The instructions placed after the fields input and
-         * before the uiBuilder select
-         * @property {string} filterNotAllowed - The message to show when a filter type
-         * doesn't work with the selected metadata fields
-         * @property {string} saveButton - Text for the button at the bottom of the
-         * editing modal that adds the filter model changes to the parent Filters
-         * collection and closes the modal
-         * @property {string} cancelButton - Text for the button at the bottom of the
-         * editing modal that closes the modal window without making any changes.
-         * @property {string} deleteButton - Text for the button at the bottom of the
-         * editing modal that removes the Filter model from the Filters collection.
-         * @property {string} validationError - The message to show at the top of the
-         * modal when there is at least one validation error.
-         * @property {string} noFilterOption - The message to show when there is no UI
-         * available for the selected field or combination of fields.
-         */
-        text: {
-          editButton: "EDIT",
-          addFilterButton: "Add a search filter",
-          step1: "Let people filter your data by",
-          step2: "...using the following interface",
-          filterNotAllowed: "This interface doesn't work with the metadata fields you" +
-            " selected. Change the 'filter data by' option to use this interface.",
-          saveButton: "Use these filter settings",
-          cancelButton: "Cancel",
-          deleteButton: "Remove filter",
-          validationError: "Please provide the content flagged below before saving this " +
-            "search filter.",
-          noFilterOption: "There are currently no filter options available to support " +
-            "this field, or this combination of fields. Change the 'filter data by' " +
-            "option to select an interface."
-        },
-
-        /**
-         * A function that returns a Backbone events object
-         * @return {object} A Backbone events object - an object with the events this view
-         * will listen to and the associated function to call.
-         */
-        events: function () {
-          var events = {}
-          events["click ." + this.classes.uiBuilderChoice] = "handleFilterIconClick"
-          return events
-        },
-
-        /**
-         * A list of query fields names to exclude from the list of options in the
-         * QueryFieldSelectView
-         * @type {string[]}
-         */
-        excludeFields: MetacatUI.appModel.get("collectionQueryExcludeFields"),
-
-        /**
-         * An additional field object contains the properties for an additional query
-         * field to add to the QueryFieldSelectView that are required to render it
-         * correctly. An additional query field is one that does not actually exist in the
-         * query service index.
-         *
-         * @typedef {Object} AdditionalField
-         *
-         * @property {string} name - A unique ID to represent this field. It must not
-         * match the name of any other query fields.
-         * @property {string[]} fields - The list of real query fields that this
-         * abstracted field will represent. It must exactly match the names of the query
-         * fields that actually exist.
-         * @property {string} label - A user-facing label to display.
-         * @property {string} description - A description for this field.
-         * @property {string} category - The name of the category under which to place
-         * this field. It must match one of the category names for an existing query
-         * field.
-         */
-
-        /**
-         * A list of additional fields which are not retrieved from the query service
-         * index, but which should be added to the list of options in the
-         * QueryFieldSelectView. This can be used to add abstracted fields which are a
-         * combination of multiple query fields, or to add a duplicate field that has a
-         * different label.
-         *
-         * @type {AdditionalField[]}
-         */
-        specialFields: [],
-
-        /**
-         * The path to the directory that contains the SVG files which are used like an
-         * icon to represent each UI type
-         * @type {string}
-         */
-        iconDir: "templates/filters/filterIcons/",
-
-        /**
-        * A single type of custom search filter that a user can select. An option
-        * represents a specific Filter model type and uses that associated Filter View.
-        * @typedef {Object} UIBuilderOption
-        * @property {string} label - The user-facing label to show for this option
-        * @property {string} modelType - The name of the filter model type that that this
-        * UI builder should create. Only one is allowed. The model must be one of the six
-        * filters that are allowed in a Portal "UIFilterGroupType". See
-        * {@link https://github.com/DataONEorg/collections-portals-schemas/blob/master/schemas/portals.xsd}.
-        * @property {string} iconFileName - The file name, including extension, of the SVG
-        * icon used to represent this option
-        * @property {string} description - A very brief, user-facing description of how
-        * this filter works
-        * @property {string[]} filterTypes - An array of one or more filter types that are
-        * allowed for this interface. If none are provided then any filter type is
-        * allowed. Filter types are one of the four keys defined in
-        * @property {string[]} blockedFields - An array of one or more search
-        * fields for which this interface should be blocked
-        * {@link QueryField#filterTypesMap}, and correspond to one of the four filter
-        * types that are allowed in a Collection definition. See
-        * {@link https://github.com/DataONEorg/collections-portals-schemas/blob/master/schemas/collections.xsd}.
-        * This property is used to help users match custom search filter UIs to
-        * appropriate query fields.
-        * @property {function} modelFunction - A function that takes an optional object
-        * with model properties and returns an instance of a model to use for this UI
-        * builder
-        * @property {function} uiFunction - A function that takes the model as an argument
-        * and returns the filter UI builder view for this option
-        */
-
-        /**
-         * The list of UI types that a user can select from. They will appear in the
-         * carousel in the order they are listed here.
-         * @type {UIBuilderOption[]}
-         */
-        uiBuilderOptions: [
-          {
-            label: "Free text",
-            modelType: "Filter",
-            iconFileName: "filter.svg",
-            description: "Allow people to search using any text they enter",
-            filterTypes: ["filter"],
-            blockedFields: [],
-            modelFunction: function (attrs) {
-              return new Filter(attrs)
-            },
-            uiFunction: function (model) {
-              return new FilterView({
-                model: model,
-                mode: "uiBuilder"
-              });
-            }
+      model: null,
+
+      /**
+       * If rendering an editor for a brand new Filter model, provide the Filters
+       * collection instead of the Filter model. A new model will be created and, if the
+       * user clicks save, it will be added to this Filters collection.
+       * @type {Filters}
+       */
+      collection: null,
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * Set to true if rendering an editor for a brand new Filter model that is not yet
+       * part of a Filters collection. If isNew is set to true, then the view requires a
+       * Filters model set to the view's collection property. A model will be created.
+       */
+      isNew: false,
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "filter-editor",
+
+      /**
+       * References to the template for this view. HTML files are converted to
+       * Underscore.js templates
+       * @type {Underscore.Template}
+       */
+      template: _.template(Template),
+
+      /**
+       * The classes to use for various elements in this view
+       * @type {Object}
+       * @property {string} fieldsContainer - the element in the template that
+       * will contain the input where a user can select metadata fields for the custom
+       * search filter.
+       * @property {string} editButton - The button a user clicks to start
+       * editing a search filter
+       * @property {string} cancelButton - the element in the template that a
+       * user clicks to undo any changes made to the filter and close the editing modal.
+       * @property {string} saveButton - the element in the template that a user
+       * clicks to add their filter changes to the parent Filters collection and close
+       * the editing modal.
+       * @property {string} deleteButton - the element in the template that a
+       * user clicks to remove the Filter model from the Filters collection
+       * @property {string} uiBuilderChoicesContainer - The container for the
+       * uiBuilderChoices and the associated instruction text
+       * @property {string} uiBuilderChoices - The container for each "button" a
+       * user can click to switch the filter type
+       * @property {string} uiBuilderChoice - The element that acts like a
+       * button that switches the filter type
+       * @property {string} uiBuilderChoiceActive - The class to add to a
+       * uiBuilderChoice buttons when that option is active/selected
+       * @property {string} uiBuilderLabel - The label that goes along with the
+       * uiBuilderChoice element
+       * @property {string} uiBuilderContainer - The element that will be turned
+       * into a carousel that switches between each UI Builder view when a user switches
+       * the filter type
+       * @property {string} modalInstructions - The class to add to the
+       * instruction text in the editing modal window
+       */
+      classes: {
+        fieldsContainer: "fields-container",
+        editButton: "edit-button",
+        cancelButton: "cancel-button",
+        saveButton: "save-button",
+        deleteButton: "delete-button",
+        uiBuilderChoicesContainer: "ui-builder-choices-container",
+        uiBuilderChoices: "ui-builder-choices",
+        uiBuilderChoice: "ui-builder-choice",
+        uiBuilderChoiceActive: "selected",
+        uiBuilderLabel: "ui-builder-choice-label",
+        uiBuilderContainer: "ui-builder-container",
+        modalInstructions: "modal-instructions",
+      },
+
+      /**
+       * Strings to use to display various messages to the user in this view
+       * @property {string} editButton - The text to show in the button a user clicks to
+       * open the editing modal window.
+       * @property {string} addFilterButton - The text to show in the button a user
+       * clicks to add a new search filter and open an editing modal window.
+       * @property {string} step1 - The instructions placed just before the fields input
+       * @property {string} step2 - The instructions placed after the fields input and
+       * before the uiBuilder select
+       * @property {string} filterNotAllowed - The message to show when a filter type
+       * doesn't work with the selected metadata fields
+       * @property {string} saveButton - Text for the button at the bottom of the
+       * editing modal that adds the filter model changes to the parent Filters
+       * collection and closes the modal
+       * @property {string} cancelButton - Text for the button at the bottom of the
+       * editing modal that closes the modal window without making any changes.
+       * @property {string} deleteButton - Text for the button at the bottom of the
+       * editing modal that removes the Filter model from the Filters collection.
+       * @property {string} validationError - The message to show at the top of the
+       * modal when there is at least one validation error.
+       * @property {string} noFilterOption - The message to show when there is no UI
+       * available for the selected field or combination of fields.
+       */
+      text: {
+        editButton: "EDIT",
+        addFilterButton: "Add a search filter",
+        step1: "Let people filter your data by",
+        step2: "...using the following interface",
+        filterNotAllowed:
+          "This interface doesn't work with the metadata fields you" +
+          " selected. Change the 'filter data by' option to use this interface.",
+        saveButton: "Use these filter settings",
+        cancelButton: "Cancel",
+        deleteButton: "Remove filter",
+        validationError:
+          "Please provide the content flagged below before saving this " +
+          "search filter.",
+        noFilterOption:
+          "There are currently no filter options available to support " +
+          "this field, or this combination of fields. Change the 'filter data by' " +
+          "option to select an interface.",
+      },
+
+      /**
+       * A function that returns a Backbone events object
+       * @return {object} A Backbone events object - an object with the events this view
+       * will listen to and the associated function to call.
+       */
+      events: function () {
+        var events = {};
+        events["click ." + this.classes.uiBuilderChoice] =
+          "handleFilterIconClick";
+        return events;
+      },
+
+      /**
+       * A list of query fields names to exclude from the list of options in the
+       * QueryFieldSelectView
+       * @type {string[]}
+       */
+      excludeFields: MetacatUI.appModel.get("collectionQueryExcludeFields"),
+
+      /**
+       * An additional field object contains the properties for an additional query
+       * field to add to the QueryFieldSelectView that are required to render it
+       * correctly. An additional query field is one that does not actually exist in the
+       * query service index.
+       *
+       * @typedef {Object} AdditionalField
+       *
+       * @property {string} name - A unique ID to represent this field. It must not
+       * match the name of any other query fields.
+       * @property {string[]} fields - The list of real query fields that this
+       * abstracted field will represent. It must exactly match the names of the query
+       * fields that actually exist.
+       * @property {string} label - A user-facing label to display.
+       * @property {string} description - A description for this field.
+       * @property {string} category - The name of the category under which to place
+       * this field. It must match one of the category names for an existing query
+       * field.
+       */
+
+      /**
+       * A list of additional fields which are not retrieved from the query service
+       * index, but which should be added to the list of options in the
+       * QueryFieldSelectView. This can be used to add abstracted fields which are a
+       * combination of multiple query fields, or to add a duplicate field that has a
+       * different label.
+       *
+       * @type {AdditionalField[]}
+       */
+      specialFields: [],
+
+      /**
+       * The path to the directory that contains the SVG files which are used like an
+       * icon to represent each UI type
+       * @type {string}
+       */
+      iconDir: "templates/filters/filterIcons/",
+
+      /**
+       * A single type of custom search filter that a user can select. An option
+       * represents a specific Filter model type and uses that associated Filter View.
+       * @typedef {Object} UIBuilderOption
+       * @property {string} label - The user-facing label to show for this option
+       * @property {string} modelType - The name of the filter model type that that this
+       * UI builder should create. Only one is allowed. The model must be one of the six
+       * filters that are allowed in a Portal "UIFilterGroupType". See
+       * {@link https://github.com/DataONEorg/collections-portals-schemas/blob/master/schemas/portals.xsd}.
+       * @property {string} iconFileName - The file name, including extension, of the SVG
+       * icon used to represent this option
+       * @property {string} description - A very brief, user-facing description of how
+       * this filter works
+       * @property {string[]} filterTypes - An array of one or more filter types that are
+       * allowed for this interface. If none are provided then any filter type is
+       * allowed. Filter types are one of the four keys defined in
+       * @property {string[]} blockedFields - An array of one or more search
+       * fields for which this interface should be blocked
+       * {@link QueryField#filterTypesMap}, and correspond to one of the four filter
+       * types that are allowed in a Collection definition. See
+       * {@link https://github.com/DataONEorg/collections-portals-schemas/blob/master/schemas/collections.xsd}.
+       * This property is used to help users match custom search filter UIs to
+       * appropriate query fields.
+       * @property {function} modelFunction - A function that takes an optional object
+       * with model properties and returns an instance of a model to use for this UI
+       * builder
+       * @property {function} uiFunction - A function that takes the model as an argument
+       * and returns the filter UI builder view for this option
+       */
+
+      /**
+       * The list of UI types that a user can select from. They will appear in the
+       * carousel in the order they are listed here.
+       * @type {UIBuilderOption[]}
+       */
+      uiBuilderOptions: [
+        {
+          label: "Free text",
+          modelType: "Filter",
+          iconFileName: "filter.svg",
+          description: "Allow people to search using any text they enter",
+          filterTypes: ["filter"],
+          blockedFields: [],
+          modelFunction: function (attrs) {
+            return new Filter(attrs);
           },
-          {
-            label: "Dropdown",
-            modelType: "ChoiceFilter",
-            iconFileName: "choice.svg",
-            description: "Allow people to select a search term from a list of options",
-            filterTypes: ["filter"],
-            blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
-            modelFunction: function (attrs) {
-              return new ChoiceFilter(attrs)
-            },
-            uiFunction: function (model) {
-              return new ChoiceFilterView({
-                model: model,
-                mode: "uiBuilder"
-              });
-            }
-          },
-          {
-            label: "Year slider",
-            modelType: "DateFilter",
-            iconFileName: "number.svg",
-            description: "Let people search for a range of years",
-            filterTypes: ["dateFilter"],
-            blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
-            modelFunction: function (attrs) {
-              return new DateFilter(attrs)
-            },
-            uiFunction: function (model) {
-              return new DateFilterView({
-                model: model,
-                mode: "uiBuilder"
-              });
-            }
+          uiFunction: function (model) {
+            return new FilterView({
+              model: model,
+              mode: "uiBuilder",
+            });
           },
-          {
-            label: "Toggle",
-            modelType: "ToggleFilter",
-            iconFileName: "toggle.svg",
-            description: "Let people add or remove a single, specific search term",
-            filterTypes: ["filter"],
-            blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
-            modelFunction: function (attrs) {
-              return new ToggleFilter(attrs)
-            },
-            uiFunction: function (model) {
-              return new ToggleFilterView({
-                model: model,
-                mode: "uiBuilder"
-              });
-            }
-          }
-        ],
-
-        /**
-         * Executed when this view is created
-         * @param {object} options - A literal object of options to pass to this view
-         * @property {Filter|ChoiceFilter|DateFilter|ToggleFilter} options.model - The
-         * filter model to render an editor for. It must be part of a Filters collection.
-         */
-        initialize: function (options) {
-          try {
-
-            // Ensure the query fields are cached for limitUITypes()
-            if (typeof MetacatUI.queryFields === "undefined") {
-              MetacatUI.queryFields = new QueryFields();
-              MetacatUI.queryFields.fetch();
-            }
-
-            if (!options || typeof options != "object") {
-              var options = {};
-            }
-
-            this.editorView = options.editorView || null;
-
-            if (!options.isNew) {
-              // If this view is an editor for an existing Filter model, check that the model
-              // and the Filters collection is provided.
-              if (!options.model) {
-                console.log("A Filter model is required to render a Filter Editor View");
-                return
-              }
-              if (!options.model.collection) {
-                console.log("The Filter model for a FilterEditorView must be part of a" +
-                  " Filters collection");
-                return
-              }
-              // Set the model and collection on the view
-              this.model = options.model
-              this.collection = options.model.collection
-            } else {
-              // If this is an editor for a new Filter model, create a default model and
-              // make sure there is a Filters collection to add it to
-              if (!options.collection) {
-                console.log("A Filters collection is required to render a " +
-                  "FilterEditorView for a new Filters model.");
-                return
-              }
-              this.model = new Filter()
-              this.collection = options.collection
-              this.isNew = true
-            }
-
-
-          } catch (error) {
-            console.log("Error creating an FilterEditorView. Error details: " + error);
-          }
         },
-
-        /**
-         * Render the view
-         */
-        render: function () {
-          try {
-            // Save a reference to this view
-            var view = this;
-
-            // Create and insert an "edit" or a "add filter" button for the filter.
-            var buttonText = this.text.editButton,
-              buttonClasses = this.classes.editButton,
-              buttonIcon = "pencil";
-
-            // Text & styling is different for the "add a new filter" button
-            if (this.isNew) {
-              buttonText = this.text.addFilterButton;
-              buttonIcon = "plus";
-              buttonClasses = buttonClasses + " btn";
-              this.$el.addClass("new");
-            }
-            var editButton = $("<a class='" + buttonClasses + "'>" +
-              "<i class='icon icon-" + buttonIcon + " icon-on-left'></i> " +
-              buttonText + "</a>");
-            this.$el.prepend(editButton);
-
-            // Render the editor modal on-the-fly to make the application load faster.
-            // No need to create editing modals for filters that a user doesn't edit.
-            editButton.on("click", function () {
-              view.renderEditorModal.call(view);
+        {
+          label: "Dropdown",
+          modelType: "ChoiceFilter",
+          iconFileName: "choice.svg",
+          description:
+            "Allow people to select a search term from a list of options",
+          filterTypes: ["filter"],
+          blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
+          modelFunction: function (attrs) {
+            return new ChoiceFilter(attrs);
+          },
+          uiFunction: function (model) {
+            return new ChoiceFilterView({
+              model: model,
+              mode: "uiBuilder",
             });
-
-            // Save a reference to this view
-            this.$el.data("view", this);
-            return this
-          } catch (error) {
-            console.log("Error rendering an FilterEditorView. Error details: " + error);
-          }
+          },
         },
-
-        /**
-         * Render and show the modal window that has all the components for editing a
-         * filter. This is created on-the-fly because creating these modals all at once in
-         * a FilterGroupsView in edit mode takes too much time.
-         */
-        renderEditorModal: function () {
-          try {
-
-            // Save a reference to this view
-            var view = this;
-
-            // The list of UI Filter Editor options needs to be mutable. We will save the
-            // draft filter models, and the associated editor views to this list. Rewrite
-            // this.uiBuilders every time the editor modal is re-rendered.
-            this.uiBuilders = [];
-            this.uiBuilderOptions.forEach(function (opt) {
-              this.uiBuilders.push(_.clone(opt))
-            }, this);
-
-            // Create and insert the modal window that will contain the editing interface
-            var modalHTML = this.template({
-              classes: view.classes,
-              text: view.text
-            });
-            this.modalEl = $(modalHTML);
-            this.$el.append(this.modalEl);
-
-            // Start rendering the metadata field input only after the modal is shown.
-            // Otherwise this step slows the rendering down, leaves too much of a delay
-            // before the modal appears.
-            this.modalEl.off();
-            this.modalEl.on("shown", function (event) {
-              view.modalEl.off("shown");
-              view.renderFieldInput();
+        {
+          label: "Year slider",
+          modelType: "DateFilter",
+          iconFileName: "number.svg",
+          description: "Let people search for a range of years",
+          filterTypes: ["dateFilter"],
+          blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
+          modelFunction: function (attrs) {
+            return new DateFilter(attrs);
+          },
+          uiFunction: function (model) {
+            return new DateFilterView({
+              model: model,
+              mode: "uiBuilder",
             });
-            this.modalEl.modal("show");
-
-            // Add listeners to the modal buttons save or cancel changes
-            this.activateModalButtons();
-
-            // Create and insert the "buttons" to switch filter type, and the elements
-            // that will contain the UI building interfaces for each filter type.
-            this.renderUIBuilders();
-
-            // Select and render the UI Filter Editor for the filter model set on this
-            // view.
-            this.switchFilterType();
-
-            // Disable any filter types that do not match the currently selected fields
-            this.handleFieldChange(_.clone(view.model.get("fields")));
-
-          }
-          catch (error) {
-            console.log('There was an error rendering the modal in a FilterEditorView' +
-              ' Error details: ' + error);
-          }
+          },
         },
-
-        /**
-         * Hide and destroy the filter editor modal window
-         */
-        hideModal: function () {
-          try {
-            var view = this;
-            view.modalEl.off("hidden");
-            view.modalEl.on("hidden", function () {
-              view.modalEl.off();
-              view.modalEl.remove();
+        {
+          label: "Toggle",
+          modelType: "ToggleFilter",
+          iconFileName: "toggle.svg",
+          description:
+            "Let people add or remove a single, specific search term",
+          filterTypes: ["filter"],
+          blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
+          modelFunction: function (attrs) {
+            return new ToggleFilter(attrs);
+          },
+          uiFunction: function (model) {
+            return new ToggleFilterView({
+              model: model,
+              mode: "uiBuilder",
             });
-            view.modalEl.modal("hide");
-          }
-          catch (error) {
-            console.log(
-              'There was an error hiding the editing modal in a FilterEditorView' +
-              '. Error details: ' + error
-            );
-          }
+          },
         },
-
-        /**
-         * Find the save and cancel buttons in the editing modal window, and add listeners
-         * that close the modal and update the Filters collection on save
-         */
-        activateModalButtons: function () {
-          try {
-
-            var view = this;
-            // The buttons at the bottom of the modal
-            var saveButton = this.modalEl.find("." + this.classes.saveButton),
-              cancelButton = this.modalEl.find("." + this.classes.cancelButton),
-              deleteButton = this.modalEl.find("." + this.classes.deleteButton);
-
-            // Add listeners to the modal's "delete", "save", and "cancel" buttons.
-
-            // SAVE
-            saveButton.on('click', function (event) {
-
-              // Don't allow user to save a filter with a field that doesn't have a
-              // matching UI type supported yet.
-              if (view.noFilterOptions) {
-                view.handleErrors()
-                // Switch the message from "warning" to "error" so that it's clear this is
-                // the reason the user cannot save the filter
-                view.showNoFilterOptionMessage(false, "error");
-                return
-              }
-
-              var results = view.createModel();
-
-              if (results.success === false) {
-                view.handleErrors(results.errors)
-                return
-              }
-
-              saveButton.off('click');
-              view.hideModal();
-              // Only update the collection after the modal has closed because adding a
-              // new model triggers a re-render of the FilterGroupsView which interferes
-              // with removing the modal.
-              var oldModel = view.model;
-              // Update the filter model in the parent Filters collection
-              view.model = view.collection.replaceModel(oldModel, results.model);
-
-              if (view.editorView) {
-                view.editorView.showControls();
-              }
-
-            });
-
-            // CANCEL
-            cancelButton.on('click', function (event) {
-              cancelButton.off('click');
-              view.currentUIBuilder = null;
-              view.hideModal();
-            })
-
-            // DELETE
-            deleteButton.on('click', function (event) {
-              deleteButton.off('click');
-              view.hideModal();
-              if (!view.isNew) {
-                view.collection.remove(view.model)
-              }
-              if (view.editorView) {
-                view.editorView.showControls();
-              }
-            })
-
-          }
-          catch (error) {
-            console.log(
-              "There was an error activating the modal buttons in a FilterEditorView" +
-              ". Error details: " + error
-            );
+      ],
+
+      /**
+       * Executed when this view is created
+       * @param {object} options - A literal object of options to pass to this view
+       * @property {Filter|ChoiceFilter|DateFilter|ToggleFilter} options.model - The
+       * filter model to render an editor for. It must be part of a Filters collection.
+       */
+      initialize: function (options) {
+        try {
+          // Ensure the query fields are cached for limitUITypes()
+          if (typeof MetacatUI.queryFields === "undefined") {
+            MetacatUI.queryFields = new QueryFields();
+            MetacatUI.queryFields.fetch();
           }
-        },
-
-        /**
-         * Create and insert the "buttons" to switch filter type and the elements
-         * that will contain the UI building interfaces for each filter type.
-         */
-        renderUIBuilders: function () {
-          try {
-            var view = this;
-
-            // The container for the list of filter icons that allows users to switch
-            // between filter types, plus the associated instruction paragraph
-            var uiBuilderChoicesContainer = this.modalEl.find("." + this.classes.uiBuilderChoicesContainer);
 
-            // The container for just the icons/buttons
-            var uiBuilderChoices = $("<div></div>").addClass(this.classes.uiBuilderChoices);
-            uiBuilderChoicesContainer.append(uiBuilderChoices);
-
-            // uiBuilderCarousel will contain all of the UIBuilder views as slides
-            this.uiBuilderCarousel = this.modalEl.find("." + this.classes.uiBuilderContainer);
-
-            // The bootstrap carousel plugin requires the carousel slide times to be
-            // contained within an inner div with the class 'carousel-inner'
-            var carouselInner = $('<div class="carousel-inner"></div>');
-            this.uiBuilderCarousel.append(carouselInner);
-
-            // Create a container and button for each uiBuilder option
-            this.uiBuilders.forEach(function (uiBuilder) {
-
-              // Create a label button that allows the user to select the given UI
-
-              // Create the button label
-              var labelEl = $("<h5>" + uiBuilder.label + "</h5>")
-                .addClass(view.classes.uiBuilderLabel);
-              // Create the button
-              var button = $("<div></div>")
-                .addClass(view.classes.uiBuilderChoice)
-                .attr("data-filter-type", uiBuilder.modelType)
-                .append(labelEl);
-              // Insert the uiBuilder icon SVG into the button
-              var svgPath = 'text!' + this.iconDir + uiBuilder.iconFileName;
-              require([svgPath], function (svgString) {
-                button.append(svgString)
-              });
-              // Add a tooltip with description to the button
-              button.tooltip({
-                title: uiBuilder.description,
-                delay: {
-                  show: 900,
-                  hide: 50
-                }
-              })
-              // Insert the button into the list of uiBuilder choices
-              uiBuilderChoices.append(button);
-              // Create and insert the container / carousel slide. The carousel plugin
-              // requires slides to have the class 'item'. Save the container to the
-              // list of uiBuilder options.
-              var uiBuilderContainer = $('<div class="item"></div>');
-              carouselInner.append(uiBuilderContainer);
-
-              // Add the button and container to the list of uiBuilders to make it
-              // easy to switch between filter types
-              uiBuilder.container = uiBuilderContainer;
-              uiBuilder.button = button;
-
-            }, this);
-
-            // Initialize the carousel
-            this.uiBuilderCarousel.addClass("slide");
-            this.uiBuilderCarousel.addClass("carousel");
-            this.uiBuilderCarousel.carousel({
-              interval: false
-            });
-            // Need active class on at least one item for carousel to work properly
-            this.uiBuilderCarousel.find(".item").first().addClass("active");
+          if (!options || typeof options != "object") {
+            var options = {};
           }
-          catch (error) {
-            console.log(
-              'There was an error rendering the UI filter builders in a FilterEditorView' +
-              '. Error details: ' + error
-            );
-          }
-        },
 
-        /**
-         * Create and insert the component that is used to edit the fields attribute of a
-         * Filter Model. Save it to the view so that the selected fields can be accessed
-         * on save.
-         */
-        renderFieldInput: function () {
-          try {
-            var view = this;
-            var selectedFields = _.clone(view.model.get("fields"));
-            view.fieldInput = new QueryFieldSelect({
-              selected: selectedFields,
-              inputLabel: "Select one or more metadata fields",
-              excludeFields: view.excludeFields,
-              addFields: view.specialFields,
-              separatorText: view.model.get("fieldsOperator"),
-            })
-            view.modalEl.find("." + view.classes.fieldsContainer).append(view.fieldInput.el)
-            view.fieldInput.render();
-
-            // When the field input is changed, limit UI options to options that match
-            // this field type
-            view.fieldInput.off("changeSelection")
-            view.fieldInput.on("changeSelection", function (selectedFields) {
-              view.handleFieldChange.call(view, selectedFields);
-            })
+          this.editorView = options.editorView || null;
+
+          if (!options.isNew) {
+            // If this view is an editor for an existing Filter model, check that the model
+            // and the Filters collection is provided.
+            if (!options.model) {
+              console.log(
+                "A Filter model is required to render a Filter Editor View",
+              );
+              return;
+            }
+            if (!options.model.collection) {
+              console.log(
+                "The Filter model for a FilterEditorView must be part of a" +
+                  " Filters collection",
+              );
+              return;
+            }
+            // Set the model and collection on the view
+            this.model = options.model;
+            this.collection = options.model.collection;
+          } else {
+            // If this is an editor for a new Filter model, create a default model and
+            // make sure there is a Filters collection to add it to
+            if (!options.collection) {
+              console.log(
+                "A Filters collection is required to render a " +
+                  "FilterEditorView for a new Filters model.",
+              );
+              return;
+            }
+            this.model = new Filter();
+            this.collection = options.collection;
+            this.isNew = true;
           }
-          catch (error) {
-            console.log('There was an error rendering a fields input in a FilterEditorView' +
-              ' Error details: ' + error);
+        } catch (error) {
+          console.log(
+            "Error creating an FilterEditorView. Error details: " + error,
+          );
+        }
+      },
+
+      /**
+       * Render the view
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // Create and insert an "edit" or a "add filter" button for the filter.
+          var buttonText = this.text.editButton,
+            buttonClasses = this.classes.editButton,
+            buttonIcon = "pencil";
+
+          // Text & styling is different for the "add a new filter" button
+          if (this.isNew) {
+            buttonText = this.text.addFilterButton;
+            buttonIcon = "plus";
+            buttonClasses = buttonClasses + " btn";
+            this.$el.addClass("new");
           }
-        },
-
-        /**
-         * Run whenever the user selects or removes fields from the Query Field input.
-         * This function checks which filter UIs support the type of Query Field selected,
-         * and then blocks or enables the UIs in the editor. This is done to help prevent
-         * users from building mis-matched search filters, e.g. "Year Slider" filters with
-         * text query fields.
-         * @param {string[]} selectedFields The Query Field names (i.e. Solr field names)
-         * of the newly selected fields
-         */
-        handleFieldChange: function (selectedFields) {
-
-          try {
-            var view = this;
+          var editButton = $(
+            "<a class='" +
+              buttonClasses +
+              "'>" +
+              "<i class='icon icon-" +
+              buttonIcon +
+              " icon-on-left'></i> " +
+              buttonText +
+              "</a>",
+          );
+          this.$el.prepend(editButton);
+
+          // Render the editor modal on-the-fly to make the application load faster.
+          // No need to create editing modals for filters that a user doesn't edit.
+          editButton.on("click", function () {
+            view.renderEditorModal.call(view);
+          });
 
-            // Enable all UI types if no field is selected yet
-            if (!selectedFields || !selectedFields.length || selectedFields[0] === "") {
-              this.uiBuilders.forEach(function (uiBuilder) {
-                view.allowUI(uiBuilder)
-              })
-              return
+          // Save a reference to this view
+          this.$el.data("view", this);
+          return this;
+        } catch (error) {
+          console.log(
+            "Error rendering an FilterEditorView. Error details: " + error,
+          );
+        }
+      },
+
+      /**
+       * Render and show the modal window that has all the components for editing a
+       * filter. This is created on-the-fly because creating these modals all at once in
+       * a FilterGroupsView in edit mode takes too much time.
+       */
+      renderEditorModal: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // The list of UI Filter Editor options needs to be mutable. We will save the
+          // draft filter models, and the associated editor views to this list. Rewrite
+          // this.uiBuilders every time the editor modal is re-rendered.
+          this.uiBuilders = [];
+          this.uiBuilderOptions.forEach(function (opt) {
+            this.uiBuilders.push(_.clone(opt));
+          }, this);
+
+          // Create and insert the modal window that will contain the editing interface
+          var modalHTML = this.template({
+            classes: view.classes,
+            text: view.text,
+          });
+          this.modalEl = $(modalHTML);
+          this.$el.append(this.modalEl);
+
+          // Start rendering the metadata field input only after the modal is shown.
+          // Otherwise this step slows the rendering down, leaves too much of a delay
+          // before the modal appears.
+          this.modalEl.off();
+          this.modalEl.on("shown", function (event) {
+            view.modalEl.off("shown");
+            view.renderFieldInput();
+          });
+          this.modalEl.modal("show");
+
+          // Add listeners to the modal buttons save or cancel changes
+          this.activateModalButtons();
+
+          // Create and insert the "buttons" to switch filter type, and the elements
+          // that will contain the UI building interfaces for each filter type.
+          this.renderUIBuilders();
+
+          // Select and render the UI Filter Editor for the filter model set on this
+          // view.
+          this.switchFilterType();
+
+          // Disable any filter types that do not match the currently selected fields
+          this.handleFieldChange(_.clone(view.model.get("fields")));
+        } catch (error) {
+          console.log(
+            "There was an error rendering the modal in a FilterEditorView" +
+              " Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Hide and destroy the filter editor modal window
+       */
+      hideModal: function () {
+        try {
+          var view = this;
+          view.modalEl.off("hidden");
+          view.modalEl.on("hidden", function () {
+            view.modalEl.off();
+            view.modalEl.remove();
+          });
+          view.modalEl.modal("hide");
+        } catch (error) {
+          console.log(
+            "There was an error hiding the editing modal in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Find the save and cancel buttons in the editing modal window, and add listeners
+       * that close the modal and update the Filters collection on save
+       */
+      activateModalButtons: function () {
+        try {
+          var view = this;
+          // The buttons at the bottom of the modal
+          var saveButton = this.modalEl.find("." + this.classes.saveButton),
+            cancelButton = this.modalEl.find("." + this.classes.cancelButton),
+            deleteButton = this.modalEl.find("." + this.classes.deleteButton);
+
+          // Add listeners to the modal's "delete", "save", and "cancel" buttons.
+
+          // SAVE
+          saveButton.on("click", function (event) {
+            // Don't allow user to save a filter with a field that doesn't have a
+            // matching UI type supported yet.
+            if (view.noFilterOptions) {
+              view.handleErrors();
+              // Switch the message from "warning" to "error" so that it's clear this is
+              // the reason the user cannot save the filter
+              view.showNoFilterOptionMessage(false, "error");
+              return;
             }
 
-            // If at least one field is selected, then limit the available UI types to
-            // those that match the type of Query Field.
-            var type = MetacatUI.queryFields.getRequiredFilterType(selectedFields)
-
-            this.uiBuilders.forEach(function (uiBuilder) {
-              if (
-                uiBuilder.filterTypes.includes(type) &&
-                view.isBuilderAllowedForFields(uiBuilder, selectedFields)
-              ) {
-                view.allowUI(uiBuilder)
-              } else {
-                view.blockUI(uiBuilder)
-              }
-            })
-          }
-          catch (error) {
-            console.log(
-              'There was an error handling a field change in a FilterEditorView' +
-              '. Error details: ' + error
-            );
-          }
+            var results = view.createModel();
 
-        },
+            if (results.success === false) {
+              view.handleErrors(results.errors);
+              return;
+            }
 
-        /**
-         * Marks a UI builder is blocked (so that it can't be selected) and updates the
-         * tooltip with text explaining that this UI can't be used with the selected
-         * fields. If the UI to block is the currently selected UI, then switches to the
-         * next allowed UI. If there are no UIs that are allowed, then shows a message and
-         * hides all UI builders.
-         * @param {UIBuilderOption} uiBuilder - The UI builder Object to block
-         */
-        blockUI: function (uiBuilder) {
-          try {
-            var view = this;
-            uiBuilder.allowed = false;
-            uiBuilder.button.addClass("disabled")
-            uiBuilder.button.tooltip("destroy")
-            uiBuilder.button.tooltip({
-              title: view.text.filterNotAllowed,
-              delay: {
-                show: 400,
-                hide: 50
-              }
-            })
-            // If the current UI is a blocked one...
-            if (this.currentUIBuilder === uiBuilder) {
-              // ... switch to the next unblocked one.
-              var allowedUIBuilder = _.findWhere(this.uiBuilders, { allowed: true });
-              if (allowedUIBuilder) {
-                view.switchFilterType(allowedUIBuilder.modelType)
-              } else {
-                // If there is no UI available, then show a message
-                this.showNoFilterOptionMessage()
-              }
+            saveButton.off("click");
+            view.hideModal();
+            // Only update the collection after the modal has closed because adding a
+            // new model triggers a re-render of the FilterGroupsView which interferes
+            // with removing the modal.
+            var oldModel = view.model;
+            // Update the filter model in the parent Filters collection
+            view.model = view.collection.replaceModel(oldModel, results.model);
+
+            if (view.editorView) {
+              view.editorView.showControls();
             }
-          }
-          catch (error) {
-            console.log(
-              'There was an error blocking a filter UI builder in a FilterEditorView' +
-              '. Error details: ' + error
-            );
-          }
-        },
+          });
 
-        /**
-         * Marks a UI builder is allowed (so that it can be selected) and updates the
-         * tooltip text with the description of this UI. If it's displayed, this function
-         * hides the message that indicates that there are no allowed UIs that match the
-         * selected query fields.
-         * @param {UIBuilderOption} uiBuilder - The UI builder Object to block
-         */
-        allowUI: function (uiBuilder) {
+          // CANCEL
+          cancelButton.on("click", function (event) {
+            cancelButton.off("click");
+            view.currentUIBuilder = null;
+            view.hideModal();
+          });
 
-          try {
-            // If at least one UI is allowed, then make sure the "no filter message" is
-            // hidden.
-            if (this.noFilterOptions) {
-              this.hideNoFilterOptionMessage()
+          // DELETE
+          deleteButton.on("click", function (event) {
+            deleteButton.off("click");
+            view.hideModal();
+            if (!view.isNew) {
+              view.collection.remove(view.model);
+            }
+            if (view.editorView) {
+              view.editorView.showControls();
             }
-            uiBuilder.allowed = true;
-            uiBuilder.button.removeClass("disabled")
-            uiBuilder.button.tooltip("destroy")
-            uiBuilder.button.tooltip({
+          });
+        } catch (error) {
+          console.log(
+            "There was an error activating the modal buttons in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Create and insert the "buttons" to switch filter type and the elements
+       * that will contain the UI building interfaces for each filter type.
+       */
+      renderUIBuilders: function () {
+        try {
+          var view = this;
+
+          // The container for the list of filter icons that allows users to switch
+          // between filter types, plus the associated instruction paragraph
+          var uiBuilderChoicesContainer = this.modalEl.find(
+            "." + this.classes.uiBuilderChoicesContainer,
+          );
+
+          // The container for just the icons/buttons
+          var uiBuilderChoices = $("<div></div>").addClass(
+            this.classes.uiBuilderChoices,
+          );
+          uiBuilderChoicesContainer.append(uiBuilderChoices);
+
+          // uiBuilderCarousel will contain all of the UIBuilder views as slides
+          this.uiBuilderCarousel = this.modalEl.find(
+            "." + this.classes.uiBuilderContainer,
+          );
+
+          // The bootstrap carousel plugin requires the carousel slide times to be
+          // contained within an inner div with the class 'carousel-inner'
+          var carouselInner = $('<div class="carousel-inner"></div>');
+          this.uiBuilderCarousel.append(carouselInner);
+
+          // Create a container and button for each uiBuilder option
+          this.uiBuilders.forEach(function (uiBuilder) {
+            // Create a label button that allows the user to select the given UI
+
+            // Create the button label
+            var labelEl = $("<h5>" + uiBuilder.label + "</h5>").addClass(
+              view.classes.uiBuilderLabel,
+            );
+            // Create the button
+            var button = $("<div></div>")
+              .addClass(view.classes.uiBuilderChoice)
+              .attr("data-filter-type", uiBuilder.modelType)
+              .append(labelEl);
+            // Insert the uiBuilder icon SVG into the button
+            var svgPath = "text!" + this.iconDir + uiBuilder.iconFileName;
+            require([svgPath], function (svgString) {
+              button.append(svgString);
+            });
+            // Add a tooltip with description to the button
+            button.tooltip({
               title: uiBuilder.description,
               delay: {
                 show: 900,
-                hide: 50
-              }
-            })
-          }
-          catch (error) {
-            console.log(
-              'There was an error unblocking a filter UI builder in a FilterEditorView' +
-              '. Error details: ' + error
-            );
+                hide: 50,
+              },
+            });
+            // Insert the button into the list of uiBuilder choices
+            uiBuilderChoices.append(button);
+            // Create and insert the container / carousel slide. The carousel plugin
+            // requires slides to have the class 'item'. Save the container to the
+            // list of uiBuilder options.
+            var uiBuilderContainer = $('<div class="item"></div>');
+            carouselInner.append(uiBuilderContainer);
+
+            // Add the button and container to the list of uiBuilders to make it
+            // easy to switch between filter types
+            uiBuilder.container = uiBuilderContainer;
+            uiBuilder.button = button;
+          }, this);
+
+          // Initialize the carousel
+          this.uiBuilderCarousel.addClass("slide");
+          this.uiBuilderCarousel.addClass("carousel");
+          this.uiBuilderCarousel.carousel({
+            interval: false,
+          });
+          // Need active class on at least one item for carousel to work properly
+          this.uiBuilderCarousel.find(".item").first().addClass("active");
+        } catch (error) {
+          console.log(
+            "There was an error rendering the UI filter builders in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Create and insert the component that is used to edit the fields attribute of a
+       * Filter Model. Save it to the view so that the selected fields can be accessed
+       * on save.
+       */
+      renderFieldInput: function () {
+        try {
+          var view = this;
+          var selectedFields = _.clone(view.model.get("fields"));
+          view.fieldInput = new QueryFieldSelect({
+            selected: selectedFields,
+            inputLabel: "Select one or more metadata fields",
+            excludeFields: view.excludeFields,
+            addFields: view.specialFields,
+            separatorText: view.model.get("fieldsOperator"),
+          });
+          view.modalEl
+            .find("." + view.classes.fieldsContainer)
+            .append(view.fieldInput.el);
+          view.fieldInput.render();
+
+          // When the field input is changed, limit UI options to options that match
+          // this field type
+          view.fieldInput.off("changeSelection");
+          view.fieldInput.on("changeSelection", function (selectedFields) {
+            view.handleFieldChange.call(view, selectedFields);
+          });
+        } catch (error) {
+          console.log(
+            "There was an error rendering a fields input in a FilterEditorView" +
+              " Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Run whenever the user selects or removes fields from the Query Field input.
+       * This function checks which filter UIs support the type of Query Field selected,
+       * and then blocks or enables the UIs in the editor. This is done to help prevent
+       * users from building mis-matched search filters, e.g. "Year Slider" filters with
+       * text query fields.
+       * @param {string[]} selectedFields The Query Field names (i.e. Solr field names)
+       * of the newly selected fields
+       */
+      handleFieldChange: function (selectedFields) {
+        try {
+          var view = this;
+
+          // Enable all UI types if no field is selected yet
+          if (
+            !selectedFields ||
+            !selectedFields.length ||
+            selectedFields[0] === ""
+          ) {
+            this.uiBuilders.forEach(function (uiBuilder) {
+              view.allowUI(uiBuilder);
+            });
+            return;
           }
-        },
 
-        /**
-         * Hides all filter builder UIs and displays a warning message indicating that
-         * there are currently no UI options that support the selected fields.
-         * @param {string} message A message to show. If not set, then the string set in
-         * the view's text.noFilterOption attribute is used.
-         * @param {string} [type="warning"] The type of message to display (warning,
-         * error, or info)
-         */
-        showNoFilterOptionMessage: function (message, type="warning") {
-          try {
-            this.noFilterOptions = true;
-            if (!message) {
-              message = this.text.noFilterOption
+          // If at least one field is selected, then limit the available UI types to
+          // those that match the type of Query Field.
+          var type =
+            MetacatUI.queryFields.getRequiredFilterType(selectedFields);
+
+          this.uiBuilders.forEach(function (uiBuilder) {
+            if (
+              uiBuilder.filterTypes.includes(type) &&
+              view.isBuilderAllowedForFields(uiBuilder, selectedFields)
+            ) {
+              view.allowUI(uiBuilder);
+            } else {
+              view.blockUI(uiBuilder);
             }
-            if (this.noFilterOptionMessageEl) {
-              this.noFilterOptionMessageEl.remove()
+          });
+        } catch (error) {
+          console.log(
+            "There was an error handling a field change in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Marks a UI builder is blocked (so that it can't be selected) and updates the
+       * tooltip with text explaining that this UI can't be used with the selected
+       * fields. If the UI to block is the currently selected UI, then switches to the
+       * next allowed UI. If there are no UIs that are allowed, then shows a message and
+       * hides all UI builders.
+       * @param {UIBuilderOption} uiBuilder - The UI builder Object to block
+       */
+      blockUI: function (uiBuilder) {
+        try {
+          var view = this;
+          uiBuilder.allowed = false;
+          uiBuilder.button.addClass("disabled");
+          uiBuilder.button.tooltip("destroy");
+          uiBuilder.button.tooltip({
+            title: view.text.filterNotAllowed,
+            delay: {
+              show: 400,
+              hide: 50,
+            },
+          });
+          // If the current UI is a blocked one...
+          if (this.currentUIBuilder === uiBuilder) {
+            // ... switch to the next unblocked one.
+            var allowedUIBuilder = _.findWhere(this.uiBuilders, {
+              allowed: true,
+            });
+            if (allowedUIBuilder) {
+              view.switchFilterType(allowedUIBuilder.modelType);
+            } else {
+              // If there is no UI available, then show a message
+              this.showNoFilterOptionMessage();
             }
-            this.noFilterOptionMessageEl = $(
-              '<div class="alert alert-' + type + '">' + message + '</div>'
-            )
-            this.uiBuilderCarousel.hide()
-            this.uiBuilderCarousel.after(this.noFilterOptionMessageEl)
           }
-          catch (error) {
-            console.log(
-              'There was an error showing a message to indicate that no filter builder ' +
-              'UI options are allowed in a FilterEditorView' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error blocking a filter UI builder in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Marks a UI builder is allowed (so that it can be selected) and updates the
+       * tooltip text with the description of this UI. If it's displayed, this function
+       * hides the message that indicates that there are no allowed UIs that match the
+       * selected query fields.
+       * @param {UIBuilderOption} uiBuilder - The UI builder Object to block
+       */
+      allowUI: function (uiBuilder) {
+        try {
+          // If at least one UI is allowed, then make sure the "no filter message" is
+          // hidden.
+          if (this.noFilterOptions) {
+            this.hideNoFilterOptionMessage();
           }
-        },
-
-        /**
-         * Removes the message displayed by the
-         * {@link FilterEditorView#showNoFilterOptionMessage} function and un-hides all
-         * the filter builder UIs.
-         */
-        hideNoFilterOptionMessage: function () {
-          try {
-            this.noFilterOptions = false;
-            if (this.noFilterOptionMessageEl) {
-              this.noFilterOptionMessageEl.remove()
-              console.log(this.noFilterOptionMessageEl);
-            }
-            if (this.uiBuilderCarousel.is(':hidden')) {
-              this.uiBuilderCarousel.show()
-            }
+          uiBuilder.allowed = true;
+          uiBuilder.button.removeClass("disabled");
+          uiBuilder.button.tooltip("destroy");
+          uiBuilder.button.tooltip({
+            title: uiBuilder.description,
+            delay: {
+              show: 900,
+              hide: 50,
+            },
+          });
+        } catch (error) {
+          console.log(
+            "There was an error unblocking a filter UI builder in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Hides all filter builder UIs and displays a warning message indicating that
+       * there are currently no UI options that support the selected fields.
+       * @param {string} message A message to show. If not set, then the string set in
+       * the view's text.noFilterOption attribute is used.
+       * @param {string} [type="warning"] The type of message to display (warning,
+       * error, or info)
+       */
+      showNoFilterOptionMessage: function (message, type = "warning") {
+        try {
+          this.noFilterOptions = true;
+          if (!message) {
+            message = this.text.noFilterOption;
           }
-          catch (error) {
-            console.log(
-              'There was an error hiding a message in a FilterEditorView' +
-              '. Error details: ' + error
-            );
+          if (this.noFilterOptionMessageEl) {
+            this.noFilterOptionMessageEl.remove();
           }
-        },
-
-        /**
-         * Functions to run when a user clicks the "save" button in the editing modal
-         * window. Creates a new Filter model with all of the new attributes that the user
-         * has selected. Checks if the model is valid. If it is, then returns the model.
-         * If it is not, then returns the errors.
-         * @param {Object} event The click event
-         * @return {Object} Returns an object with a success property set to either true
-         * (if there were no errors), or false (if there were errors). If there were
-         * errors, then the object also has an errors property with the errors return from
-         * the Filter validate function. If there were no errors, then the object contains
-         * a model property with the new Filter to be saved to the Filters collection.
-         */
-        createModel: function (event) {
-          try {
-            var selectedUI = this.currentUIBuilder,
-              newModelAttrs = selectedUI.draftModel.toJSON();
-
-            // Set the new fields
-            newModelAttrs.fields = _.clone(this.fieldInput.selected);
-            // set the new fieldsOperator
-            newModelAttrs.fieldsOperator = this.fieldInput.separatorText;
-
-            delete newModelAttrs.objectDOM
-            delete newModelAttrs.cid
-
-            // The collection's model function identifies the type of model to create
-            // based on the filterType attribute. Create a model before we add it to the
-            // collection, so that we can make sure it's valid first, while still allowing
-            // a user to press the UNDO button and not add any changes to the Filters
-            // collection.
-            var newModel = this.collection.model(newModelAttrs);
-
-            // Check if the filter is valid.
-            var newModelErrors = newModel.validate();
-            if (newModelErrors) {
-              return {
-                success: false,
-                errors: newModelErrors
-              }
-            } else {
-              return {
-                success: true,
-                model: newModel
-              }
-            }
+          this.noFilterOptionMessageEl = $(
+            '<div class="alert alert-' + type + '">' + message + "</div>",
+          );
+          this.uiBuilderCarousel.hide();
+          this.uiBuilderCarousel.after(this.noFilterOptionMessageEl);
+        } catch (error) {
+          console.log(
+            "There was an error showing a message to indicate that no filter builder " +
+              "UI options are allowed in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Removes the message displayed by the
+       * {@link FilterEditorView#showNoFilterOptionMessage} function and un-hides all
+       * the filter builder UIs.
+       */
+      hideNoFilterOptionMessage: function () {
+        try {
+          this.noFilterOptions = false;
+          if (this.noFilterOptionMessageEl) {
+            this.noFilterOptionMessageEl.remove();
+            console.log(this.noFilterOptionMessageEl);
           }
-          catch (error) {
-            console.log('There was an error updating a Filter model in a FilterEditorView' +
-              ' Error details: ' + error);
+          if (this.uiBuilderCarousel.is(":hidden")) {
+            this.uiBuilderCarousel.show();
           }
-        },
-
-        /**
-         * Shows errors in the filter editor modal window.
-         * @param {object} errors An object where keys represent the Filter model
-         * attribute that has an error, and the corresponding value explains the error in
-         * text.
-         */
-        handleErrors: function (errors) {
-          try {
-            var view = this;
-
-            // Show a general error message in the modal. (Don't add it twice.)
-            if (view.validationErrorEl) {
-              view.validationErrorEl.remove()
-            }
-            view.validationErrorEl = $('<p class="alert alert-error">' + view.text.validationError + '</p>')
-            this.$el.find(".modal-body").prepend(view.validationErrorEl)
-
-            if (errors) {
-              // Show an error for the "fields" attribute (common to all Filters)
-              if (errors.fields) {
-                view.fieldInput.showMessage(errors.fields, "error", true)
-              }
-
-              // Show errors for the attributes specific to each Filter type
-              view.currentUIBuilder.view.showValidationErrors(errors)
-            }
+        } catch (error) {
+          console.log(
+            "There was an error hiding a message in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Functions to run when a user clicks the "save" button in the editing modal
+       * window. Creates a new Filter model with all of the new attributes that the user
+       * has selected. Checks if the model is valid. If it is, then returns the model.
+       * If it is not, then returns the errors.
+       * @param {Object} event The click event
+       * @return {Object} Returns an object with a success property set to either true
+       * (if there were no errors), or false (if there were errors). If there were
+       * errors, then the object also has an errors property with the errors return from
+       * the Filter validate function. If there were no errors, then the object contains
+       * a model property with the new Filter to be saved to the Filters collection.
+       */
+      createModel: function (event) {
+        try {
+          var selectedUI = this.currentUIBuilder,
+            newModelAttrs = selectedUI.draftModel.toJSON();
+
+          // Set the new fields
+          newModelAttrs.fields = _.clone(this.fieldInput.selected);
+          // set the new fieldsOperator
+          newModelAttrs.fieldsOperator = this.fieldInput.separatorText;
+
+          delete newModelAttrs.objectDOM;
+          delete newModelAttrs.cid;
+
+          // The collection's model function identifies the type of model to create
+          // based on the filterType attribute. Create a model before we add it to the
+          // collection, so that we can make sure it's valid first, while still allowing
+          // a user to press the UNDO button and not add any changes to the Filters
+          // collection.
+          var newModel = this.collection.model(newModelAttrs);
+
+          // Check if the filter is valid.
+          var newModelErrors = newModel.validate();
+          if (newModelErrors) {
+            return {
+              success: false,
+              errors: newModelErrors,
+            };
+          } else {
+            return {
+              success: true,
+              model: newModel,
+            };
           }
-          catch (error) {
-            console.log(
-              'There was an error  in a FilterEditorView' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error updating a Filter model in a FilterEditorView" +
+              " Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Shows errors in the filter editor modal window.
+       * @param {object} errors An object where keys represent the Filter model
+       * attribute that has an error, and the corresponding value explains the error in
+       * text.
+       */
+      handleErrors: function (errors) {
+        try {
+          var view = this;
+
+          // Show a general error message in the modal. (Don't add it twice.)
+          if (view.validationErrorEl) {
+            view.validationErrorEl.remove();
           }
-        },
-
-        /**
-         * Function that takes the event when a user clicks on one of the filter type
-         * options, gets the name of the desired filter type, and passes it to the switch
-         * filter function.
-         * @param {object} event The click event
-         */
-        handleFilterIconClick: function (event) {
-          try {
-
-            // Get the new Filter Type from the click event. The name of the new Filter
-            // Type is stored as a data attribute in the clicked Filter icon.
-            // var filterTypeIcon = 
-            var newFilterType = event.currentTarget.dataset.filterType;
+          view.validationErrorEl = $(
+            '<p class="alert alert-error">' +
+              view.text.validationError +
+              "</p>",
+          );
+          this.$el.find(".modal-body").prepend(view.validationErrorEl);
+
+          if (errors) {
+            // Show an error for the "fields" attribute (common to all Filters)
+            if (errors.fields) {
+              view.fieldInput.showMessage(errors.fields, "error", true);
+            }
 
-            // Pass the Filter Type to the switch filter function
-            this.switchFilterType(newFilterType)
+            // Show errors for the attributes specific to each Filter type
+            view.currentUIBuilder.view.showValidationErrors(errors);
           }
-          catch (error) {
-            console.log('There was an error handling a click event in a FilterEditorView' +
-              ' Error details: ' + error);
+        } catch (error) {
+          console.log(
+            "There was an error  in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Function that takes the event when a user clicks on one of the filter type
+       * options, gets the name of the desired filter type, and passes it to the switch
+       * filter function.
+       * @param {object} event The click event
+       */
+      handleFilterIconClick: function (event) {
+        try {
+          // Get the new Filter Type from the click event. The name of the new Filter
+          // Type is stored as a data attribute in the clicked Filter icon.
+          // var filterTypeIcon =
+          var newFilterType = event.currentTarget.dataset.filterType;
+
+          // Pass the Filter Type to the switch filter function
+          this.switchFilterType(newFilterType);
+        } catch (error) {
+          console.log(
+            "There was an error handling a click event in a FilterEditorView" +
+              " Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Switches the current draft Filter model to a different Filter model type.
+       * Carries over any common attributes from the previously selected filter type.
+       * If no filter type is provided, defaults to type of the view's model
+       * @param {string} newFilterType The name of the filter type to switch to
+       */
+      switchFilterType: function (newFilterType) {
+        try {
+          var view = this;
+
+          // Use the filter type of the model if none is provided.
+          if (!newFilterType) {
+            newFilterType = this.model.type;
           }
-        },
-
-        /**
-         * Switches the current draft Filter model to a different Filter model type.
-         * Carries over any common attributes from the previously selected filter type.
-         * If no filter type is provided, defaults to type of the view's model
-         * @param {string} newFilterType The name of the filter type to switch to
-         */
-        switchFilterType: function (newFilterType) {
-          try {
-
-            var view = this;
-
-            // Use the filter type of the model if none is provided.
-            if (!newFilterType) {
-              newFilterType = this.model.type
-            }
-
-            // Get the properties of the Filter UI Editor for the new filter type.
-            var uiBuilder = _.findWhere(this.uiBuilders, { modelType: newFilterType });
-
-            // Don't allow user to build a mis-matched filter (e.g. text filter with date
-            // field)
-            if (uiBuilder.allowed === false) {
-              return
-            }
-
-            // (Index is used for the carousel)
-            var index = this.uiBuilders.indexOf(uiBuilder);
 
-            // Treat the first Filter in the list of filter UI editor options as the
-            // default
-            if (!uiBuilder) {
-              uiBuilder = this.uiBuilders[0];
-              filterType = uiBuilder.modelType
-            }
-
-            // Create an object with the properties to pass on to the new draft model
-            var newModelAttrs = {}
-
-            // If there is a currently selected UI editor, then find the common model
-            // attributes that we should pass on to the new UI editor type
-            if (this.currentUIBuilder) {
-              newModelAttrs = this.getCommonAttributes(
-                this.currentUIBuilder.draftModel,
-                newFilterType
-              )
-            }
-            // All search filter models are UI Filter Type
-            newModelAttrs.isUIFilterType = true
-
-            // If a UI editor has already been created for this Filter Type, then just
-            // update the pre-existing draft model. This way, if a user has already
-            // selected content that is specific to a filter type (e.g. choices for a
-            // choiceFilter), that content will still be there when they switch back to
-            // it. Otherwise, use a clone of the model set on this view. We will update
-            // the actual model in the Filters collection only when the user clicks save.
-            if (!uiBuilder.draftModel) {
-              if (this.model.type == newFilterType) {
-                uiBuilder.draftModel = this.model.clone()
-              } else {
-                uiBuilder.draftModel = uiBuilder.modelFunction({ isUIFilterType: true });
-              }
-            }
-
-            if (Object.keys(newModelAttrs).length) {
-              uiBuilder.draftModel.set(newModelAttrs)
-            }
-            // Save the new selection to the view
-            this.currentUIBuilder = uiBuilder;
-
-            // Find the container for this filter type
-            var uiBuilderContainer = uiBuilder.container;
-
-            // Create or update view
-            this.currentUIBuilder.view = this.currentUIBuilder.uiFunction(uiBuilder.draftModel);
-            uiBuilderContainer.html(this.currentUIBuilder.view.el)
-            this.currentUIBuilder.view.render();
+          // Get the properties of the Filter UI Editor for the new filter type.
+          var uiBuilder = _.findWhere(this.uiBuilders, {
+            modelType: newFilterType,
+          });
 
-            // Add the selected/active class to the clicked FilterTypeIcon, remove it from
-            // the other icons.
-            this.uiBuilders.forEach(function (uiBuilder) {
-              uiBuilder.button.removeClass(view.classes.uiBuilderChoiceActive)
-            })
-            this.currentUIBuilder.button.addClass(view.classes.uiBuilderChoiceActive);
+          // Don't allow user to build a mis-matched filter (e.g. text filter with date
+          // field)
+          if (uiBuilder.allowed === false) {
+            return;
+          }
 
-            // Have the carousel slide to the selected uiBuilder container.
-            this.uiBuilderCarousel.carousel(index)
+          // (Index is used for the carousel)
+          var index = this.uiBuilders.indexOf(uiBuilder);
 
+          // Treat the first Filter in the list of filter UI editor options as the
+          // default
+          if (!uiBuilder) {
+            uiBuilder = this.uiBuilders[0];
+            filterType = uiBuilder.modelType;
           }
-          catch (error) {
-            console.log(
-              'There was an error switching filter types in a FilterEditorView.' +
-              ' Error details: ' + error);
-          }
-        },
 
-        /**
-         * Checks for attribute keys that are the same between a given Filter model, and a
-         * new Filter model type. Returns an object of model attributes that are relevant
-         * to the new Filter model type. The values for this object will be pulled from
-         * the given model. objectDOM, cid, and nodeName attributes are always excluded.
-         *
-         * @param {Filter} filterModel A filter model
-         * @param {string} newFilterType The name of the new filter model type
-         *
-         * @returns {Object} returns the model attributes from the given filterModel that
-         * are also relevant to the new Filter model type.
-         */
-        getCommonAttributes: function (filterModel, newFilterType) {
-          try {
-
-            // The filter model attributes that are common to both the current Filter Model
-            // and the new Filter Type that we want to create.
-            var commonAttributes = {};
-
-            // Given the newFilterType string, get the default attribute names for a new
-            // model of that type. 
-            var uiBuilder = _.findWhere(this.uiBuilders, { modelType: newFilterType });
-            var defaultAttrs = uiBuilder.modelFunction().defaults();
-            var defaultAttrNames = Object.keys(defaultAttrs);
-
-            // Check if any of those attribute types exist in the current filter model.
-            // If they do, include them in the common attributes object.
-            var currentAttrs = filterModel.toJSON();
-            defaultAttrNames.forEach(function (attrName) {
-              var valueInDraftModel = currentAttrs[attrName];
-              if (valueInDraftModel || valueInDraftModel === 0 | valueInDraftModel === false) {
-                commonAttributes[attrName] = valueInDraftModel
-              }
-            }, this);
-
-            // Exclude attributes that shouldn't be passed to a new model, like the
-            // objectDOM and the model ID.
-            delete commonAttributes.objectDOM
-            delete commonAttributes.cid
-            delete commonAttributes.nodeName
-
-            // Return the common attributes
-            return commonAttributes
+          // Create an object with the properties to pass on to the new draft model
+          var newModelAttrs = {};
+
+          // If there is a currently selected UI editor, then find the common model
+          // attributes that we should pass on to the new UI editor type
+          if (this.currentUIBuilder) {
+            newModelAttrs = this.getCommonAttributes(
+              this.currentUIBuilder.draftModel,
+              newFilterType,
+            );
           }
-          catch (error) {
-            console.log(
-              'There was an error getting common model attributes in a FilterEditorView' +
-              '. Error details: ' + error);
+          // All search filter models are UI Filter Type
+          newModelAttrs.isUIFilterType = true;
+
+          // If a UI editor has already been created for this Filter Type, then just
+          // update the pre-existing draft model. This way, if a user has already
+          // selected content that is specific to a filter type (e.g. choices for a
+          // choiceFilter), that content will still be there when they switch back to
+          // it. Otherwise, use a clone of the model set on this view. We will update
+          // the actual model in the Filters collection only when the user clicks save.
+          if (!uiBuilder.draftModel) {
+            if (this.model.type == newFilterType) {
+              uiBuilder.draftModel = this.model.clone();
+            } else {
+              uiBuilder.draftModel = uiBuilder.modelFunction({
+                isUIFilterType: true,
+              });
+            }
           }
-        },
 
-        /**
-         * Determine whether a particular UIBuilder is allowed for a set of
-         * search field names. For use in handleFieldChange to enable or disable
-         * certain UI builders using allowUI/blockUI when the user selects
-         * different search fields.
-         *
-         * @param {UIBuilderOption} uiBuilder The UIBuilderOption object to
-         * check
-         * @param {string[]} selectedFields An array of search field names to
-         * look for restrictions
-         *
-         * @return {boolean} Whether or not the uiBuilder is allowed for all
-         * of selectedFields. Returns true only if all selectedFields are
-         * allowed, not just one or more.
-         */
-        isBuilderAllowedForFields: function (uiBuilder, selectedFields) {
-          // Return true early if this uiBuilder has no blockedFields
-          if (!uiBuilder.blockedFields || uiBuilder.blockedFields.length == 0) {
-            return true;
+          if (Object.keys(newModelAttrs).length) {
+            uiBuilder.draftModel.set(newModelAttrs);
           }
-
-          // Check each blockedField for presence in selectedFields
-          var isAllowed = uiBuilder.blockedFields.map(function (blockedField) {
-            return !selectedFields.includes(blockedField);
+          // Save the new selection to the view
+          this.currentUIBuilder = uiBuilder;
+
+          // Find the container for this filter type
+          var uiBuilderContainer = uiBuilder.container;
+
+          // Create or update view
+          this.currentUIBuilder.view = this.currentUIBuilder.uiFunction(
+            uiBuilder.draftModel,
+          );
+          uiBuilderContainer.html(this.currentUIBuilder.view.el);
+          this.currentUIBuilder.view.render();
+
+          // Add the selected/active class to the clicked FilterTypeIcon, remove it from
+          // the other icons.
+          this.uiBuilders.forEach(function (uiBuilder) {
+            uiBuilder.button.removeClass(view.classes.uiBuilderChoiceActive);
           });
-
-          return isAllowed.every(function (e) {
-            return e;
+          this.currentUIBuilder.button.addClass(
+            view.classes.uiBuilderChoiceActive,
+          );
+
+          // Have the carousel slide to the selected uiBuilder container.
+          this.uiBuilderCarousel.carousel(index);
+        } catch (error) {
+          console.log(
+            "There was an error switching filter types in a FilterEditorView." +
+              " Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Checks for attribute keys that are the same between a given Filter model, and a
+       * new Filter model type. Returns an object of model attributes that are relevant
+       * to the new Filter model type. The values for this object will be pulled from
+       * the given model. objectDOM, cid, and nodeName attributes are always excluded.
+       *
+       * @param {Filter} filterModel A filter model
+       * @param {string} newFilterType The name of the new filter model type
+       *
+       * @returns {Object} returns the model attributes from the given filterModel that
+       * are also relevant to the new Filter model type.
+       */
+      getCommonAttributes: function (filterModel, newFilterType) {
+        try {
+          // The filter model attributes that are common to both the current Filter Model
+          // and the new Filter Type that we want to create.
+          var commonAttributes = {};
+
+          // Given the newFilterType string, get the default attribute names for a new
+          // model of that type.
+          var uiBuilder = _.findWhere(this.uiBuilders, {
+            modelType: newFilterType,
           });
+          var defaultAttrs = uiBuilder.modelFunction().defaults();
+          var defaultAttrNames = Object.keys(defaultAttrs);
+
+          // Check if any of those attribute types exist in the current filter model.
+          // If they do, include them in the common attributes object.
+          var currentAttrs = filterModel.toJSON();
+          defaultAttrNames.forEach(function (attrName) {
+            var valueInDraftModel = currentAttrs[attrName];
+            if (
+              valueInDraftModel ||
+              (valueInDraftModel === 0) | (valueInDraftModel === false)
+            ) {
+              commonAttributes[attrName] = valueInDraftModel;
+            }
+          }, this);
+
+          // Exclude attributes that shouldn't be passed to a new model, like the
+          // objectDOM and the model ID.
+          delete commonAttributes.objectDOM;
+          delete commonAttributes.cid;
+          delete commonAttributes.nodeName;
+
+          // Return the common attributes
+          return commonAttributes;
+        } catch (error) {
+          console.log(
+            "There was an error getting common model attributes in a FilterEditorView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Determine whether a particular UIBuilder is allowed for a set of
+       * search field names. For use in handleFieldChange to enable or disable
+       * certain UI builders using allowUI/blockUI when the user selects
+       * different search fields.
+       *
+       * @param {UIBuilderOption} uiBuilder The UIBuilderOption object to
+       * check
+       * @param {string[]} selectedFields An array of search field names to
+       * look for restrictions
+       *
+       * @return {boolean} Whether or not the uiBuilder is allowed for all
+       * of selectedFields. Returns true only if all selectedFields are
+       * allowed, not just one or more.
+       */
+      isBuilderAllowedForFields: function (uiBuilder, selectedFields) {
+        // Return true early if this uiBuilder has no blockedFields
+        if (!uiBuilder.blockedFields || uiBuilder.blockedFields.length == 0) {
+          return true;
         }
-      })
-    return FilterEditorView
-  });
+ + // Check each blockedField for presence in selectedFields + var isAllowed = uiBuilder.blockedFields.map(function (blockedField) { + return !selectedFields.includes(blockedField); + }); + + return isAllowed.every(function (e) { + return e; + }); + }, + }, + ); + return FilterEditorView; +}); +
diff --git a/docs/docs/src_js_views_filters_FilterGroupView.js.html b/docs/docs/src_js_views_filters_FilterGroupView.js.html index dfabbf3a2..797756947 100644 --- a/docs/docs/src_js_views_filters_FilterGroupView.js.html +++ b/docs/docs/src_js_views_filters_FilterGroupView.js.html @@ -44,240 +44,248 @@

Source: src/js/views/filters/FilterGroupView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone',
-        'models/filters/FilterGroup',
-        'views/filters/FilterView',
-        'views/filters/BooleanFilterView',
-        'views/filters/ChoiceFilterView',
-        'views/filters/DateFilterView',
-        'views/filters/NumericFilterView',
-        'views/filters/ToggleFilterView',
-        'views/searchSelect/AnnotationFilterView',
-        "views/searchSelect/SearchableSelectView",
-        "views/filters/SemanticFilterView"
-      ],
-  function($, _, Backbone, FilterGroup, FilterView, BooleanFilterView, ChoiceFilterView,
-    DateFilterView, NumericFilterView, ToggleFilterView, AnnotationFilterView, SearchableSelectView, SemanticFilterView) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/filters/FilterGroup",
+  "views/filters/FilterView",
+  "views/filters/BooleanFilterView",
+  "views/filters/ChoiceFilterView",
+  "views/filters/DateFilterView",
+  "views/filters/NumericFilterView",
+  "views/filters/ToggleFilterView",
+  "views/searchSelect/AnnotationFilterView",
+  "views/searchSelect/SearchableSelectView",
+  "views/filters/SemanticFilterView",
+], function (
+  $,
+  _,
+  Backbone,
+  FilterGroup,
+  FilterView,
+  BooleanFilterView,
+  ChoiceFilterView,
+  DateFilterView,
+  NumericFilterView,
+  ToggleFilterView,
+  AnnotationFilterView,
+  SearchableSelectView,
+  SemanticFilterView,
+) {
+  "use strict";
 
   /**
-  * @class FilterGroupView
-  * @classdesc Renders a display of a group of filters
-  * @classcategory Views/Filters
-  * @extends Backbone.View
-  */
+   * @class FilterGroupView
+   * @classdesc Renders a display of a group of filters
+   * @classcategory Views/Filters
+   * @extends Backbone.View
+   */
   var FilterGroupView = Backbone.View.extend(
-    /** @lends FilterGroupView.prototype */{
-
-    /**
-    * A FilterGroup model to be rendered in this view
-    * @type {FilterGroup} */
-    model: null,
-
-    /**
-     * A reference to the PortalEditorView
-     * @type {PortalEditorView}
-     */
-    editorView: undefined,
-
-    subviews: new Array(),
-
-    tagName: "div",
-
-    className: "filter-group tab-pane",
-
-    /**
-     * Set to true to render this view as a FilterGroup editor; allow the user to add,
-     * delete, and edit filters within this group.
-     * @type {boolean}
-     * @since 2.17.0
-     */
+    /** @lends FilterGroupView.prototype */ {
+      /**
+       * A FilterGroup model to be rendered in this view
+       * @type {FilterGroup} */
+      model: null,
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      subviews: new Array(),
+
+      tagName: "div",
+
+      className: "filter-group tab-pane",
+
+      /**
+       * Set to true to render this view as a FilterGroup editor; allow the user to add,
+       * delete, and edit filters within this group.
+       * @type {boolean}
+       * @since 2.17.0
+       */
       edit: false,
-    
-    /**
-     * If set to true, then all filters within this group will be collapsible.
-     * See {@link FilterView#collapsible}
-     * @type {boolean}
-     * @since 2.25.0
-     * @default false
-     */
-    collapsible: false,
-
-    initialize: function (options) {
-
-      if( !options || typeof options != "object" ){
-        var options = {};
-      }
-
-      this.model = options.model || new FilterGroup();
-
-      this.editorView = options.editorView || null;
-
-      this.subviews = new Array();
-
-      if(options.edit === true){
-        this.edit = true
-      }
-
-      if (options.collapsible && typeof options.collapsible === "boolean") {
-        this.collapsible = options.collapsible;
-      }
-
-    },
-
-    render: function () {
-
-      var view = this;
 
-      //Add the id attribute from the filter group label
-      this.$el.attr("id", this.model.get("label").replace( /([^a-zA-Z0-9])/g, "") );
-
-      //Attach a reference to this view to the element
-      this.$el.data("view", this);
-
-      // Get the collection of filters from the FilterGroup model
-      var filters = this.model.get("filters");
+      /**
+       * If set to true, then all filters within this group will be collapsible.
+       * See {@link FilterView#collapsible}
+       * @type {boolean}
+       * @since 2.25.0
+       * @default false
+       */
+      collapsible: false,
+
+      initialize: function (options) {
+        if (!options || typeof options != "object") {
+          var options = {};
+        }
 
-      var filtersRow = $(document.createElement("div")).addClass("filters-container");
-      this.$el.append(filtersRow);
+        this.model = options.model || new FilterGroup();
 
-      // If this is a FilterGroup editor, pass the "edit" status on to the Filter Views
-      // so that a user can make changes to filters in this group.
-      var filterMode = this.edit ? "edit" : "regular"
+        this.editorView = options.editorView || null;
 
-      //Render each filter model in the FilterGroup model
-      filters.each(function(filter, i){
+        this.subviews = new Array();
 
-        // The options to pass on to every FilterView
-        var viewOptions = {
-          model: filter,
-          mode: filterMode,
-          editorView: this.editorView,
-          collapsible: this.collapsible
+        if (options.edit === true) {
+          this.edit = true;
         }
 
-        //Some filters are handled specially
-        //The isPartOf filter should be rendered as a ToggleFilter
-        if( filter.get && filter.get("fields").includes("isPartOf") ){
-
-          //Set a trueValue on the model so it works with the ToggleView
-          if( filter.get("values").length && filter.get("values")[0] ){
-            filter.set("trueValue", filter.get("values")[0]);
+        if (options.collapsible && typeof options.collapsible === "boolean") {
+          this.collapsible = options.collapsible;
+        }
+      },
+
+      render: function () {
+        var view = this;
+
+        //Add the id attribute from the filter group label
+        this.$el.attr(
+          "id",
+          this.model.get("label").replace(/([^a-zA-Z0-9])/g, ""),
+        );
+
+        //Attach a reference to this view to the element
+        this.$el.data("view", this);
+
+        // Get the collection of filters from the FilterGroup model
+        var filters = this.model.get("filters");
+
+        var filtersRow = $(document.createElement("div")).addClass(
+          "filters-container",
+        );
+        this.$el.append(filtersRow);
+
+        // If this is a FilterGroup editor, pass the "edit" status on to the Filter Views
+        // so that a user can make changes to filters in this group.
+        var filterMode = this.edit ? "edit" : "regular";
+
+        //Render each filter model in the FilterGroup model
+        filters.each(function (filter, i) {
+          // The options to pass on to every FilterView
+          var viewOptions = {
+            model: filter,
+            mode: filterMode,
+            editorView: this.editorView,
+            collapsible: this.collapsible,
+          };
+
+          //Some filters are handled specially
+          //The isPartOf filter should be rendered as a ToggleFilter
+          if (filter.get && filter.get("fields").includes("isPartOf")) {
+            //Set a trueValue on the model so it works with the ToggleView
+            if (filter.get("values").length && filter.get("values")[0]) {
+              filter.set("trueValue", filter.get("values")[0]);
+            }
+
+            //Create a ToggleView
+            var filterView = new ToggleFilterView(viewOptions);
+          } else if (
+            view.areAllFieldsSemantic(filter.get("fields")) &&
+            MetacatUI.appModel.get("bioportalAPIKey") &&
+            filter.type === "Filter"
+          ) {
+            var filterView = new SemanticFilterView(viewOptions);
+          } else {
+            //Depending on the filter type, create a filter view
+            switch (filter.type) {
+              case "Filter":
+                var filterView = new FilterView(viewOptions);
+                break;
+              case "BooleanFilter":
+                // TODO: Set up "edit" and "uiBuilder" mode for BooleanFilters
+                var filterView = new BooleanFilterView(viewOptions);
+                break;
+              case "ChoiceFilter":
+                var filterView = new ChoiceFilterView(viewOptions);
+                break;
+              case "DateFilter":
+                var filterView = new DateFilterView(viewOptions);
+                break;
+              case "NumericFilter":
+                // TODO: Set up "edit" and "uiBuilder" mode for numeric filters
+                var filterView = new NumericFilterView({ model: filter });
+                break;
+              case "ToggleFilter":
+                var filterView = new ToggleFilterView(viewOptions);
+                break;
+              default:
+                var filterView = new FilterView(viewOptions);
+            }
           }
 
-          //Create a ToggleView
-          var filterView = new ToggleFilterView(viewOptions);
-        }
-        else if (view.areAllFieldsSemantic(filter.get("fields")) && MetacatUI.appModel.get("bioportalAPIKey") && filter.type === "Filter") {
-          var filterView = new SemanticFilterView(viewOptions);
+          //Render the view and append it's element to this view
+          filterView.render();
+
+          //Append the filter view element to the view el
+          filtersRow.append(filterView.el);
+
+          //Save a reference to this subview
+          this.subviews.push(filterView);
+        }, this);
+
+        // Insert a button to add a new Filter model if this view is in edit mode
+        if (this.edit) {
+          require(["views/filters/FilterEditorView"], function (FilterEditor) {
+            var addFilterButton = new FilterEditor({
+              collection: view.model.get("filters"),
+              mode: "edit",
+              isNew: true,
+              editorView: view.editorView,
+            });
+            // Render the view and append it's element to this view
+            addFilterButton.render();
+            // Append the filter view element to the view el
+            filtersRow.append(addFilterButton.el);
+            // Save a reference to this subview
+            view.subviews.push(addFilterButton);
+          });
         }
-        else{
-
-          //Depending on the filter type, create a filter view
-          switch( filter.type ){
-            case "Filter":
-              var filterView = new FilterView(viewOptions);
-              break;
-            case "BooleanFilter":
-              // TODO: Set up "edit" and "uiBuilder" mode for BooleanFilters
-              var filterView = new BooleanFilterView(viewOptions);
-              break;
-            case "ChoiceFilter":
-              var filterView = new ChoiceFilterView(viewOptions);
-              break;
-            case "DateFilter":
-              var filterView = new DateFilterView(viewOptions);
-              break;
-            case "NumericFilter":
-              // TODO: Set up "edit" and "uiBuilder" mode for numeric filters
-              var filterView = new NumericFilterView({ model: filter });
-              break;
-            case "ToggleFilter":
-              var filterView = new ToggleFilterView(viewOptions);
-              break;
-            default:
-              var filterView = new FilterView(viewOptions);
+      },
+
+      /**
+       * Actions to perform after the render() function has completed and this view's
+       * element is added to the webpage.
+       */
+      postRender: function () {
+        //Iterate over each subview and call postRender() if it exists
+        _.each(this.subviews, function (subview) {
+          if (subview.postRender) {
+            subview.postRender();
           }
+        });
+      },
+
+      /**
+       * Helper function to check whether or not a set of query field names are
+       * all semantic fields.
+       *
+       * Checks the array "fields"
+       *
+       * @param {string[]} fields The list of query fields to check
+       * @return {boolean} Whether or not all members of fields are semantic.
+       * Returns true only when fields and AppModel.querySemanticFields are
+       * non-zero in length and all values of fields are present in
+       * AppModel.querySemanticFields.
+       */
+      areAllFieldsSemantic: function (fields) {
+        if (!fields || !fields.length) {
+          return false;
         }
 
-        //Render the view and append it's element to this view
-        filterView.render();
-
-        //Append the filter view element to the view el
-        filtersRow.append(filterView.el);
-
-        //Save a reference to this subview
-        this.subviews.push(filterView);
+        var querySemanticFields = MetacatUI.appModel.get("querySemanticFields");
 
-      }, this);
-
-      // Insert a button to add a new Filter model if this view is in edit mode
-      if(this.edit){
-        require(['views/filters/FilterEditorView'], function (FilterEditor) {
-          var addFilterButton = new FilterEditor({
-            collection: view.model.get("filters"),
-            mode: "edit",
-            isNew: true,
-            editorView: view.editorView
-          });
-          // Render the view and append it's element to this view
-          addFilterButton.render();
-          // Append the filter view element to the view el
-          filtersRow.append(addFilterButton.el);
-          // Save a reference to this subview
-          view.subviews.push(addFilterButton);
-        })
-      }
-
-    },
-
-    /**
-    * Actions to perform after the render() function has completed and this view's
-    * element is added to the webpage.
-    */
-    postRender: function(){
-
-      //Iterate over each subview and call postRender() if it exists
-      _.each( this.subviews, function(subview){
-
-        if( subview.postRender ){
-          subview.postRender();
+        if (!querySemanticFields) {
+          return false;
         }
 
-      });
-
+        return fields.every(function (field) {
+          return querySemanticFields.includes(field);
+        });
+      },
     },
-
-    /**
-     * Helper function to check whether or not a set of query field names are
-     * all semantic fields.
-     *
-     * Checks the array "fields"
-     *
-     * @param {string[]} fields The list of query fields to check
-     * @return {boolean} Whether or not all members of fields are semantic.
-     * Returns true only when fields and AppModel.querySemanticFields are
-     * non-zero in length and all values of fields are present in
-     * AppModel.querySemanticFields.
-     */
-    areAllFieldsSemantic: function (fields) {
-      if (!fields || !fields.length) {
-        return false;
-      }
-
-      var querySemanticFields = MetacatUI.appModel.get("querySemanticFields");
-
-      if (!querySemanticFields) {
-        return false;
-      }
-
-      return fields.every(function (field) {
-        return querySemanticFields.includes(field);
-      });
-    }
-
-  });
+  );
   return FilterGroupView;
 });
 
diff --git a/docs/docs/src_js_views_filters_FilterGroupsView.js.html b/docs/docs/src_js_views_filters_FilterGroupsView.js.html index 9d8eff1ef..848431e5e 100644 --- a/docs/docs/src_js_views_filters_FilterGroupsView.js.html +++ b/docs/docs/src_js_views_filters_FilterGroupsView.js.html @@ -44,996 +44,1114 @@

Source: src/js/views/filters/FilterGroupsView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone',
-        'collections/Filters',
-        'models/filters/Filter',
-        'models/filters/FilterGroup',
-        'views/filters/FilterGroupView',
-        'views/filters/FilterView',
-        'text!templates/filters/filterGroups.html'],
-  function($, _, Backbone, Filters, Filter, FilterGroup, FilterGroupView, FilterView, Template) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/Filters",
+  "models/filters/Filter",
+  "models/filters/FilterGroup",
+  "views/filters/FilterGroupView",
+  "views/filters/FilterView",
+  "text!templates/filters/filterGroups.html",
+], function (
+  $,
+  _,
+  Backbone,
+  Filters,
+  Filter,
+  FilterGroup,
+  FilterGroupView,
+  FilterView,
+  Template,
+) {
+  "use strict";
 
   /**
-  * @class FilterGroupsView
-  * @classdesc Creates a view of one or more FilterGroupViews
-  * @classcategory Views/Filters
-  * @name FilterGroupsView
-  * @extends Backbone.View
-  * @constructor
-  */
+   * @class FilterGroupsView
+   * @classdesc Creates a view of one or more FilterGroupViews
+   * @classcategory Views/Filters
+   * @name FilterGroupsView
+   * @extends Backbone.View
+   * @constructor
+   */
   var FilterGroupsView = Backbone.View.extend(
-    /** @lends FilterGroupsView.prototype */{
-
-    /**
-    * The FilterGroup models to display in this view
-    * @type {FilterGroup[]}
-    */
-    filterGroups: [],
-
-    /**
-    * The Filters Collection that contains the same Filter
-    * models from each FilterGroup and any additional Filter Models that may not be in
-    * FilterGroups because they're not displayed or applied behind the scenes.
-    * @type {Filters}
-    */
-    filters: null,
-    
-    /**
-     * A reference to the PortalEditorView
-     * @type {PortalEditorView}
-    */
-    editorView: undefined,
-
-    /**
-    * @inheritdoc
-    */
-    tagName: "div",
-
-    /**
-    * @inheritdoc
-    */
-    className: "filter-groups tabbable",
-
-    /**
-     * The template for this view. An HTML file is converted to an Underscore.js template
-     * @since 2.17.0
-     */
-    template: _.template(Template),
-
-    /**
-    * If true, displays the FilterGroups in a vertical list
-    * @type {Boolean}
-    */
-    vertical: false,
-
-    /**
-     * Set to true to render this view as a FilterGroups editor; allow the user add, edit,
-     * and remove FilterGroups (TODO), and to add, delete, and edit filters within groups.
-     * @type {boolean}
-     * @since 2.17.0
-     */
-    edit: false,
-    
-    /**
-     * If set to true, then all filters within this group will be collapsible.
-     * See {@link FilterView#collapsible}
-     * @type {boolean}
-     * @since 2.25.0
-     * @default false
-     */
-    collapsible: false,
-
-    /**
-     * The initial query to use when the view is first rendered. This is a text value
-     * that will be set on the general `text` Solr field.
-     * @type {string}
-     * @since 2.25.0
-     */
-    initialQuery: undefined,
-
-    /**
-    * @inheritdoc
-    */
-    events: {
-      "click .remove-filter" : "handleRemove",
-      "click .clear-all"     : "removeAllFilters"
-    },
-
-    /**
-    * @inheritdoc
-    */
-    initialize: function (options) {
-
-      if( !options || typeof options != "object" ){
-        var options = {};
-      }
-
-      this.filterGroups = options.filterGroups || new Array();
-      this.filters = options.filters || new Filters();
-
-      // For portal search filters, ID filters should be added to the query with an AND
-      // operator, so that ID searches search *within* the definition collection.
-      if(this.filters){
-        this.filters.mustMatchIds = true
-      }
-
-      if( options.vertical == true ){
-        this.vertical = true;
-      }
-
-      this.parentView = options.parentView || null;
-      this.editorView = options.editorView || null;
-
-      if(options.edit === true){
-        this.edit = true
-      }
-
-      if (options.initialQuery) {
-        this.initialQuery = options.initialQuery;
-      }
-
-      if (options.collapsible && typeof options.collapsible === "boolean") {
-        this.collapsible = options.collapsible;
-      }
+    /** @lends FilterGroupsView.prototype */ {
+      /**
+       * The FilterGroup models to display in this view
+       * @type {FilterGroup[]}
+       */
+      filterGroups: [],
+
+      /**
+       * The Filters Collection that contains the same Filter
+       * models from each FilterGroup and any additional Filter Models that may not be in
+       * FilterGroups because they're not displayed or applied behind the scenes.
+       * @type {Filters}
+       */
+      filters: null,
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * @inheritdoc
+       */
+      tagName: "div",
+
+      /**
+       * @inheritdoc
+       */
+      className: "filter-groups tabbable",
+
+      /**
+       * The template for this view. An HTML file is converted to an Underscore.js template
+       * @since 2.17.0
+       */
+      template: _.template(Template),
+
+      /**
+       * If true, displays the FilterGroups in a vertical list
+       * @type {Boolean}
+       */
+      vertical: false,
+
+      /**
+       * Set to true to render this view as a FilterGroups editor; allow the user add, edit,
+       * and remove FilterGroups (TODO), and to add, delete, and edit filters within groups.
+       * @type {boolean}
+       * @since 2.17.0
+       */
+      edit: false,
+
+      /**
+       * If set to true, then all filters within this group will be collapsible.
+       * See {@link FilterView#collapsible}
+       * @type {boolean}
+       * @since 2.25.0
+       * @default false
+       */
+      collapsible: false,
+
+      /**
+       * The initial query to use when the view is first rendered. This is a text value
+       * that will be set on the general `text` Solr field.
+       * @type {string}
+       * @since 2.25.0
+       */
+      initialQuery: undefined,
+
+      /**
+       * @inheritdoc
+       */
+      events: {
+        "click .remove-filter": "handleRemove",
+        "click .clear-all": "removeAllFilters",
+      },
+
+      /**
+       * @inheritdoc
+       */
+      initialize: function (options) {
+        if (!options || typeof options != "object") {
+          var options = {};
+        }
 
-    },
+        this.filterGroups = options.filterGroups || new Array();
+        this.filters = options.filters || new Filters();
 
-    /**
-    * @inheritdoc
-    */
-    render: function () {
-
-      //Since this view may be re-rendered at some point, empty the element and remove listeners
-      this.$el.empty();
-      this.stopListening();
-
-      // Add information about editing the filter groups if this view is in edit mode
-      if(this.edit){
-        var title = "Change how people can search for data within your collection"
-        var isNew = this.filterGroups.length === 0;
-        if (this.filterGroups.length === 1 && this.filterGroups[0].isEmpty()) {
-          var isNew = true;
+        // For portal search filters, ID filters should be added to the query with an AND
+        // operator, so that ID searches search *within* the definition collection.
+        if (this.filters) {
+          this.filters.mustMatchIds = true;
         }
 
-        if (isNew) {
-          title = "Add filters to help people find data within your collection"
+        if (options.vertical == true) {
+          this.vertical = true;
         }
 
-        var description = "Search filters allow people to filter your data by specific " +
-          "metadata fields.",
-          learnMoreUrl = MetacatUI.appModel.get("portalSearchFiltersInfoURL");
+        this.parentView = options.parentView || null;
+        this.editorView = options.editorView || null;
 
-        if (learnMoreUrl) {
-          description = description + ' <a href="' + learnMoreUrl + '">Learn more</a>'
+        if (options.edit === true) {
+          this.edit = true;
         }
 
-        this.$el.html(this.template({
-          title: title,
-          description: description,
-          helpText: ""
-        }));
-
-        // Remove this when the custom search filter builder is no longer new:
-        this.$el
-          .find(".port-editor-subtitle")
-          .append($('<span class="new-icon" style="margin-left:10px; font-size:1rem; line-height: 25px;"><i class="icon icon-star icon-on-right"></i> NEW </span>'));
-      }
-
-        
-      //Create an unordered list for all the filter tabs
-      var groupTabs = $(document.createElement("ul")).addClass("nav nav-tabs filter-group-links");
-
-      // Until we allow adding/editing filter groups in the portal data page, hide the group tabs
-      // element if the portal does not already have groups in the editor.
-      if (this.filterGroups.length === 1 && !this.filterGroups[0].get("label") && !this.filterGroups[0].get("icon")){
-        groupTabs.hide()
-      }
-
-      //Create a container div for the filter groups
-      var filterGroupContainer = $(document.createElement("div")).addClass("tab-content");
-
-      //Add the filter group elements to this view
-      this.$el.append(groupTabs, filterGroupContainer);
-
-      var divideIntoGroups = true;
-
-      _.each( this.filterGroups, function(filterGroup){
-
-        //If there is only one filter group specified, and there is no label or icon,
-        // then don't divide the filters into separate filter groups
-        if( this.filterGroups.length == 1 && !this.filterGroups[0].get("label") &&
-            !this.filterGroups[0].get("icon") ){
-          divideIntoGroups = false;
+        if (options.initialQuery) {
+          this.initialQuery = options.initialQuery;
         }
 
-        if( divideIntoGroups ){
-          //Create a link to the filter group
-          var groupTab  = $(document.createElement("li")).addClass("filter-group-link");
-          var groupLink = $(document.createElement("a"))
-                              .attr("href", "#" + filterGroup.get("label").replace( /([^a-zA-Z0-9])/g, "") )
-                              .attr("data-toggle", "tab");
-
-          //Add the FilterGroup icon
-          if( filterGroup.get("icon") ){
-            groupLink.append( $(document.createElement("i")).addClass("icon icon-" + filterGroup.get("icon")) );
+        if (options.collapsible && typeof options.collapsible === "boolean") {
+          this.collapsible = options.collapsible;
+        }
+      },
+
+      /**
+       * @inheritdoc
+       */
+      render: function () {
+        //Since this view may be re-rendered at some point, empty the element and remove listeners
+        this.$el.empty();
+        this.stopListening();
+
+        // Add information about editing the filter groups if this view is in edit mode
+        if (this.edit) {
+          var title =
+            "Change how people can search for data within your collection";
+          var isNew = this.filterGroups.length === 0;
+          if (
+            this.filterGroups.length === 1 &&
+            this.filterGroups[0].isEmpty()
+          ) {
+            var isNew = true;
           }
 
-          //Add the FilterGroup label
-          if( filterGroup.get("label") ){
-            groupLink.append(filterGroup.get("label"));
+          if (isNew) {
+            title =
+              "Add filters to help people find data within your collection";
           }
 
-          //Insert the link into the tab and add the tab to the tab list
-          groupTab.append(groupLink);
-          groupTabs.append(groupTab);
+          var description =
+              "Search filters allow people to filter your data by specific " +
+              "metadata fields.",
+            learnMoreUrl = MetacatUI.appModel.get("portalSearchFiltersInfoURL");
 
-          //Create a tooltip for the link
-          groupTab.tooltip({
-            placement: "top",
-            title: filterGroup.get("description"),
-            trigger: "hover",
-            delay: {
-              show: 800
-            }
-          });
+          if (learnMoreUrl) {
+            description =
+              description + ' <a href="' + learnMoreUrl + '">Learn more</a>';
+          }
 
-          //Make all the tab widths equal
-          groupTab.css("width", (100 / this.filterGroups.length) + "%");
+          this.$el.html(
+            this.template({
+              title: title,
+              description: description,
+              helpText: "",
+            }),
+          );
+
+          // Remove this when the custom search filter builder is no longer new:
+          this.$el
+            .find(".port-editor-subtitle")
+            .append(
+              $(
+                '<span class="new-icon" style="margin-left:10px; font-size:1rem; line-height: 25px;"><i class="icon icon-star icon-on-right"></i> NEW </span>',
+              ),
+            );
         }
 
-        // Create a FilterGroupView. Ensure the FilterGroup is in edit mode if the parent
-        // FilterGroups is.
-        var filterGroupView = new FilterGroupView({
-          model: filterGroup,
-          edit: this.edit,
-          editorView: this.editorView,
-          collapsible: this.collapsible
-        });
-
-        //Render the FilterGroupView
-        filterGroupView.render();
-
-        //Add the FilterGroupView element to this view
-        filterGroupContainer.append(filterGroupView.el);
-
-        //Store a reference to the FilterGroupView in the tab link
-        if( divideIntoGroups ){
-          groupLink.data("view", filterGroupView);
+        //Create an unordered list for all the filter tabs
+        var groupTabs = $(document.createElement("ul")).addClass(
+          "nav nav-tabs filter-group-links",
+        );
+
+        // Until we allow adding/editing filter groups in the portal data page, hide the group tabs
+        // element if the portal does not already have groups in the editor.
+        if (
+          this.filterGroups.length === 1 &&
+          !this.filterGroups[0].get("label") &&
+          !this.filterGroups[0].get("icon")
+        ) {
+          groupTabs.hide();
         }
 
-        //If a new filter is ever added to this filter group, re-render this view
-        this.listenTo( filterGroup.get("filters"), "add remove", this.render );
-      }, this);
-
-      if( divideIntoGroups ){
-        //Mark the first filter group as active
-        groupTabs.children("li").first().addClass("active");
-
-        //When each filter group tab is shown, perform any post render function, if needed.
-        this.$('a[data-toggle="tab"]').on('shown', function (e) {
-          //Get the filter group view
-          var filterGroupView = $(e.target).data("view");
-
-          //If there is a post render function, call it
-          if( filterGroupView && filterGroupView.postRender ){
-            filterGroupView.postRender();
-          }
-
-        });
-      }
-
-      //Mark the first filter group as active
-      var firstFilterGroupEl = filterGroupContainer.find(".filter-group").first();
-      firstFilterGroupEl.addClass("active");
-      var activeFilterGroup = firstFilterGroupEl.data("view");
-
-      //Call postRender() now for the active FilterGroup, since the `shown` event
-      // won't trigger until/unless it's hidden then shown again.
-      if( activeFilterGroup ){
-        activeFilterGroup.postRender();
-      }
-
-      // Applied filters and the general search input are not needed when this view is
-      // in editing mode
-      if(!this.edit){
-        //Add a header element above the filter groups
-        this.$el.prepend( $(document.createElement("div")).addClass("filters-header") );
-
-        //Render the applied filters
-        this.renderAppliedFiltersSection();
-
-        // Render an "All" filter. If the view was initialized with an initial
-        // query, set it on this filter. 
-        this.renderAllFilter(this.initialQuery);
-      }
-
-      if(this.edit){
-        this.$el.addClass("edit-mode");
-      }
-
-      if( this.vertical ){
-        this.$el.addClass("vertical");
-      }
-
-    },
+        //Create a container div for the filter groups
+        var filterGroupContainer = $(document.createElement("div")).addClass(
+          "tab-content",
+        );
+
+        //Add the filter group elements to this view
+        this.$el.append(groupTabs, filterGroupContainer);
+
+        var divideIntoGroups = true;
+
+        _.each(
+          this.filterGroups,
+          function (filterGroup) {
+            //If there is only one filter group specified, and there is no label or icon,
+            // then don't divide the filters into separate filter groups
+            if (
+              this.filterGroups.length == 1 &&
+              !this.filterGroups[0].get("label") &&
+              !this.filterGroups[0].get("icon")
+            ) {
+              divideIntoGroups = false;
+            }
 
-    /**
-    * Renders the section of the view that will display the currently-applied filters
-    */
-    renderAppliedFiltersSection: function(){
+            if (divideIntoGroups) {
+              //Create a link to the filter group
+              var groupTab = $(document.createElement("li")).addClass(
+                "filter-group-link",
+              );
+              var groupLink = $(document.createElement("a"))
+                .attr(
+                  "href",
+                  "#" + filterGroup.get("label").replace(/([^a-zA-Z0-9])/g, ""),
+                )
+                .attr("data-toggle", "tab");
+
+              //Add the FilterGroup icon
+              if (filterGroup.get("icon")) {
+                groupLink.append(
+                  $(document.createElement("i")).addClass(
+                    "icon icon-" + filterGroup.get("icon"),
+                  ),
+                );
+              }
+
+              //Add the FilterGroup label
+              if (filterGroup.get("label")) {
+                groupLink.append(filterGroup.get("label"));
+              }
+
+              //Insert the link into the tab and add the tab to the tab list
+              groupTab.append(groupLink);
+              groupTabs.append(groupTab);
+
+              //Create a tooltip for the link
+              groupTab.tooltip({
+                placement: "top",
+                title: filterGroup.get("description"),
+                trigger: "hover",
+                delay: {
+                  show: 800,
+                },
+              });
+
+              //Make all the tab widths equal
+              groupTab.css("width", 100 / this.filterGroups.length + "%");
+            }
 
-      //Add a title to the header
-      var appliedFiltersContainer = $(document.createElement("div")).addClass("applied-filters-container"),
-          headerText = $(document.createElement("h5"))
-                        .addClass("filters-title")
-                        .text("Current search")
-                        .append( $(document.createElement("a"))
-                                  .text("Clear all")
-                                  .addClass("clear-all")
-                                  .prepend( $(document.createElement("i"))
-                                              .addClass("icon icon-remove icon-on-left") ));
-
-      //Make the applied filters list
-      var appliedFiltersEl = $(document.createElement("ul")).addClass("applied-filters");
-
-      //Add the applied filters element to the filters header
-      appliedFiltersContainer.append(headerText, appliedFiltersEl);
-      this.$(".filters-header").append(appliedFiltersContainer);
-
-      //Get all the nonNumeric filter models. Reject nested filterGroups.
-      var nonNumericFilters = this.filters.reject(function(filterModel){
-        return (["FilterGroup", "NumericFilter", "DateFilter"].includes(filterModel.type));
-      });
-      //Listen to changes on the "values" attribute for nonNumeric filters
-      _.each(nonNumericFilters, function(nonNumericFilter){
-
-        this.listenTo(nonNumericFilter, "change:values", this.updateAppliedFilters);
-
-        if( nonNumericFilter.get("values").length ){
-          this.updateAppliedFilters(nonNumericFilter, { displayWithoutChanges: true });
-        }
-      }, this);
+            // Create a FilterGroupView. Ensure the FilterGroup is in edit mode if the parent
+            // FilterGroups is.
+            var filterGroupView = new FilterGroupView({
+              model: filterGroup,
+              edit: this.edit,
+              editorView: this.editorView,
+              collapsible: this.collapsible,
+            });
 
-      //Get the numeric filters and listen to the min and max values
-      var numericFilters = _.where(this.filters.models, { type: "NumericFilter" });
-      _.each(numericFilters, function(numericFilter){
+            //Render the FilterGroupView
+            filterGroupView.render();
 
-        if( numericFilter.get("range") == true ){
-          this.listenTo(numericFilter, "change:min change:max", this.updateAppliedRangeFilters);
+            //Add the FilterGroupView element to this view
+            filterGroupContainer.append(filterGroupView.el);
 
-          var filterDefaults = numericFilter.defaults();
+            //Store a reference to the FilterGroupView in the tab link
+            if (divideIntoGroups) {
+              groupLink.data("view", filterGroupView);
+            }
 
-          if( numericFilter.get("min") != filterDefaults.min ||
-              numericFilter.get("max") != filterDefaults.max ||
-              numericFilter.get("values").length ){
-            this.updateAppliedRangeFilters(numericFilter, { displayWithoutChanges: true });
-          }
+            //If a new filter is ever added to this filter group, re-render this view
+            this.listenTo(
+              filterGroup.get("filters"),
+              "add remove",
+              this.render,
+            );
+          },
+          this,
+        );
+
+        if (divideIntoGroups) {
+          //Mark the first filter group as active
+          groupTabs.children("li").first().addClass("active");
+
+          //When each filter group tab is shown, perform any post render function, if needed.
+          this.$('a[data-toggle="tab"]').on("shown", function (e) {
+            //Get the filter group view
+            var filterGroupView = $(e.target).data("view");
+
+            //If there is a post render function, call it
+            if (filterGroupView && filterGroupView.postRender) {
+              filterGroupView.postRender();
+            }
+          });
         }
-        else{
-          this.listenTo(numericFilter, "change:values", this.updateAppliedRangeFilters);
 
-          if( numericFilter.get("values")[0] != numericFilter.defaults().values[0] ){
-            this.updateAppliedRangeFilters(numericFilter, { displayWithoutChanges: true });
-          }
+        //Mark the first filter group as active
+        var firstFilterGroupEl = filterGroupContainer
+          .find(".filter-group")
+          .first();
+        firstFilterGroupEl.addClass("active");
+        var activeFilterGroup = firstFilterGroupEl.data("view");
+
+        //Call postRender() now for the active FilterGroup, since the `shown` event
+        // won't trigger until/unless it's hidden then shown again.
+        if (activeFilterGroup) {
+          activeFilterGroup.postRender();
         }
 
-      }, this);
+        // Applied filters and the general search input are not needed when this view is
+        // in editing mode
+        if (!this.edit) {
+          //Add a header element above the filter groups
+          this.$el.prepend(
+            $(document.createElement("div")).addClass("filters-header"),
+          );
 
-      //Get the date filters and listen to the min and max values
-      var dateFilters = _.where(this.filters.models, { type: "DateFilter" });
-      _.each(dateFilters, function(dateFilter){
-        this.listenTo(dateFilter, "change:min change:max", this.updateAppliedRangeFilters);
+          //Render the applied filters
+          this.renderAppliedFiltersSection();
 
-        if( dateFilter.get("min") != dateFilter.defaults().min ||
-            dateFilter.get("max") != dateFilter.defaults().max ){
-          this.updateAppliedRangeFilters(dateFilter, { displayWithoutChanges: true });
+          // Render an "All" filter. If the view was initialized with an initial
+          // query, set it on this filter.
+          this.renderAllFilter(this.initialQuery);
         }
-      }, this);
-
-      //When a Filter has been removed from the Filters collection, remove it's DOM element from the page
-      this.listenTo(this.filters, "remove", function(removedFilter){
-        this.removeAppliedFilterElByModel(removedFilter);
-      });
 
-    },
-
-    /**
-     * Renders an "All" filter that will search the general `text` Solr field
-     * @param {string} searchFor - The initial value of the "All" filter. This
-     * will get set on the filter model and trigger a change event. Optional.
-     */
-    renderAllFilter: function (searchFor="") {
-
-      //Create an "All" filter that will search the general `text` Solr field
-      var filter = new Filter({
-        fields: ["text"],
-        label: "Search",
-        description: "Search the datasets by typing in any keyword, topic, creator, etc.",
-        placeholder: "Search these datasets"
-      });
-      this.filters.add( filter );
-
-      //Create a FilterView for the All filter
-      var filterView = new FilterView({
-        model: filter
-      });
-      this.listenTo(filter, "change:values", this.updateAppliedFilters);
-
-      //Render the view and add the element to the filters header
-      filterView.render();
-      this.$(".filters-header").prepend(filterView.el);
-
-      if (searchFor && searchFor.length) {
-        filter.set('values', [searchFor]);
-      }
-    },
-
-    postRender: function(){
-
-      var groupTabs = this.$(".filter-group-links");
-
-      //Check if there is a difference in heights
-      var maxHeight = 0;
-
-      _.each( groupTabs.find("a"), function(link){
-
-        if( $(link).height() > maxHeight ){
-          maxHeight = $(link).height();
+        if (this.edit) {
+          this.$el.addClass("edit-mode");
         }
 
-      });
-
-      //Set the height of each filter group link so they are all equal
-      _.each( groupTabs.find("a"), function(link){
-
-        if( $(link).height() < maxHeight ){
-          $(link).height(maxHeight + "px");
+        if (this.vertical) {
+          this.$el.addClass("vertical");
         }
-
-      });
-    },
-
-    /**
-    * Renders the values of the given Filter Model in the current filter model
-    *
-    * @param {Filter} filterModel - The FilterModel to display
-    * @param {object} options - Additional options for this function
-    * @property {boolean} options.displayWithoutChanges - If true, this filter will
-    * display even if the value hasn't been changed
-    */
-    updateAppliedFilters: function(filterModel, options){
-
-      //Create an options object if one wasn't sent
-      if( typeof options != "object" ){
-        var options = {};
-      }
-      this.options = options;
-      var view = this;
-
-      //If the value of this filter has changed, or if the displayWithoutChanges option
-      // was passed, and if the filter is not invisible, then display it
-      if( !filterModel.get("isInvisible") &&
-          ((filterModel.changed && filterModel.changed.values) ||
-          options.displayWithoutChanges) ){
-
-        //Get the new values and the previous values
-        var newValues      = options.displayWithoutChanges? filterModel.get("values") : filterModel.changed.values,
-            previousValues = options.displayWithoutChanges? [] : filterModel.previousAttributes().values,
-            //Find the values that were removed
-            removedValues  = _.difference(previousValues, newValues),
-            //Find the values that were added
-            addedValues    = _.difference(newValues, previousValues);
-
-        //If a filter has been added, display it
-        _.each(addedValues, function(value){
-          //Add the applied filter to the view
-          this.$(".applied-filters").append( this.createAppliedFilter(filterModel, value) );
-
-        }, this);
-
-        //Iterate over each removed filter value and remove them
-        _.each(removedValues, function(value){
-          //Find all applied filter elements with a matching value
-          var matchingFilters = this.$(".applied-filter[data-value='" + value + "']");
-
-          //Iterate over each filter element with a matching value
-          _.each(matchingFilters, function(matchingFilter){
-
-            //If this is the filter element associated with this filter model, then remove it
-            if( $(matchingFilter).data("model") == filterModel ){
-              $(matchingFilter).remove();
+      },
+
+      /**
+       * Renders the section of the view that will display the currently-applied filters
+       */
+      renderAppliedFiltersSection: function () {
+        //Add a title to the header
+        var appliedFiltersContainer = $(document.createElement("div")).addClass(
+            "applied-filters-container",
+          ),
+          headerText = $(document.createElement("h5"))
+            .addClass("filters-title")
+            .text("Current search")
+            .append(
+              $(document.createElement("a"))
+                .text("Clear all")
+                .addClass("clear-all")
+                .prepend(
+                  $(document.createElement("i")).addClass(
+                    "icon icon-remove icon-on-left",
+                  ),
+                ),
+            );
+
+        //Make the applied filters list
+        var appliedFiltersEl = $(document.createElement("ul")).addClass(
+          "applied-filters",
+        );
+
+        //Add the applied filters element to the filters header
+        appliedFiltersContainer.append(headerText, appliedFiltersEl);
+        this.$(".filters-header").append(appliedFiltersContainer);
+
+        //Get all the nonNumeric filter models. Reject nested filterGroups.
+        var nonNumericFilters = this.filters.reject(function (filterModel) {
+          return ["FilterGroup", "NumericFilter", "DateFilter"].includes(
+            filterModel.type,
+          );
+        });
+        //Listen to changes on the "values" attribute for nonNumeric filters
+        _.each(
+          nonNumericFilters,
+          function (nonNumericFilter) {
+            this.listenTo(
+              nonNumericFilter,
+              "change:values",
+              this.updateAppliedFilters,
+            );
+
+            if (nonNumericFilter.get("values").length) {
+              this.updateAppliedFilters(nonNumericFilter, {
+                displayWithoutChanges: true,
+              });
             }
+          },
+          this,
+        );
 
-          });
-
-        }, this);
-
-      }
-
-      //Toggle the applied filters header
-      this.toggleAppliedFiltersHeader();
-
-    },
-
-    /**
-     * Hides or shows the applied filter list title/header, as well as the help
-     * message that lets the user know they can add filters when there are none
-     */
-    toggleAppliedFiltersHeader: function(){
-
-      //If there is an applied filter
-      if( this.$(".applied-filter").length ){
-        // hide the "add some filters" help text
-        //$(this.parentView.helpTextContainer).css("display", "none");
-        // show the Clear All button
-        this.$(".filters-title").css("display", "block");
-      }
-      //If there are no applied filters
-      else{
-        // show the "add some filters" help text
-      //  $(this.parentView.helpTextContainer).css("display", "block");
-        // hide the Clear All button
-        this.$(".filters-title").css("display", "none");
-      }
-
-    },
+        //Get the numeric filters and listen to the min and max values
+        var numericFilters = _.where(this.filters.models, {
+          type: "NumericFilter",
+        });
+        _.each(
+          numericFilters,
+          function (numericFilter) {
+            if (numericFilter.get("range") == true) {
+              this.listenTo(
+                numericFilter,
+                "change:min change:max",
+                this.updateAppliedRangeFilters,
+              );
+
+              var filterDefaults = numericFilter.defaults();
+
+              if (
+                numericFilter.get("min") != filterDefaults.min ||
+                numericFilter.get("max") != filterDefaults.max ||
+                numericFilter.get("values").length
+              ) {
+                this.updateAppliedRangeFilters(numericFilter, {
+                  displayWithoutChanges: true,
+                });
+              }
+            } else {
+              this.listenTo(
+                numericFilter,
+                "change:values",
+                this.updateAppliedRangeFilters,
+              );
+
+              if (
+                numericFilter.get("values")[0] !=
+                numericFilter.defaults().values[0]
+              ) {
+                this.updateAppliedRangeFilters(numericFilter, {
+                  displayWithoutChanges: true,
+                });
+              }
+            }
+          },
+          this,
+        );
+
+        //Get the date filters and listen to the min and max values
+        var dateFilters = _.where(this.filters.models, { type: "DateFilter" });
+        _.each(
+          dateFilters,
+          function (dateFilter) {
+            this.listenTo(
+              dateFilter,
+              "change:min change:max",
+              this.updateAppliedRangeFilters,
+            );
+
+            if (
+              dateFilter.get("min") != dateFilter.defaults().min ||
+              dateFilter.get("max") != dateFilter.defaults().max
+            ) {
+              this.updateAppliedRangeFilters(dateFilter, {
+                displayWithoutChanges: true,
+              });
+            }
+          },
+          this,
+        );
 
-    /**
-    * When a NumericFilter or DateFilter model is changed, update the applied filters in the UI
-    * @param {DateFilter|NumericFilter} filterModel - The model whose values to display
-    * @param {object} [options] - Additional options for this function
-    * @property {boolean} [options.displayWithoutChanges] - If true, this filter will display even if the value hasn't been changed
-    */
-    updateAppliedRangeFilters: function(filterModel, options){
+        //When a Filter has been removed from the Filters collection, remove it's DOM element from the page
+        this.listenTo(this.filters, "remove", function (removedFilter) {
+          this.removeAppliedFilterElByModel(removedFilter);
+        });
+      },
+
+      /**
+       * Renders an "All" filter that will search the general `text` Solr field
+       * @param {string} searchFor - The initial value of the "All" filter. This
+       * will get set on the filter model and trigger a change event. Optional.
+       */
+      renderAllFilter: function (searchFor = "") {
+        //Create an "All" filter that will search the general `text` Solr field
+        var filter = new Filter({
+          fields: ["text"],
+          label: "Search",
+          description:
+            "Search the datasets by typing in any keyword, topic, creator, etc.",
+          placeholder: "Search these datasets",
+        });
+        this.filters.add(filter);
 
-      if( !filterModel ){
-        return;
-      }
+        //Create a FilterView for the All filter
+        var filterView = new FilterView({
+          model: filter,
+        });
+        this.listenTo(filter, "change:values", this.updateAppliedFilters);
 
-      if( typeof options === "undefined" || !options ){
-        var options = {};
-      }
+        //Render the view and add the element to the filters header
+        filterView.render();
+        this.$(".filters-header").prepend(filterView.el);
 
-      //If the Filter is invisible, don't render it
-      if( filterModel.get("isInvisible") ){
-        return;
-      }
+        if (searchFor && searchFor.length) {
+          filter.set("values", [searchFor]);
+        }
+      },
 
-      //If the minimum and maximum values are set to the default, remove the filter element
-      if( filterModel.get("min") == filterModel.get("rangeMin") &&
-          filterModel.get("max") == filterModel.get("rangeMax")){
+      postRender: function () {
+        var groupTabs = this.$(".filter-group-links");
 
-        //Find the applied filter element for this filter model
-        _.each(this.$(".applied-filter"), function(filterEl){
+        //Check if there is a difference in heights
+        var maxHeight = 0;
 
-          if( $(filterEl).data("model") == filterModel ){
-            //Remove the applied filter element
-            $(filterEl).remove();
+        _.each(groupTabs.find("a"), function (link) {
+          if ($(link).height() > maxHeight) {
+            maxHeight = $(link).height();
           }
+        });
 
-        }, this);
-
-      }
-      //If the values attribue has changed, or if the displayWithoutChanges attribute was passed
-      else if( (filterModel.changed && (filterModel.changed.min || filterModel.changed.max)) ||
-                options.displayWithoutChanges ){
-
-        //Create the filter label for ranges of numbers
-        var filterValue = filterModel.getReadableValue();
-
-        //Create the applied filter
-        var appliedFilter = this.createAppliedFilter(filterModel, filterValue);
-
-        //Keep track if this filter is already displayed and needs to be replaced
-        var replaced = false;
-
-        //Check if this filter model already has an applied filter in the UI
-        _.each(this.$(".applied-filter"), function(appliedFilterEl){
-
-          //If this applied filter already is displayed, replace it
-          if( $(appliedFilterEl).data("model") == filterModel ){
-            //Replace the applied filter element with the new one
-            $(appliedFilterEl).replaceWith(appliedFilter);
-            replaced = true;
+        //Set the height of each filter group link so they are all equal
+        _.each(groupTabs.find("a"), function (link) {
+          if ($(link).height() < maxHeight) {
+            $(link).height(maxHeight + "px");
           }
-
-        }, this);
-
-        if( !replaced ){
-          //Add the applied filter to the view
-          this.$(".applied-filters").append(appliedFilter);
+        });
+      },
+
+      /**
+       * Renders the values of the given Filter Model in the current filter model
+       *
+       * @param {Filter} filterModel - The FilterModel to display
+       * @param {object} options - Additional options for this function
+       * @property {boolean} options.displayWithoutChanges - If true, this filter will
+       * display even if the value hasn't been changed
+       */
+      updateAppliedFilters: function (filterModel, options) {
+        //Create an options object if one wasn't sent
+        if (typeof options != "object") {
+          var options = {};
         }
+        this.options = options;
+        var view = this;
 
-      }
-
-      this.toggleAppliedFiltersHeader();
-
-    },
-
-    /**
-    * Creates a single applied filter element and returns it. Filters can
-    *  have multiple values, so one value is passed to this function at a time.
-    * @param {Filter} filterModel - The Filter model that is being added to the display
-    * @param {string|number|Boolean} value - The new value set on the Filter model that is displayed in this applied filter
-    * @returns {jQuery} - The complete applied filter element
-    */
-    createAppliedFilter: function(filterModel, value){
-
-      //Create the filter label
-      var filterLabel = filterModel.get("label"),
-          filterValue = value;
-
-      //If the filter type is Choice, get the choice label which can be different from the value
-      if( filterModel.type == "ChoiceFilter" ){
-        //Find the choice object with the given value
-        var matchingChoice = _.findWhere(filterModel.get("choices"), { "value" : value });
-
-        //Get the label for that choice
-        if(matchingChoice){
-          filterValue = matchingChoice.label;
+        //If the value of this filter has changed, or if the displayWithoutChanges option
+        // was passed, and if the filter is not invisible, then display it
+        if (
+          !filterModel.get("isInvisible") &&
+          ((filterModel.changed && filterModel.changed.values) ||
+            options.displayWithoutChanges)
+        ) {
+          //Get the new values and the previous values
+          var newValues = options.displayWithoutChanges
+              ? filterModel.get("values")
+              : filterModel.changed.values,
+            previousValues = options.displayWithoutChanges
+              ? []
+              : filterModel.previousAttributes().values,
+            //Find the values that were removed
+            removedValues = _.difference(previousValues, newValues),
+            //Find the values that were added
+            addedValues = _.difference(newValues, previousValues);
+
+          //If a filter has been added, display it
+          _.each(
+            addedValues,
+            function (value) {
+              //Add the applied filter to the view
+              this.$(".applied-filters").append(
+                this.createAppliedFilter(filterModel, value),
+              );
+            },
+            this,
+          );
+
+          //Iterate over each removed filter value and remove them
+          _.each(
+            removedValues,
+            function (value) {
+              //Find all applied filter elements with a matching value
+              var matchingFilters = this.$(
+                ".applied-filter[data-value='" + value + "']",
+              );
+
+              //Iterate over each filter element with a matching value
+              _.each(matchingFilters, function (matchingFilter) {
+                //If this is the filter element associated with this filter model, then remove it
+                if ($(matchingFilter).data("model") == filterModel) {
+                  $(matchingFilter).remove();
+                }
+              });
+            },
+            this,
+          );
         }
-      }
-      //Create the filter label for boolean filters
-      else if( filterModel.type == "BooleanFilter" ){
-
-        //If the filter is set to false, remove the applied filter element
-        if( filterModel.get("values")[0] === false ){
 
-          //Iterate over the applied filters
-          _.each(this.$(".applied-filter"), function(appliedFilterEl){
-
-            //If this is the applied filter element for this model,
-            if( $(appliedFilterEl).data("model") == filterModel ){
-              //Remove the applied filter element from the page
-              $(appliedFilterEl).remove();
-            }
+        //Toggle the applied filters header
+        this.toggleAppliedFiltersHeader();
+      },
+
+      /**
+       * Hides or shows the applied filter list title/header, as well as the help
+       * message that lets the user know they can add filters when there are none
+       */
+      toggleAppliedFiltersHeader: function () {
+        //If there is an applied filter
+        if (this.$(".applied-filter").length) {
+          // hide the "add some filters" help text
+          //$(this.parentView.helpTextContainer).css("display", "none");
+          // show the Clear All button
+          this.$(".filters-title").css("display", "block");
+        }
+        //If there are no applied filters
+        else {
+          // show the "add some filters" help text
+          //  $(this.parentView.helpTextContainer).css("display", "block");
+          // hide the Clear All button
+          this.$(".filters-title").css("display", "none");
+        }
+      },
+
+      /**
+       * When a NumericFilter or DateFilter model is changed, update the applied filters in the UI
+       * @param {DateFilter|NumericFilter} filterModel - The model whose values to display
+       * @param {object} [options] - Additional options for this function
+       * @property {boolean} [options.displayWithoutChanges] - If true, this filter will display even if the value hasn't been changed
+       */
+      updateAppliedRangeFilters: function (filterModel, options) {
+        if (!filterModel) {
+          return;
+        }
 
-          }, this);
+        if (typeof options === "undefined" || !options) {
+          var options = {};
+        }
 
-          //Exit the function at this point since there is nothing else to
-          // do for false BooleanFilters
+        //If the Filter is invisible, don't render it
+        if (filterModel.get("isInvisible")) {
           return;
         }
-        else if( filterModel.get("values")[0] === true ){
-          if( !filterLabel ){
-            filterLabel = filterModel.get("fields")[0];
-            filterValue = "";
+
+        //If the minimum and maximum values are set to the default, remove the filter element
+        if (
+          filterModel.get("min") == filterModel.get("rangeMin") &&
+          filterModel.get("max") == filterModel.get("rangeMax")
+        ) {
+          //Find the applied filter element for this filter model
+          _.each(
+            this.$(".applied-filter"),
+            function (filterEl) {
+              if ($(filterEl).data("model") == filterModel) {
+                //Remove the applied filter element
+                $(filterEl).remove();
+              }
+            },
+            this,
+          );
+        }
+        //If the values attribue has changed, or if the displayWithoutChanges attribute was passed
+        else if (
+          (filterModel.changed &&
+            (filterModel.changed.min || filterModel.changed.max)) ||
+          options.displayWithoutChanges
+        ) {
+          //Create the filter label for ranges of numbers
+          var filterValue = filterModel.getReadableValue();
+
+          //Create the applied filter
+          var appliedFilter = this.createAppliedFilter(
+            filterModel,
+            filterValue,
+          );
+
+          //Keep track if this filter is already displayed and needs to be replaced
+          var replaced = false;
+
+          //Check if this filter model already has an applied filter in the UI
+          _.each(
+            this.$(".applied-filter"),
+            function (appliedFilterEl) {
+              //If this applied filter already is displayed, replace it
+              if ($(appliedFilterEl).data("model") == filterModel) {
+                //Replace the applied filter element with the new one
+                $(appliedFilterEl).replaceWith(appliedFilter);
+                replaced = true;
+              }
+            },
+            this,
+          );
+
+          if (!replaced) {
+            //Add the applied filter to the view
+            this.$(".applied-filters").append(appliedFilter);
           }
         }
 
-      }
-      else if( filterModel.type == "ToggleFilter" ){
+        this.toggleAppliedFiltersHeader();
+      },
+
+      /**
+       * Creates a single applied filter element and returns it. Filters can
+       *  have multiple values, so one value is passed to this function at a time.
+       * @param {Filter} filterModel - The Filter model that is being added to the display
+       * @param {string|number|Boolean} value - The new value set on the Filter model that is displayed in this applied filter
+       * @returns {jQuery} - The complete applied filter element
+       */
+      createAppliedFilter: function (filterModel, value) {
+        //Create the filter label
+        var filterLabel = filterModel.get("label"),
+          filterValue = value;
 
-        if( filterModel.get("values")[0] == filterModel.get("trueValue") ){
-          if( filterModel.get("label") && filterModel.get("trueLabel") ){
-            filterValue = filterModel.get("trueLabel");
-          }
-          else if( !filterModel.get("label") && filterModel.get("trueLabel") ){
-            filterLabel = "";
-            filterValue = filterModel.get("trueLabel");
-          }
-          else if( filterModel.get("label") ){
-            filterLabel = "";
-            filterValue = filterModel.get("label");
+        //If the filter type is Choice, get the choice label which can be different from the value
+        if (filterModel.type == "ChoiceFilter") {
+          //Find the choice object with the given value
+          var matchingChoice = _.findWhere(filterModel.get("choices"), {
+            value: value,
+          });
+
+          //Get the label for that choice
+          if (matchingChoice) {
+            filterValue = matchingChoice.label;
           }
         }
-        else{
-          if( filterModel.get("label") && filterModel.get("falseLabel") ){
-            filterValue = filterModel.get("falseLabel");
-          }
-          else if( !filterModel.get("label") && filterModel.get("falseLabel") ){
-            filterLabel = "";
-            filterValue = filterModel.get("falseLabel");
+        //Create the filter label for boolean filters
+        else if (filterModel.type == "BooleanFilter") {
+          //If the filter is set to false, remove the applied filter element
+          if (filterModel.get("values")[0] === false) {
+            //Iterate over the applied filters
+            _.each(
+              this.$(".applied-filter"),
+              function (appliedFilterEl) {
+                //If this is the applied filter element for this model,
+                if ($(appliedFilterEl).data("model") == filterModel) {
+                  //Remove the applied filter element from the page
+                  $(appliedFilterEl).remove();
+                }
+              },
+              this,
+            );
+
+            //Exit the function at this point since there is nothing else to
+            // do for false BooleanFilters
+            return;
+          } else if (filterModel.get("values")[0] === true) {
+            if (!filterLabel) {
+              filterLabel = filterModel.get("fields")[0];
+              filterValue = "";
+            }
           }
-          else if( filterModel.get("label") ){
-            filterLabel = "";
-            filterValue = filterModel.get("label");
+        } else if (filterModel.type == "ToggleFilter") {
+          if (filterModel.get("values")[0] == filterModel.get("trueValue")) {
+            if (filterModel.get("label") && filterModel.get("trueLabel")) {
+              filterValue = filterModel.get("trueLabel");
+            } else if (
+              !filterModel.get("label") &&
+              filterModel.get("trueLabel")
+            ) {
+              filterLabel = "";
+              filterValue = filterModel.get("trueLabel");
+            } else if (filterModel.get("label")) {
+              filterLabel = "";
+              filterValue = filterModel.get("label");
+            }
+          } else {
+            if (filterModel.get("label") && filterModel.get("falseLabel")) {
+              filterValue = filterModel.get("falseLabel");
+            } else if (
+              !filterModel.get("label") &&
+              filterModel.get("falseLabel")
+            ) {
+              filterLabel = "";
+              filterValue = filterModel.get("falseLabel");
+            } else if (filterModel.get("label")) {
+              filterLabel = "";
+              filterValue = filterModel.get("label");
+            }
           }
         }
+        //If this Filter model is a full-text search, don't display a label
+        else if (
+          filterModel.get("fields").length == 1 &&
+          filterModel.get("fields")[0] == "text"
+        ) {
+          filterLabel = "";
+        }
+        //isPartOf filters should just display the label, not the value
+        else if (
+          filterModel.get("fields").length == 1 &&
+          filterModel.get("fields")[0] == "isPartOf"
+        ) {
+          filterValue = "";
+        }
+        //If the filter value is just an asterisk (i.e. `match anything`), just display the label
+        else if (
+          filterModel.get("values").length == 1 &&
+          filterModel.get("values")[0] == "*"
+        ) {
+          filterValue = "";
+        }
+        //Filters with the valueLabels attribute want to display an alternate value from the raw value here
+        else if (filterModel.get("valueLabels")) {
+          filterValue = filterModel.get("valueLabels")[value] || value;
+        } else if (!filterLabel) {
+          filterLabel = filterModel.get("fields")[0];
+        }
 
-      }
-      //If this Filter model is a full-text search, don't display a label
-      else if( filterModel.get("fields").length == 1 && filterModel.get("fields")[0] == "text"){
-        filterLabel = "";
-      }
-      //isPartOf filters should just display the label, not the value
-      else if( filterModel.get("fields").length == 1 && filterModel.get("fields")[0] == "isPartOf" ){
-        filterValue = "";
-      }
-      //If the filter value is just an asterisk (i.e. `match anything`), just display the label
-      else if( filterModel.get("values").length == 1 && filterModel.get("values")[0] == "*" ){
-        filterValue = "";
-      }
-      //Filters with the valueLabels attribute want to display an alternate value from the raw value here
-      else if ( filterModel.get("valueLabels") ) {
-        filterValue = filterModel.get("valueLabels")[value] || value;
-      }
-      else if( !filterLabel ){
-        filterLabel = filterModel.get("fields")[0];
-      }
-
-      //Create the applied filter element
-      var removeIcon    = $(document.createElement("a"))
-                            .addClass("icon icon-remove remove-filter icon-on-right")
-                            .attr("title", "Remove this filter"),
-          appliedFilter = $(document.createElement("li"))
-                            .addClass("applied-filter label")
-                            .append(removeIcon)
-                            .data("model", filterModel)
-                            .attr("data-value", value);
-
-      //Create an element to contain both the label and value
-      var filterLabelEl = $(document.createElement("span")).addClass("label");
-      var filterValueEl = $(document.createElement("span")).addClass("value").text(filterValue);
-
-      var filterTextContainer = $(document.createElement("span"))
-                                 .append(filterLabelEl, filterValueEl);
-
-      //If there is both a label and value, separated them with a colon
-      if( filterLabel && filterValue ){
-        filterLabelEl.text( filterLabel + ": ");
-      }
-      //Otherwise just use the label text only
-      else if( filterLabel ){
-        filterLabelEl.text(filterLabel);
-      }
-
-      //Add the filter text to the filter element
-      appliedFilter.prepend(filterTextContainer);
-
-      // Add a tooltip to the filter
-      if(filterModel.get("description")){
-
-        appliedFilter.tooltip({
-          placement: "right",
-          title: filterModel.get("description"),
-          trigger: "hover",
-          delay: {
-            show: 700
-          }
-        });
-
-      }
-
-      return appliedFilter;
-    },
-
-
-    /**
-    * Adds a custom filter that likely exists outside of the FilterGroups but needs
-    * to be displayed with these other applied fitlers.
-    *
-    * @param {Filter} filterModel - The Filter Model to display
-    */
-    addCustomAppliedFilter: function(filterModel){
-
-      //If the Filter is invisible, don't render it
-      if( filterModel.get("isInvisible") ){
-        return;
-      }
-
-      //If this filter already exists in the applied filter list, exit this function
-      var alreadyExists = _.find( this.$(".applied-filter.custom"), function(appliedFilterEl){
-        return $(appliedFilterEl).data("model") == filterModel;
-      });
-
-      if( alreadyExists ){
-        return;
-      }
-
-      //Create the applied filter element
-      var removeIcon    = $(document.createElement("a"))
-                            .addClass("icon icon-remove remove-filter icon-on-right")
-                            .attr("title", "Remove this filter"),
-          filterText    = $(document.createElement("span")).text(filterModel.get("label")),
+        //Create the applied filter element
+        var removeIcon = $(document.createElement("a"))
+            .addClass("icon icon-remove remove-filter icon-on-right")
+            .attr("title", "Remove this filter"),
           appliedFilter = $(document.createElement("li"))
-                            .addClass("applied-filter label custom")
-                            .append(filterText, removeIcon)
-                            .data("model", filterModel)
-                            .attr("data-value", filterModel.get("values"));
-
-      if( filterModel.type == "SpatialFilter" ){
-        filterText.prepend( $(document.createElement("i"))
-                              .addClass("icon icon-on-left icon-" + filterModel.get("icon")) );
+            .addClass("applied-filter label")
+            .append(removeIcon)
+            .data("model", filterModel)
+            .attr("data-value", value);
+
+        //Create an element to contain both the label and value
+        var filterLabelEl = $(document.createElement("span")).addClass("label");
+        var filterValueEl = $(document.createElement("span"))
+          .addClass("value")
+          .text(filterValue);
+
+        var filterTextContainer = $(document.createElement("span")).append(
+          filterLabelEl,
+          filterValueEl,
+        );
+
+        //If there is both a label and value, separated them with a colon
+        if (filterLabel && filterValue) {
+          filterLabelEl.text(filterLabel + ": ");
+        }
+        //Otherwise just use the label text only
+        else if (filterLabel) {
+          filterLabelEl.text(filterLabel);
+        }
 
-      }
+        //Add the filter text to the filter element
+        appliedFilter.prepend(filterTextContainer);
 
-      //Add the applied filter to the view
-      this.$(".applied-filters").append(appliedFilter);
+        // Add a tooltip to the filter
+        if (filterModel.get("description")) {
+          appliedFilter.tooltip({
+            placement: "right",
+            title: filterModel.get("description"),
+            trigger: "hover",
+            delay: {
+              show: 700,
+            },
+          });
+        }
 
-      //Display the filters title
-      this.toggleAppliedFiltersHeader();
+        return appliedFilter;
+      },
+
+      /**
+       * Adds a custom filter that likely exists outside of the FilterGroups but needs
+       * to be displayed with these other applied fitlers.
+       *
+       * @param {Filter} filterModel - The Filter Model to display
+       */
+      addCustomAppliedFilter: function (filterModel) {
+        //If the Filter is invisible, don't render it
+        if (filterModel.get("isInvisible")) {
+          return;
+        }
 
-    },
+        //If this filter already exists in the applied filter list, exit this function
+        var alreadyExists = _.find(
+          this.$(".applied-filter.custom"),
+          function (appliedFilterEl) {
+            return $(appliedFilterEl).data("model") == filterModel;
+          },
+        );
 
-    /**
-    * Removes the custom applied filter from the UI.
-    *
-    * @param {Filter} filterModel - The Filter Model to display
-    */
-    removeCustomAppliedFilter: function(filterModel){
+        if (alreadyExists) {
+          return;
+        }
 
-      _.each(this.$(".custom.applied-filter"), function(appliedFilterEl){
-        if( $(appliedFilterEl).data("model") == filterModel ){
-          $(appliedFilterEl).remove();
-          this.trigger("customAppliedFilterRemoved", filterModel);
+        //Create the applied filter element
+        var removeIcon = $(document.createElement("a"))
+            .addClass("icon icon-remove remove-filter icon-on-right")
+            .attr("title", "Remove this filter"),
+          filterText = $(document.createElement("span")).text(
+            filterModel.get("label"),
+          ),
+          appliedFilter = $(document.createElement("li"))
+            .addClass("applied-filter label custom")
+            .append(filterText, removeIcon)
+            .data("model", filterModel)
+            .attr("data-value", filterModel.get("values"));
+
+        if (filterModel.type == "SpatialFilter") {
+          filterText.prepend(
+            $(document.createElement("i")).addClass(
+              "icon icon-on-left icon-" + filterModel.get("icon"),
+            ),
+          );
         }
-      }, this);
 
-      //Hide the filters title
-      this.toggleAppliedFiltersHeader();
+        //Add the applied filter to the view
+        this.$(".applied-filters").append(appliedFilter);
+
+        //Display the filters title
+        this.toggleAppliedFiltersHeader();
+      },
+
+      /**
+       * Removes the custom applied filter from the UI.
+       *
+       * @param {Filter} filterModel - The Filter Model to display
+       */
+      removeCustomAppliedFilter: function (filterModel) {
+        _.each(
+          this.$(".custom.applied-filter"),
+          function (appliedFilterEl) {
+            if ($(appliedFilterEl).data("model") == filterModel) {
+              $(appliedFilterEl).remove();
+              this.trigger("customAppliedFilterRemoved", filterModel);
+            }
+          },
+          this,
+        );
 
-    },
+        //Hide the filters title
+        this.toggleAppliedFiltersHeader();
+      },
 
-    /**
+      /**
     * When a remove button is clicked, get the filter model associated with it
     /* and remove the filter from the filter group
     *
     * @param {Event} - The DOM Event that occured on the filter remove icon
     */
-    handleRemove: function(e){
-
-      // Ensure tooltips are removed
-      try{
-        if(e.delegateTarget){
-          $(e.delegateTarget).find(".tooltip").remove();
-        }
-      }
-      catch(e) {
-        console.log("Could not remove tooltip from filter label, error message: " + e);
-      };
-
-      //Get the applied filter element and the filter model associated with it
-      var appliedFilterEl = $(e.target).parents(".applied-filter"),
-          filterModel =  appliedFilterEl.data("model");
-
-      if( appliedFilterEl.is(".custom") ){
-        this.removeCustomAppliedFilter(filterModel);
-      }
-      else{
-        //Remove the filter from the filter group model
-        this.removeFilter(filterModel, appliedFilterEl);
-      }
-
-    },
-
-    /**
-    * Remove the filter from the UI and the Search collection
-    * @param {Filter} filterModel The Filter to remove from the Filters collection
-    * @param {Element} appliedFilterEl The DOM Element for the applied filter on the page
-    * @param {object} options Additional options for this function
-    * @param {boolean} options.removeSilently If true, the Filter model will be removed siltently from the Filters collection.
-    *  This is useful when removing multiple Filters at once, and triggering a remove/change/reset event after all have
-    *  been removed.
-    */
-    removeFilter: function(filterModel, appliedFilterEl, options){
-
-      var removeSilently = false;
-
-      //Create an options object if one wasn't sent
-      if( typeof options != "object" ){
-        var options = {};
-      }
-      this.options = options;
-      var view = this;
-
-      //Parse all the additional options for this function
-      if( typeof options == "object" ){
-        removeSilently = typeof options.removeSilently != "undefined"? options.removeSilently : false;
-      }
-
-
-      if( filterModel ){
-        //NumericFilters and DateFilters get the min and max values reset
-        if( filterModel.type == "NumericFilter" || filterModel.type == "DateFilter" ){
-
-          //Set the min and max values
-          filterModel.set({
-            min: filterModel.get("rangeMin"),
-            max: filterModel.get("rangeMax"),
-            values: filterModel.defaults().values
-          });
-
-          if( !removeSilently ){
-            //Trigger the reset event
-            filterModel.trigger("rangeReset");
+      handleRemove: function (e) {
+        // Ensure tooltips are removed
+        try {
+          if (e.delegateTarget) {
+            $(e.delegateTarget).find(".tooltip").remove();
           }
-
+        } catch (e) {
+          console.log(
+            "Could not remove tooltip from filter label, error message: " + e,
+          );
         }
-        //For all other filter types
-        else{
-          //Get the current value
-          var modelValues = filterModel.get("values"),
-              thisValue   = $(appliedFilterEl).data("value");
-
-          //Numbers that are set on the element `data` are stored as type `number`, but when `number`s are
-          // set on Backbone models, they are converted to `string`s. So we need to check for this use case.
-          if( typeof thisValue == "number" ){
-            //Convert the number to a string
-            thisValue = thisValue.toString();
-          }
-
-          //Remove the value that was in this applied filter
-          var newValues = _.without(modelValues, thisValue),
-              setOptions = {};
 
-          if( removeSilently ){
-            setOptions.silent = true;
-          }
+        //Get the applied filter element and the filter model associated with it
+        var appliedFilterEl = $(e.target).parents(".applied-filter"),
+          filterModel = appliedFilterEl.data("model");
 
-          //Updates the values on the model
-          filterModel.set("values", newValues, setOptions);
+        if (appliedFilterEl.is(".custom")) {
+          this.removeCustomAppliedFilter(filterModel);
+        } else {
+          //Remove the filter from the filter group model
+          this.removeFilter(filterModel, appliedFilterEl);
         }
-
-      }
-
-    },
-
-    /**
-    * Gets all the applied filters in this view and their associated filter models
-    *   and removes them.
-    */
-    removeAllFilters: function(){
-
-      let removedFilters = [];
-
-      //Iterate over each applied filter in the view
-      _.each( this.$(".applied-filter"), function(appliedFilterEl){
-
-        var $appliedFilterEl = $(appliedFilterEl);
-
-        removedFilters.push($appliedFilterEl.data("model"));
-
-        if( $appliedFilterEl.is(".custom") ){
-          this.removeCustomAppliedFilter( $appliedFilterEl.data("model") );
+      },
+
+      /**
+       * Remove the filter from the UI and the Search collection
+       * @param {Filter} filterModel The Filter to remove from the Filters collection
+       * @param {Element} appliedFilterEl The DOM Element for the applied filter on the page
+       * @param {object} options Additional options for this function
+       * @param {boolean} options.removeSilently If true, the Filter model will be removed siltently from the Filters collection.
+       *  This is useful when removing multiple Filters at once, and triggering a remove/change/reset event after all have
+       *  been removed.
+       */
+      removeFilter: function (filterModel, appliedFilterEl, options) {
+        var removeSilently = false;
+
+        //Create an options object if one wasn't sent
+        if (typeof options != "object") {
+          var options = {};
         }
-        else{
-
-          //Remove the filter from the fitler group. Do this silently since we will trigger a "reset" event later
-          this.removeFilter( $appliedFilterEl.data("model"), appliedFilterEl, { removeSilently: true } );
-
+        this.options = options;
+        var view = this;
+
+        //Parse all the additional options for this function
+        if (typeof options == "object") {
+          removeSilently =
+            typeof options.removeSilently != "undefined"
+              ? options.removeSilently
+              : false;
         }
 
-        //Remove the applied filter element from the page
-        $appliedFilterEl.remove();
-
-      }, this);
-
-      //Trigger the reset event on the Filters collection
-      this.filters.trigger("reset");
-
-      //Trigger the remove event on all the models now that they are all removed
-      _.invoke(removedFilters, "trigger", "remove");
-
-      //Toggle the applied filters header
-      this.toggleAppliedFiltersHeader();
+        if (filterModel) {
+          //NumericFilters and DateFilters get the min and max values reset
+          if (
+            filterModel.type == "NumericFilter" ||
+            filterModel.type == "DateFilter"
+          ) {
+            //Set the min and max values
+            filterModel.set({
+              min: filterModel.get("rangeMin"),
+              max: filterModel.get("rangeMax"),
+              values: filterModel.defaults().values,
+            });
+
+            if (!removeSilently) {
+              //Trigger the reset event
+              filterModel.trigger("rangeReset");
+            }
+          }
+          //For all other filter types
+          else {
+            //Get the current value
+            var modelValues = filterModel.get("values"),
+              thisValue = $(appliedFilterEl).data("value");
+
+            //Numbers that are set on the element `data` are stored as type `number`, but when `number`s are
+            // set on Backbone models, they are converted to `string`s. So we need to check for this use case.
+            if (typeof thisValue == "number") {
+              //Convert the number to a string
+              thisValue = thisValue.toString();
+            }
 
-    },
+            //Remove the value that was in this applied filter
+            var newValues = _.without(modelValues, thisValue),
+              setOptions = {};
 
-    /**
-    * Remove the applied filter element for the given model
-    * This only removed the element from the page, it doesn't update the model at all or
-    * trigger any events.
-    * @param {Filter} - The Filter model whose elements will be deleted
-    */
-    removeAppliedFilterElByModel: function(filterModel){
+            if (removeSilently) {
+              setOptions.silent = true;
+            }
 
-      //Iterate over each applied filter element and find the matching filters
-      this.$(".applied-filter").each(function(i, el){
-        if( $(el).data("model") == filterModel ){
-          //Remove the element from the page
-          $(el).remove();
+            //Updates the values on the model
+            filterModel.set("values", newValues, setOptions);
+          }
         }
-      });
-
-      //Toggle the applied filters header
-      this.toggleAppliedFiltersHeader();
+      },
+
+      /**
+       * Gets all the applied filters in this view and their associated filter models
+       *   and removes them.
+       */
+      removeAllFilters: function () {
+        let removedFilters = [];
+
+        //Iterate over each applied filter in the view
+        _.each(
+          this.$(".applied-filter"),
+          function (appliedFilterEl) {
+            var $appliedFilterEl = $(appliedFilterEl);
+
+            removedFilters.push($appliedFilterEl.data("model"));
+
+            if ($appliedFilterEl.is(".custom")) {
+              this.removeCustomAppliedFilter($appliedFilterEl.data("model"));
+            } else {
+              //Remove the filter from the fitler group. Do this silently since we will trigger a "reset" event later
+              this.removeFilter(
+                $appliedFilterEl.data("model"),
+                appliedFilterEl,
+                { removeSilently: true },
+              );
+            }
 
-    }
+            //Remove the applied filter element from the page
+            $appliedFilterEl.remove();
+          },
+          this,
+        );
+
+        //Trigger the reset event on the Filters collection
+        this.filters.trigger("reset");
+
+        //Trigger the remove event on all the models now that they are all removed
+        _.invoke(removedFilters, "trigger", "remove");
+
+        //Toggle the applied filters header
+        this.toggleAppliedFiltersHeader();
+      },
+
+      /**
+       * Remove the applied filter element for the given model
+       * This only removed the element from the page, it doesn't update the model at all or
+       * trigger any events.
+       * @param {Filter} - The Filter model whose elements will be deleted
+       */
+      removeAppliedFilterElByModel: function (filterModel) {
+        //Iterate over each applied filter element and find the matching filters
+        this.$(".applied-filter").each(function (i, el) {
+          if ($(el).data("model") == filterModel) {
+            //Remove the element from the page
+            $(el).remove();
+          }
+        });
 
-  });
+        //Toggle the applied filters header
+        this.toggleAppliedFiltersHeader();
+      },
+    },
+  );
   return FilterGroupsView;
 });
 
diff --git a/docs/docs/src_js_views_filters_FilterView.js.html b/docs/docs/src_js_views_filters_FilterView.js.html index f4b398494..5facee0a4 100644 --- a/docs/docs/src_js_views_filters_FilterView.js.html +++ b/docs/docs/src_js_views_filters_FilterView.js.html @@ -44,348 +44,354 @@

Source: src/js/views/filters/FilterView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone',
-        'models/filters/Filter',
-        'text!templates/filters/filter.html',
-        'text!templates/filters/filterLabel.html'],
-  function($, _, Backbone, Filter, Template, LabelTemplate) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/filters/Filter",
+  "text!templates/filters/filter.html",
+  "text!templates/filters/filterLabel.html",
+], function ($, _, Backbone, Filter, Template, LabelTemplate) {
+  "use strict";
 
   /**
-  * @class FilterView
-  * @classdesc Render a view of a single FilterModel
-  * @classcategory Views/Filters
-  * @extends Backbone.View
-  */
+   * @class FilterView
+   * @classdesc Render a view of a single FilterModel
+   * @classcategory Views/Filters
+   * @extends Backbone.View
+   */
   var FilterView = Backbone.View.extend(
-    /** @lends FilterView.prototype */{
-
-    /**
-    * A Filter model to be rendered in this view
-    * @type {Filter} */
-    model: null,
-
-    /**
-     * The Filter model that this View renders. This is used to create a new
-     * instance of the model if one is not provided to the view.
-     * @type {Backbone.Model}
-     * @since 2.17.0
-     */
-    modelClass: Filter,
-
-    tagName: "div",
-
-    className: "filter",
-
-    /**
-    * Reference to template for this view. HTML files are converted to Underscore.js
-    * templates
-    * @type {Underscore.Template}
-    */
-    template: _.template(Template),
-
-    /**
-    * The template that renders the icon and label of a filter
-    * @type {Underscore.Template}
-    * @since 2.17.0
-    */
-    labelTemplate: _.template(LabelTemplate),
-
-    /**
-     * One of "normal", "edit", or "uiBuilder". "normal" renders a regular filter used to
-     * update a search model in a DataCatalogViewWithFilters. "edit" creates a filter that
-     * cannot update a search model, but which has an "EDIT" button that opens a modal
-     * with an interface for editing the filter model's properties (e.g. fields, model
-     * type, etc.). "uiBuilder" is the view of the filter within this editing modal; it
-     * has inputs that are overlaid above the filter elements where a user can edit the
-     * placeholder text, label, etc. in a WYSIWYG fashion.
-     * @type {string}
-     * @since 2.17.0
-     */
-    mode: "normal",
-
-    /**
-     * The class to add to the filter when it is in "uiBuilder" mode
-     * @type {string}
-     * @since 2.17.0
-     */
+    /** @lends FilterView.prototype */ {
+      /**
+       * A Filter model to be rendered in this view
+       * @type {Filter} */
+      model: null,
+
+      /**
+       * The Filter model that this View renders. This is used to create a new
+       * instance of the model if one is not provided to the view.
+       * @type {Backbone.Model}
+       * @since 2.17.0
+       */
+      modelClass: Filter,
+
+      tagName: "div",
+
+      className: "filter",
+
+      /**
+       * Reference to template for this view. HTML files are converted to Underscore.js
+       * templates
+       * @type {Underscore.Template}
+       */
+      template: _.template(Template),
+
+      /**
+       * The template that renders the icon and label of a filter
+       * @type {Underscore.Template}
+       * @since 2.17.0
+       */
+      labelTemplate: _.template(LabelTemplate),
+
+      /**
+       * One of "normal", "edit", or "uiBuilder". "normal" renders a regular filter used to
+       * update a search model in a DataCatalogViewWithFilters. "edit" creates a filter that
+       * cannot update a search model, but which has an "EDIT" button that opens a modal
+       * with an interface for editing the filter model's properties (e.g. fields, model
+       * type, etc.). "uiBuilder" is the view of the filter within this editing modal; it
+       * has inputs that are overlaid above the filter elements where a user can edit the
+       * placeholder text, label, etc. in a WYSIWYG fashion.
+       * @type {string}
+       * @since 2.17.0
+       */
+      mode: "normal",
+
+      /**
+       * The class to add to the filter when it is in "uiBuilder" mode
+       * @type {string}
+       * @since 2.17.0
+       */
       uiBuilderClass: "ui-build",
-    
-    /**
-     * Whether the filter is collapsible. If true, the filter will have a button that
-     * toggles the collapsed state.
-     * @type {boolean}
-     * @since 2.25.0
-     */
-    collapsible: false,
-
-    /**
-     * The class to add to the filter when it is collapsed.
-     * @type {string}
-     * @since 2.25.0
-     * @default "collapsed"
-     */
-    collapsedClass: "collapsed",
-
-    /**
-     * The class used for the button that toggles the collapsed state of the filter.
-     * @type {string}
-     * @since 2.25.0
-     * @default "collapse-toggle"
-     */
-    collapseToggleClass: "collapse-toggle",
-    
-    /**
-     * The current state of the filter, if it is {@link FilterView#collapsible}.
-     * Whatever this value is set to at initialization, will be how the filter is
-     * initially rendered.
-     * @type {boolean}
-     * @since 2.25.0
-     * @default true
-     */
-    collapsed: true,
-
-    /**
-     * The class used for input elements where the user can change UI attributes when this
-     * view is in "uiBuilder" mode. For example, the input for the placeholder text should
-     * have this class. Elements with this class also need to have a data-category
-     * attribute with the name of the model attribute they correspond to.
-     * @type {string}
-     * @since 2.17.0
-     */
-    uiInputClass: "ui-build-input",
-
-    /**
-     * A function that creates and returns the Backbone events object.
-     * @return {Object} Returns a Backbone events object
-     */
-    events: function(){
-      try {
-        var events = {
-          "click .btn": "handleChange",
-          "keydown input": "handleTyping"
-        }
-        events["change ." + this.uiInputClass] = "updateUIAttribute";
-        events[`click .${this.collapseToggleClass}`] = "toggleCollapse";
-        return events
-      }
-      catch (error) {
-        console.log( 'There was an error setting the events object in a FilterView' +
-          ' Error details: ' + error );
-      }
-      },
 
-    /**
-     * Function executed whenever a new FilterView is created.
-     * @param {Object} [options] - A literal object of options to set on this View
-     */
-      initialize: function (options) {
+      /**
+       * Whether the filter is collapsible. If true, the filter will have a button that
+       * toggles the collapsed state.
+       * @type {boolean}
+       * @since 2.25.0
+       */
+      collapsible: false,
 
-      try {
-        if (!options || typeof options != "object") {
-          var options = {};
-        }
+      /**
+       * The class to add to the filter when it is collapsed.
+       * @type {string}
+       * @since 2.25.0
+       * @default "collapsed"
+       */
+      collapsedClass: "collapsed",
+
+      /**
+       * The class used for the button that toggles the collapsed state of the filter.
+       * @type {string}
+       * @since 2.25.0
+       * @default "collapse-toggle"
+       */
+      collapseToggleClass: "collapse-toggle",
 
-        this.editorView = options.editorView || null;
+      /**
+       * The current state of the filter, if it is {@link FilterView#collapsible}.
+       * Whatever this value is set to at initialization, will be how the filter is
+       * initially rendered.
+       * @type {boolean}
+       * @since 2.25.0
+       * @default true
+       */
+      collapsed: true,
 
-        if (options.mode && ["edit", "uiBuilder", "normal"].includes(options.mode)) {
-          this.mode = options.mode
-        }
+      /**
+       * The class used for input elements where the user can change UI attributes when this
+       * view is in "uiBuilder" mode. For example, the input for the placeholder text should
+       * have this class. Elements with this class also need to have a data-category
+       * attribute with the name of the model attribute they correspond to.
+       * @type {string}
+       * @since 2.17.0
+       */
+      uiInputClass: "ui-build-input",
 
-        // When this view is being rendered in an editable mode (e.g. in the custom search
-        // filter editor), then overwrite the functions that update the search model. This
-        // way the user can interact with the filter without causing the
-        // dataCatalogViewWithFilters to update the search results. For simplicity, and
-        // because extended Filter Views call this function, update functions from other
-        // types of Filter views are included here.
-        if (["edit", "uiBuilder"].includes(this.mode)) {
-          var functionsToOverwrite = [
-            "updateModel", "handleChange", "handleTyping",
-            "updateChoices", "updateToggle", "updateYearRange"
-          ]
-          functionsToOverwrite.forEach(function (fnName) {
-            if (typeof this[fnName] === "function") {
-              this[fnName] = function () { return }
-            }
-          }, this)
+      /**
+       * A function that creates and returns the Backbone events object.
+       * @return {Object} Returns a Backbone events object
+       */
+      events: function () {
+        try {
+          var events = {
+            "click .btn": "handleChange",
+            "keydown input": "handleTyping",
+          };
+          events["change ." + this.uiInputClass] = "updateUIAttribute";
+          events[`click .${this.collapseToggleClass}`] = "toggleCollapse";
+          return events;
+        } catch (error) {
+          console.log(
+            "There was an error setting the events object in a FilterView" +
+              " Error details: " +
+              error,
+          );
         }
+      },
 
-        this.model = options.model || new this.modelClass();
+      /**
+       * Function executed whenever a new FilterView is created.
+       * @param {Object} [options] - A literal object of options to set on this View
+       */
+      initialize: function (options) {
+        try {
+          if (!options || typeof options != "object") {
+            var options = {};
+          }
 
-        if (options.collapsible && typeof options.collapsible === "boolean") {
-          this.collapsible = options.collapsible;
-        }
+          this.editorView = options.editorView || null;
 
-      }
+          if (
+            options.mode &&
+            ["edit", "uiBuilder", "normal"].includes(options.mode)
+          ) {
+            this.mode = options.mode;
+          }
 
-      catch (error) {
-        console.log( 'There was an error initializing a FilterView' +
-          ' Error details: ' + error );
-      }
+          // When this view is being rendered in an editable mode (e.g. in the custom search
+          // filter editor), then overwrite the functions that update the search model. This
+          // way the user can interact with the filter without causing the
+          // dataCatalogViewWithFilters to update the search results. For simplicity, and
+          // because extended Filter Views call this function, update functions from other
+          // types of Filter views are included here.
+          if (["edit", "uiBuilder"].includes(this.mode)) {
+            var functionsToOverwrite = [
+              "updateModel",
+              "handleChange",
+              "handleTyping",
+              "updateChoices",
+              "updateToggle",
+              "updateYearRange",
+            ];
+            functionsToOverwrite.forEach(function (fnName) {
+              if (typeof this[fnName] === "function") {
+                this[fnName] = function () {
+                  return;
+                };
+              }
+            }, this);
+          }
 
-    },
+          this.model = options.model || new this.modelClass();
 
-    /**
-     * Render an instance of a Filter View. All of the extended Filter Views also call
-     * this render function.
-     * @param {Object} templateVars - The variables to use in the HTML template. If not
-     * provided, defaults to the model in JSON
-     */
-    render: function (templateVars) {
-      try {
-        var view = this;
-      
-        if(!templateVars){
-          var templateVars = this.model.toJSON()
+          if (options.collapsible && typeof options.collapsible === "boolean") {
+            this.collapsible = options.collapsible;
+          }
+        } catch (error) {
+          console.log(
+            "There was an error initializing a FilterView" +
+              " Error details: " +
+              error,
+          );
         }
+      },
 
-        // Pass the mode (e.g. "edit", "uiBuilder") to the template, as well
-        // as the variables related to collapsibility.
-        const viewVars = {
-          mode: this.mode,
-          collapsible: this.collapsible,
-          collapseToggleClass: this.collapseToggleClass
-        }
-        templateVars = _.extend(templateVars, viewVars)
-
-        // Render the filter HTML (without label or icon)
-        this.$el.html( this.template( templateVars ) );
-        // Add the filter label & icon (common between most filters)
-        this.$el.prepend( this.labelTemplate( templateVars ) );
-
-        // a FilterEditorView adds an "EDIT" button, which opens a modal allowing the user
-        // to change the UI options of the filter - e.g., label, icon, placeholder text,
-        // etc.
-        if(this.mode === "edit"){
-          require(['views/filters/FilterEditorView'], function(FilterEditor){
-            var filterEditor = new FilterEditor({
-              model: view.model,
-              editorView: view.editorView
+      /**
+       * Render an instance of a Filter View. All of the extended Filter Views also call
+       * this render function.
+       * @param {Object} templateVars - The variables to use in the HTML template. If not
+       * provided, defaults to the model in JSON
+       */
+      render: function (templateVars) {
+        try {
+          var view = this;
+
+          if (!templateVars) {
+            var templateVars = this.model.toJSON();
+          }
+
+          // Pass the mode (e.g. "edit", "uiBuilder") to the template, as well
+          // as the variables related to collapsibility.
+          const viewVars = {
+            mode: this.mode,
+            collapsible: this.collapsible,
+            collapseToggleClass: this.collapseToggleClass,
+          };
+          templateVars = _.extend(templateVars, viewVars);
+
+          // Render the filter HTML (without label or icon)
+          this.$el.html(this.template(templateVars));
+          // Add the filter label & icon (common between most filters)
+          this.$el.prepend(this.labelTemplate(templateVars));
+
+          // a FilterEditorView adds an "EDIT" button, which opens a modal allowing the user
+          // to change the UI options of the filter - e.g., label, icon, placeholder text,
+          // etc.
+          if (this.mode === "edit") {
+            require(["views/filters/FilterEditorView"], function (
+              FilterEditor,
+            ) {
+              var filterEditor = new FilterEditor({
+                model: view.model,
+                editorView: view.editorView,
+              });
+              filterEditor.render();
+              view.$el.prepend(filterEditor.el);
             });
-            filterEditor.render();
-            view.$el.prepend(filterEditor.el);
-          });
-        }
-        if(this.mode === "uiBuilder"){
-          this.$el.addClass(this.uiBuilderClass);
-        }
-        // Don't show the editor footer with save button when a user types text into
-        // a filter in edit or build mode.
-        if(["edit", "uiBuilder"].includes(this.mode)){
-          this.$el.find("input").addClass("ignore-changes")
-        }
+          }
+          if (this.mode === "uiBuilder") {
+            this.$el.addClass(this.uiBuilderClass);
+          }
+          // Don't show the editor footer with save button when a user types text into
+          // a filter in edit or build mode.
+          if (["edit", "uiBuilder"].includes(this.mode)) {
+            this.$el.find("input").addClass("ignore-changes");
+          }
 
-        // If the filter is collapsible, set the initial collapsed state
-        if(this.collapsible && typeof this.collapsed === "boolean"){
-          this.toggleCollapse(this.collapsed)
+          // If the filter is collapsible, set the initial collapsed state
+          if (this.collapsible && typeof this.collapsed === "boolean") {
+            this.toggleCollapse(this.collapsed);
+          }
+        } catch (error) {
+          console.log(
+            "There was an error rendering a FilterView" +
+              " Error details: " +
+              error,
+          );
         }
-        
-      }
-      catch (error) {
-        console.log( 'There was an error rendering a FilterView' +
-          ' Error details: ' + error );
-      }
-    },
+      },
 
-    /**
-    * When the user presses Enter in the input element, update the view and model
-    *
-    * @param {Event} - The DOM Event that occurred on the filter view input element
-    */
+      /**
+       * When the user presses Enter in the input element, update the view and model
+       *
+       * @param {Event} - The DOM Event that occurred on the filter view input element
+       */
       handleTyping: function (e) {
-      
         if (["edit", "uiBuilder"].includes(this.mode)) {
-          return
+          return;
         }
 
-        if (e.key == "Enter"){
+        if (e.key == "Enter") {
           this.handleChange();
           return;
-        }
-        else{
+        } else {
           /** @todo Get search suggestions when the user is typing. See {@link DataCatalogView#getAutoCompletes }*/
-          
         }
+      },
 
-    },
-
-    /**
-    * Updates the view when the filter input is updated
-    *
-    * @param {Event} - The DOM Event that occurred on the filter view input element
-    */
-    handleChange: function () {
-
-      if (["edit", "uiBuilder"].includes(this.mode)) {
-        return
-      }
-
-      this.updateModel();
-
-      //Clear the value of the text input
-      this.$("input").val("");
+      /**
+       * Updates the view when the filter input is updated
+       *
+       * @param {Event} - The DOM Event that occurred on the filter view input element
+       */
+      handleChange: function () {
+        if (["edit", "uiBuilder"].includes(this.mode)) {
+          return;
+        }
 
-    },
+        this.updateModel();
 
-    /**
-    * Updates the value set on the Filter Model associated with this view.
-    * The filter value is grabbed from the input element in this view.
-    */
-    updateModel: function () {
-      
-      if (["edit", "uiBuilder"].includes(this.mode)) {
-        return
-      }
+        //Clear the value of the text input
+        this.$("input").val("");
+      },
 
-      //Get the new value from the text input
-      var newValue = this.$("input").val();
+      /**
+       * Updates the value set on the Filter Model associated with this view.
+       * The filter value is grabbed from the input element in this view.
+       */
+      updateModel: function () {
+        if (["edit", "uiBuilder"].includes(this.mode)) {
+          return;
+        }
 
-      if( newValue == "" )
-        return;
+        //Get the new value from the text input
+        var newValue = this.$("input").val();
 
-      //Get the current values array from the model
-      var currentValue = this.model.get("values");
+        if (newValue == "") return;
 
-      //Create a copy of the array
-      var newValuesArray = _.flatten(new Array(currentValue, newValue));
+        //Get the current values array from the model
+        var currentValue = this.model.get("values");
 
-      //Trigger the change event manually since it is an array
-      this.model.set("values", newValuesArray);
+        //Create a copy of the array
+        var newValuesArray = _.flatten(new Array(currentValue, newValue));
 
-    },
+        //Trigger the change event manually since it is an array
+        this.model.set("values", newValuesArray);
+      },
 
-    /**
-     * Updates the corresponding model attribute when an input for one of the UI options
-     * changes (in "uiBuilder" mode).
-     * @param {Object} e The change event
-     * @since 2.17.0
-     */
-    updateUIAttribute: function(e){
-      try {
-        if (this.mode != "uiBuilder") {
-          return
-        }
-        var inputEl = e.target;
-        if (!inputEl) {
-          return
-        }
-        if (!inputEl.dataset || !inputEl.dataset.category) {
-          return
-        }
-        var modelAttribute = inputEl.dataset.category,
-          newValue = inputEl.value;
-        if (inputEl.type === "number") {
-          newValue = parseInt(newValue)
+      /**
+       * Updates the corresponding model attribute when an input for one of the UI options
+       * changes (in "uiBuilder" mode).
+       * @param {Object} e The change event
+       * @since 2.17.0
+       */
+      updateUIAttribute: function (e) {
+        try {
+          if (this.mode != "uiBuilder") {
+            return;
+          }
+          var inputEl = e.target;
+          if (!inputEl) {
+            return;
+          }
+          if (!inputEl.dataset || !inputEl.dataset.category) {
+            return;
+          }
+          var modelAttribute = inputEl.dataset.category,
+            newValue = inputEl.value;
+          if (inputEl.type === "number") {
+            newValue = parseInt(newValue);
+          }
+          this.model.set(modelAttribute, newValue);
+        } catch (error) {
+          console.log(
+            "There was an error updating a UI attribute in a FilterView" +
+              " Error details: " +
+              error,
+          );
         }
-        this.model.set(modelAttribute, newValue)
-      }
-      catch (error) {
-        console.log( 'There was an error updating a UI attribute in a FilterView' +
-          ' Error details: ' + error );
-      }
       },
-    
+
       /**
        * Show validation errors. This is used for filters that are in "UIBuilder" mode.
        * @param {Object} errors The error messages associated with each attribute that has
@@ -397,23 +403,30 @@ 

Source: src/js/views/filters/FilterView.js

var uiInputClass = this.uiInputClass; for (const [category, message] of Object.entries(errors)) { - - const input = view.el.querySelector("." + uiInputClass + "[data-category='" + category + "']"); - const messageContainer = view.el.querySelector(".notification[data-category='" + category + "']"); + const input = view.el.querySelector( + "." + uiInputClass + "[data-category='" + category + "']", + ); + const messageContainer = view.el.querySelector( + ".notification[data-category='" + category + "']", + ); view.showInputError(input, messageContainer, message); if (input) { - input.addEventListener('input', function () { - view.hideInputError(input, messageContainer) - }, { once: true }) + input.addEventListener( + "input", + function () { + view.hideInputError(input, messageContainer); + }, + { once: true }, + ); } } - } - catch (error) { + } catch (error) { console.log( - 'There was an error showing validation errors in a FilterView' + - '. Error details: ' + error + "There was an error showing validation errors in a FilterView" + + ". Error details: " + + error, ); } }, @@ -435,11 +448,11 @@

Source: src/js/views/filters/FilterView.js

if (input) { input.classList.add("error"); } - } - catch (error) { + } catch (error) { console.log( - 'Failed to show an error message for an input in a FilterView' + - '. Error details: ' + error + "Failed to show an error message for an input in a FilterView" + + ". Error details: " + + error, ); } }, @@ -460,11 +473,11 @@

Source: src/js/views/filters/FilterView.js

if (input) { input.classList.remove("error"); } - } - catch (error) { + } catch (error) { console.log( - 'Failed to hide the error message for an input in a FilterView' + - '. Error details: ' + error + "Failed to hide the error message for an input in a FilterView" + + ". Error details: " + + error, ); } }, @@ -482,24 +495,21 @@

Source: src/js/views/filters/FilterView.js

// If collapse is a boolean, then set the collapsed state to that value. // Otherwise, set it to the opposite of whichever state is currently set. if (typeof collapse !== "boolean") { - collapse = !this.collapsed + collapse = !this.collapsed; } if (collapse) { - this.el.classList.add(this.collapsedClass) - this.collapsed = true + this.el.classList.add(this.collapsedClass); + this.collapsed = true; } else { - this.el.classList.remove(this.collapsedClass) - this.collapsed = false + this.el.classList.remove(this.collapsedClass); + this.collapsed = false; } - } - catch (e) { + } catch (e) { console.log("Could not un/collapse filter.", e); } }, - - - - }); + }, + ); return FilterView; });
diff --git a/docs/docs/src_js_views_filters_NumericFilterView.js.html b/docs/docs/src_js_views_filters_NumericFilterView.js.html index e078970db..ccaa9e358 100644 --- a/docs/docs/src_js_views_filters_NumericFilterView.js.html +++ b/docs/docs/src_js_views_filters_NumericFilterView.js.html @@ -44,206 +44,198 @@

Source: src/js/views/filters/NumericFilterView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone',
-        'models/filters/NumericFilter',
-        'views/filters/FilterView',
-        'text!templates/filters/numericFilter.html'],
-  function($, _, Backbone, NumericFilter, FilterView, Template) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/filters/NumericFilter",
+  "views/filters/FilterView",
+  "text!templates/filters/numericFilter.html",
+], function ($, _, Backbone, NumericFilter, FilterView, Template) {
+  "use strict";
 
   /**
-  * @class NumericFilterView
-  * @classdesc Render a view of a single NumericFilter model
-  * @classcategory Views/Filters
-  * @extends FilterView
-  */
+   * @class NumericFilterView
+   * @classdesc Render a view of a single NumericFilter model
+   * @classcategory Views/Filters
+   * @extends FilterView
+   */
   var NumericFilterView = FilterView.extend(
-    /** @lends NumericFilterView.prototype */{
-
-    /**
-    *  A NumericFilter model to be rendered in this view
-    * @type {NumericFilter} */
-    model: null,
-
-    className: "filter numeric",
-
-    template: _.template(Template),
-
-    events: {
-      "change input.range" : "updateRange",
-      "change input.single-number" : "updateModel",
-      "click .btn"   : "handleChange",
-      // "keypress input.single-number" : "handleTyping"
-    },
-    
-    /**    
-     * For single input (non-range) models, whether or not to show the search
-     * button    
-     * @type {boolean}
-     */     
-    showButton: true,
-
-    initialize: function (options) {
-      
-      const view = this
-
-      if( !options || typeof options != "object" ){
-        var options = {};
-      }
-
-      this.model = options.model || new NumericFilter();
-      
-      if(typeof options.showButton === "boolean"){
-        this.showButton = options.showButton;
-      }
-
-      // Re-render if the rangeMin, rangeMax, or step changes
-      const limitChange = "change:rangeMin change:rangeMax change:step"
-      this.stopListening(this.model, limitChange);
-      this.listenTo(this.model, limitChange, function () {
-        setTimeout(function () {
-          view.render();
-        }, 1);
-      });
+    /** @lends NumericFilterView.prototype */ {
+      /**
+       *  A NumericFilter model to be rendered in this view
+       * @type {NumericFilter} */
+      model: null,
+
+      className: "filter numeric",
+
+      template: _.template(Template),
+
+      events: {
+        "change input.range": "updateRange",
+        "change input.single-number": "updateModel",
+        "click .btn": "handleChange",
+        // "keypress input.single-number" : "handleTyping"
+      },
+
+      /**
+       * For single input (non-range) models, whether or not to show the search
+       * button
+       * @type {boolean}
+       */
+      showButton: true,
+
+      initialize: function (options) {
+        const view = this;
+
+        if (!options || typeof options != "object") {
+          var options = {};
+        }
 
+        this.model = options.model || new NumericFilter();
 
-    },
+        if (typeof options.showButton === "boolean") {
+          this.showButton = options.showButton;
+        }
 
-    render: function () {
-      
-      var templateVars = _.extend(
-        this.model.toJSON(),
-        { showButton: this.showButton }
-      );
-      
-      this.$el.html(
-        this.template(templateVars)
-      );
-
-      //If a range of values is allowed, show the filter as a numeric slider
-      if(
-        this.model.get("range") &&
-        ( this.model.get("rangeMin") || this.model.get("rangeMax") )
-      ){
-
-        var view = this;
-
-        //jQueryUI slider
-        this.$('.slider').slider({
+        // Re-render if the rangeMin, rangeMax, or step changes
+        const limitChange = "change:rangeMin change:rangeMax change:step";
+        this.stopListening(this.model, limitChange);
+        this.listenTo(this.model, limitChange, function () {
+          setTimeout(function () {
+            view.render();
+          }, 1);
+        });
+      },
+
+      render: function () {
+        var templateVars = _.extend(this.model.toJSON(), {
+          showButton: this.showButton,
+        });
+
+        this.$el.html(this.template(templateVars));
+
+        //If a range of values is allowed, show the filter as a numeric slider
+        if (
+          this.model.get("range") &&
+          (this.model.get("rangeMin") || this.model.get("rangeMax"))
+        ) {
+          var view = this;
+
+          //jQueryUI slider
+          this.$(".slider").slider({
             range: true,
             disabled: false,
-            min: this.model.get("rangeMin"),  //sets the minimum on the UI slider on initialization
-            max: this.model.get("rangeMax"),   //sets the maximum on the UI slider on initialization
+            min: this.model.get("rangeMin"), //sets the minimum on the UI slider on initialization
+            max: this.model.get("rangeMax"), //sets the maximum on the UI slider on initialization
             values: [this.model.get("min"), this.model.get("max")], //where the left and right slider handles are
             step: this.model.get("step"),
-            stop: function( event, ui ) {
-
+            stop: function (event, ui) {
               // When the slider is changed, update the input values
-              view.$('input.min').val(ui.values[0]);
-              view.$('input.max').val(ui.values[1]);
+              view.$("input.min").val(ui.values[0]);
+              view.$("input.max").val(ui.values[1]);
 
               //Also update the DateFilter model
-              view.model.set('min', ui.values[0]);
-              view.model.set('max', ui.values[1]);
-
-            }
+              view.model.set("min", ui.values[0]);
+              view.model.set("max", ui.values[1]);
+            },
           });
 
           //When the rangeReset event is triggered, reset the slider
           this.listenTo(view.model, "rangeReset", this.resetSlider);
-
-      }
-      else {
-        // If a range of values is not allowed, show the filter as a single number input
-        var numberInput = this.$("input.single-number");
-        
-        if(numberInput && numberInput.length){
-          //If a minimum number is set on the model defaults
-          if(this.model.get("min") != null){
-            //Set the minimum value on the number input
-            numberInput.attr("value", this.model.get("min"));
-            this.singleValueType = "min"
-          //If a maximum number is set on the model defaults
-          } else if(this.model.get("max") != null){
-            //Set the minimum value on the number input
-            numberInput.attr("value", this.model.get("max"));
-            this.singleValueType = "max"
-          } else if (this.model.get("values")) {
-            if(this.model.get("values").length){
-              numberInput.attr("value", this.model.get("values")[0]);
-              this.singleValueType = "value"
+        } else {
+          // If a range of values is not allowed, show the filter as a single number input
+          var numberInput = this.$("input.single-number");
+
+          if (numberInput && numberInput.length) {
+            //If a minimum number is set on the model defaults
+            if (this.model.get("min") != null) {
+              //Set the minimum value on the number input
+              numberInput.attr("value", this.model.get("min"));
+              this.singleValueType = "min";
+              //If a maximum number is set on the model defaults
+            } else if (this.model.get("max") != null) {
+              //Set the minimum value on the number input
+              numberInput.attr("value", this.model.get("max"));
+              this.singleValueType = "max";
+            } else if (this.model.get("values")) {
+              if (this.model.get("values").length) {
+                numberInput.attr("value", this.model.get("values")[0]);
+                this.singleValueType = "value";
+              }
             }
           }
-      }
-        //Set a step attribute if there is one set on the model
-        if( this.model.get("step") != null ){
-          numberInput.attr("step", this.model.get("step"));
+          //Set a step attribute if there is one set on the model
+          if (this.model.get("step") != null) {
+            numberInput.attr("step", this.model.get("step"));
+          }
         }
-      }
-    },
-
-    /**
-    * Updates the value set on the Filter Model associated with this view.
-    * The filter value is grabbed from the input element in this view,
-    * and then set on either the min, max, or value attribute, depending
-    * on the single value type.
-    */
-    updateModel: function(){
-      // Get the value of the number input
-      var value = this.$("input.single-number").val(),
+      },
+
+      /**
+       * Updates the value set on the Filter Model associated with this view.
+       * The filter value is grabbed from the input element in this view,
+       * and then set on either the min, max, or value attribute, depending
+       * on the single value type.
+       */
+      updateModel: function () {
+        // Get the value of the number input
+        var value = this.$("input.single-number").val(),
           value = Number(value);
-          
-      if(["min", "max"].includes(this.singleValueType)){
-        this.model.set(this.singleValueType, value)
-      } else {
-        this.model.set("values", [value])
-      }
-    },
-
-    /**
-    * Gets the min and max years from the number inputs and updates the DateFilter
-    *  model and the year UI slider.
-    * @param {Event} e - The event that triggered this callback function
-    */
-    updateRange : function(e) {
-
-      //Get the min and max values from the number inputs
-      var minVal = Number(this.$('input.min').val());
-      var maxVal = Number(this.$('input.max').val());
-
-      //Update the DateFilter model to match what is in the text inputs
-      this.model.set('min', Number(minVal));
-      this.model.set('max', Number(maxVal));
-
-      // Update the UI slider to match the new min and max.
-      // Can only update the slider values if the slider has been initialized.
-      // There's no slider if there is a min & max on the model, but not maxRange
-      // and no minRange.
-      if(this.$( ".slider" ).slider("instance")){
-        this.$( ".slider" ).slider( "option", "values", [ minVal, maxVal ] );
-      }
 
-      //Track this event
-      MetacatUI.analytics?.trackEvent("portal search", "filter, Data Year", minVal + " to " + maxVal);
+        if (["min", "max"].includes(this.singleValueType)) {
+          this.model.set(this.singleValueType, value);
+        } else {
+          this.model.set("values", [value]);
+        }
+      },
+
+      /**
+       * Gets the min and max years from the number inputs and updates the DateFilter
+       *  model and the year UI slider.
+       * @param {Event} e - The event that triggered this callback function
+       */
+      updateRange: function (e) {
+        //Get the min and max values from the number inputs
+        var minVal = Number(this.$("input.min").val());
+        var maxVal = Number(this.$("input.max").val());
+
+        //Update the DateFilter model to match what is in the text inputs
+        this.model.set("min", Number(minVal));
+        this.model.set("max", Number(maxVal));
+
+        // Update the UI slider to match the new min and max.
+        // Can only update the slider values if the slider has been initialized.
+        // There's no slider if there is a min & max on the model, but not maxRange
+        // and no minRange.
+        if (this.$(".slider").slider("instance")) {
+          this.$(".slider").slider("option", "values", [minVal, maxVal]);
+        }
 
+        //Track this event
+        MetacatUI.analytics?.trackEvent(
+          "portal search",
+          "filter, Data Year",
+          minVal + " to " + maxVal,
+        );
+      },
+
+      /**
+       * Resets the slider to the default values
+       */
+      resetSlider: function () {
+        //Set the min and max values on the slider widget
+        this.$(".slider").slider("option", "values", [
+          this.model.get("rangeMin"),
+          this.model.get("rangeMax"),
+        ]);
+
+        //Reset the min and max values
+        this.$("input.min").val(this.model.get("rangeMin"));
+        this.$("input.max").val(this.model.get("rangeMax"));
+      },
     },
-
-    /**
-    * Resets the slider to the default values
-    */
-    resetSlider: function(){
-
-      //Set the min and max values on the slider widget
-      this.$( ".slider" ).slider( "option", "values", [ this.model.get("rangeMin"), this.model.get("rangeMax") ] );
-
-      //Reset the min and max values
-      this.$('input.min').val( this.model.get("rangeMin") );
-      this.$('input.max').val( this.model.get("rangeMax") );
-
-    }
-
-  });
+  );
   return NumericFilterView;
 });
 
diff --git a/docs/docs/src_js_views_filters_SemanticFilterView.js.html b/docs/docs/src_js_views_filters_SemanticFilterView.js.html index a20bf46ae..ce14eb39c 100644 --- a/docs/docs/src_js_views_filters_SemanticFilterView.js.html +++ b/docs/docs/src_js_views_filters_SemanticFilterView.js.html @@ -44,149 +44,152 @@

Source: src/js/views/filters/SemanticFilterView.js

-
/*global define */
-define(['jquery',
-  'underscore',
-  'backbone',
-  'models/filters/Filter',
-  'views/filters/FilterView',
-  'views/searchSelect/AnnotationFilterView'],
-  function ($, _, Backbone, Filter, FilterView, AnnotationFilterView) {
-    'use strict';
-
-    /**
-    * @class SemanticFilterView
-    * @classdesc Render a specialized view of a single Filter model using the
-    *   AnnotationFilterView.
-    * @classcategory Views/Filters
-    * @extends FilterView
-    * @screenshot views/filters/SemanticFilterView.png
-    * @since 2.22.0
-    */
-    var SemanticFilterView = FilterView.extend(
-    /** @lends SemanticFilterView.prototype */{
-
-        /**
-         * @inheritdoc
-         */
-        model: null,
-
-        /**
-         * @inheritdoc
-         */
-        modelClass: Filter,
-
-        className: "filter semantic",
-
-        // Template is an empty function because this view delegates to the
-        // AnnotationFilterView. See render() method.
-        template: function () { },
-
-        /**
-        * Render an instance of a Semantic Filter View.
-        *
-        * Note that this View doesn't have a template and instead delegates to
-        * the AnnotationFilterView which renders a SearchableSelectView which
-        * renders an NCBOTree.
-        * @since 2.22.0
-        */
-        render: function () {
-
-          try {
-            var templateVars = this.model.toJSON();
-            templateVars.id = this.model.cid;
-
-            // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
-            FilterView.prototype.render.call(this, templateVars);
-
-            var viewOpts = {
-              "useSearchableSelect": true,
-              "placeholderText": templateVars.placeholder,
-              "inputLabel": null, // Hides label and uses FilterView label
-              "ontology": this.model.get("ontology"),
-              "startingRoot": this.model.get("startingRoot")
-            };
-
-            var subView = new AnnotationFilterView(viewOpts);
-
-            this.$el.append(subView.el);
-            subView.render();
-
-            var view = this;
-            subView.on("annotationSelected", function (event, item) {
-              // Get the value of the associated input
-              var term = (!item || !item.value) ? input.val() : item.value;
-              var label = (!item || !item.filterLabel) ? null : item.filterLabel;
-
-              // Set up a label mapping for the term so we can display a
-              // human-readable label for it in the UI
-              view.setLabelMapping(term, label);
-
-              // Set the value, supports multiple values
-              var currentValue = view.model.get("values");
-              var newValuesArray = _.flatten(new Array(currentValue, term));
-              view.model.set("values", newValuesArray);
-
-              view.defocus()
-            });
-          }
-          catch (error) {
-            console.log('There was an error rendering a SemanticFilterView.' +
-              ' Error details: ' + error);
-          }
-        },
-
-        /**
-         * Helper function which defocuses the dropdown portion of the
-         * SearchableSelectView used by this View's AnnotationFilterView. When the
-         * user clicks an item in the NCBOTree widget, we want the
-         * SearchableSelectView's dropdown to go away and I couldn't find any API
-         * to do that so we have this code. See the render() method to see how it's
-         * called.
-         *
-         * Note: This isn't really a stable API and is really something we might
-         * remove in the future if we refactor the NCBOTree widget.
-         * @since 2.22.0
-         */
-        defocus: function () {
-          this.$el.find("div.menu").removeClass("visible").addClass("hidden")
-          this.$el.find("div.fluid.ui.dropdown").removeClass("active").removeClass("visible")
-          this.$el.find("input").blur()
-        },
-
-        /**
-         * Set the human-readable label for a term URI.
-         *
-         * For most uses of the Filter model, the value(s) set on the model can
-         * be shown directly in the UI. But for Semantic searches, we need to
-         * be able to display a human-readable label for the value because the
-         * value is likely an opaque URI.
-         *
-         * Rather than fetch and/or store all the possible labels for all
-         * possible URIs, we store a label for whichever terms the user chooses
-         * and keep that around until we need it in the UI.
-         *
-         * @param {string} term The term URI to set a label for
-         * @param {string} label The label to set
-         * @since 2.22.0
-         */
-        setLabelMapping: function (term, label) {
-          var newMappings;
-
-          if (this.model.get("valueLabels")) {
-            newMappings = _.clone(this.model.get("valueLabels"));
-          }
-          else {
-            newMappings = new Object();
-          }
-
-          newMappings[term] = label;
-          this.model.set("valueLabels", newMappings);
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/filters/Filter",
+  "views/filters/FilterView",
+  "views/searchSelect/AnnotationFilterView",
+], function ($, _, Backbone, Filter, FilterView, AnnotationFilterView) {
+  "use strict";
+
+  /**
+   * @class SemanticFilterView
+   * @classdesc Render a specialized view of a single Filter model using the
+   *   AnnotationFilterView.
+   * @classcategory Views/Filters
+   * @extends FilterView
+   * @screenshot views/filters/SemanticFilterView.png
+   * @since 2.22.0
+   */
+  var SemanticFilterView = FilterView.extend(
+    /** @lends SemanticFilterView.prototype */ {
+      /**
+       * @inheritdoc
+       */
+      model: null,
+
+      /**
+       * @inheritdoc
+       */
+      modelClass: Filter,
+
+      className: "filter semantic",
+
+      // Template is an empty function because this view delegates to the
+      // AnnotationFilterView. See render() method.
+      template: function () {},
+
+      /**
+       * Render an instance of a Semantic Filter View.
+       *
+       * Note that this View doesn't have a template and instead delegates to
+       * the AnnotationFilterView which renders a SearchableSelectView which
+       * renders an NCBOTree.
+       * @since 2.22.0
+       */
+      render: function () {
+        try {
+          var templateVars = this.model.toJSON();
+          templateVars.id = this.model.cid;
+
+          // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
+          FilterView.prototype.render.call(this, templateVars);
+
+          var viewOpts = {
+            useSearchableSelect: true,
+            placeholderText: templateVars.placeholder,
+            inputLabel: null, // Hides label and uses FilterView label
+            ontology: this.model.get("ontology"),
+            startingRoot: this.model.get("startingRoot"),
+          };
+
+          var subView = new AnnotationFilterView(viewOpts);
+
+          this.$el.append(subView.el);
+          subView.render();
+
+          var view = this;
+          subView.on("annotationSelected", function (event, item) {
+            // Get the value of the associated input
+            var term = !item || !item.value ? input.val() : item.value;
+            var label = !item || !item.filterLabel ? null : item.filterLabel;
+
+            // Set up a label mapping for the term so we can display a
+            // human-readable label for it in the UI
+            view.setLabelMapping(term, label);
+
+            // Set the value, supports multiple values
+            var currentValue = view.model.get("values");
+            var newValuesArray = _.flatten(new Array(currentValue, term));
+            view.model.set("values", newValuesArray);
+
+            view.defocus();
+          });
+        } catch (error) {
+          console.log(
+            "There was an error rendering a SemanticFilterView." +
+              " Error details: " +
+              error,
+          );
         }
-      });
+      },
+
+      /**
+       * Helper function which defocuses the dropdown portion of the
+       * SearchableSelectView used by this View's AnnotationFilterView. When the
+       * user clicks an item in the NCBOTree widget, we want the
+       * SearchableSelectView's dropdown to go away and I couldn't find any API
+       * to do that so we have this code. See the render() method to see how it's
+       * called.
+       *
+       * Note: This isn't really a stable API and is really something we might
+       * remove in the future if we refactor the NCBOTree widget.
+       * @since 2.22.0
+       */
+      defocus: function () {
+        this.$el.find("div.menu").removeClass("visible").addClass("hidden");
+        this.$el
+          .find("div.fluid.ui.dropdown")
+          .removeClass("active")
+          .removeClass("visible");
+        this.$el.find("input").blur();
+      },
+
+      /**
+       * Set the human-readable label for a term URI.
+       *
+       * For most uses of the Filter model, the value(s) set on the model can
+       * be shown directly in the UI. But for Semantic searches, we need to
+       * be able to display a human-readable label for the value because the
+       * value is likely an opaque URI.
+       *
+       * Rather than fetch and/or store all the possible labels for all
+       * possible URIs, we store a label for whichever terms the user chooses
+       * and keep that around until we need it in the UI.
+       *
+       * @param {string} term The term URI to set a label for
+       * @param {string} label The label to set
+       * @since 2.22.0
+       */
+      setLabelMapping: function (term, label) {
+        var newMappings;
+
+        if (this.model.get("valueLabels")) {
+          newMappings = _.clone(this.model.get("valueLabels"));
+        } else {
+          newMappings = new Object();
+        }
+
+        newMappings[term] = label;
+        this.model.set("valueLabels", newMappings);
+      },
+    },
+  );
 
-    return SemanticFilterView;
-  });
+  return SemanticFilterView;
+});
 
diff --git a/docs/docs/src_js_views_filters_ToggleFilterView.js.html b/docs/docs/src_js_views_filters_ToggleFilterView.js.html index 3b303e5b6..ec695c180 100644 --- a/docs/docs/src_js_views_filters_ToggleFilterView.js.html +++ b/docs/docs/src_js_views_filters_ToggleFilterView.js.html @@ -44,208 +44,247 @@

Source: src/js/views/filters/ToggleFilterView.js

-
/*global define */
-define(['jquery', 'underscore', 'backbone',
-        'models/filters/ToggleFilter',
-        'views/filters/FilterView',
-        'text!templates/filters/toggleFilter.html',
-        'text!templates/filters/booleanFilter.html'],
-  function($, _, Backbone, ToggleFilter, FilterView, Template, BooleanTemplate) {
-  'use strict';
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/filters/ToggleFilter",
+  "views/filters/FilterView",
+  "text!templates/filters/toggleFilter.html",
+  "text!templates/filters/booleanFilter.html",
+], function (
+  $,
+  _,
+  Backbone,
+  ToggleFilter,
+  FilterView,
+  Template,
+  BooleanTemplate,
+) {
+  "use strict";
 
   /**
-  * @class ToggleFilterView
-  * @classdesc Render a view of a single ToggleFilter model
-  * @classcategory Views/Filters
-  * @extends FilterView
-  */
+   * @class ToggleFilterView
+   * @classdesc Render a view of a single ToggleFilter model
+   * @classcategory Views/Filters
+   * @extends FilterView
+   */
   var ToggleFilterView = FilterView.extend(
-    /** @lends ToggleFilterView.prototype */{
-
-    /**
-    *  A ToggleFilter model to be rendered in this view
-    * @type {ToggleFilter} */
-    model: null,
-
-    /**
-     * @inheritdoc
-     */
-    modelClass: ToggleFilter,
-
-    className: "filter toggle",
-
-    template: _.template(Template),
-    booleanTemplate: _.template(BooleanTemplate),
-
-    /**
-     * @inheritdoc
-     */
-    events: function () {
-      try {
-        var events = FilterView.prototype.events.call(this);
-        events["click input[type='checkbox']"] = "updateModel";
-        
-        return events
-      }
-      catch (error) {
-        console.log('There was an error creating the events object for a ToggleFilterView' +
-          ' Error details: ' + error);
-      }
-    },
+    /** @lends ToggleFilterView.prototype */ {
+      /**
+       *  A ToggleFilter model to be rendered in this view
+       * @type {ToggleFilter} */
+      model: null,
+
+      /**
+       * @inheritdoc
+       */
+      modelClass: ToggleFilter,
+
+      className: "filter toggle",
+
+      template: _.template(Template),
+      booleanTemplate: _.template(BooleanTemplate),
+
+      /**
+       * @inheritdoc
+       */
+      events: function () {
+        try {
+          var events = FilterView.prototype.events.call(this);
+          events["click input[type='checkbox']"] = "updateModel";
+
+          return events;
+        } catch (error) {
+          console.log(
+            "There was an error creating the events object for a ToggleFilterView" +
+              " Error details: " +
+              error,
+          );
+        }
+      },
 
-    /**
-    * @inheritdoc
-    */
+      /**
+       * @inheritdoc
+       */
       render: function (templateVars = {}) {
-
-      try {
-        templateVars = _.extend(this.model.toJSON(), templateVars);
-        templateVars.id = this.model.cid;
-
-        if (!this.model.get("falseLabel")) {
-          //If the value is the same as the trueValue, the checkbox should be checked
-          templateVars.checked = (this.model.get("values")[0] == this.model.get("trueValue"))? true : false;
-
-          //Use the BooleanFilter template for toggles with only a true value
-          this.$el.addClass("boolean");
-          this.template = this.booleanTemplate
+        try {
+          templateVars = _.extend(this.model.toJSON(), templateVars);
+          templateVars.id = this.model.cid;
+
+          if (!this.model.get("falseLabel")) {
+            //If the value is the same as the trueValue, the checkbox should be checked
+            templateVars.checked =
+              this.model.get("values")[0] == this.model.get("trueValue")
+                ? true
+                : false;
+
+            //Use the BooleanFilter template for toggles with only a true value
+            this.$el.addClass("boolean");
+            this.template = this.booleanTemplate;
+          }
+
+          // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
+          FilterView.prototype.render.call(this, templateVars);
+
+          this.listenTo(this.model, "change:values", this.updateToggle);
+        } catch (error) {
+          console.log(
+            "There was an error rendering a ToggleFilterView." +
+              " Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Actions to perform after the render() function has completed and this view's
+       * element is added to the webpage.
+       */
+      postRender: function () {
+        this.setToggleWidth();
+      },
+
+      updateToggle: function () {
+        //If the model is set to true
+        if (
+          this.model.get("values").length &&
+          this.model.get("values")[0] == this.model.get("trueValue")
+        ) {
+          this.$("input").prop("checked", true);
+        } else if (
+          this.model.get("values").length &&
+          this.model.get("values")[0] == this.model.get("falseValue")
+        ) {
+          this.$("input").prop("checked", false);
+        } else if (!this.model.get("values").length) {
+          this.$("input").prop("checked", false);
         }
-        
-        // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
-        FilterView.prototype.render.call(this, templateVars);
-
-        this.listenTo(this.model, "change:values", this.updateToggle);
-      }
-      catch (error) {
-        console.log( 'There was an error rendering a ToggleFilterView.' +
-          ' Error details: ' + error );
-      }
-      
-    },
-
-    /**
-    * Actions to perform after the render() function has completed and this view's
-    * element is added to the webpage.
-    */
-    postRender: function(){
-      this.setToggleWidth();
-    },
-
-    updateToggle: function(){
-
-      //If the model is set to true
-      if( this.model.get("values").length && this.model.get("values")[0] == this.model.get("trueValue") ){
-        this.$("input").prop("checked", true);
-      }
-      else if( this.model.get("values").length && this.model.get("values")[0] == this.model.get("falseValue") ){
-        this.$("input").prop("checked", false);
-      }
-      else if( !this.model.get("values").length ){
-        this.$("input").prop("checked", false);
-      }
-
-      this.setToggleWidth();
-
-    },
-
-    /**
-    * Gets the width of the toggle labels and sets the various CSS attributes
-    * necessary for the switch to fully display each label
-    */
-    setToggleWidth: function(){
 
-      //If there is no toggle element, exit now
-      if( !this.$(".can-toggle-switch").length ){
-        return;
-      }
+        this.setToggleWidth();
+      },
+
+      /**
+       * Gets the width of the toggle labels and sets the various CSS attributes
+       * necessary for the switch to fully display each label
+       */
+      setToggleWidth: function () {
+        //If there is no toggle element, exit now
+        if (!this.$(".can-toggle-switch").length) {
+          return;
+        }
 
-      //Get the padding and widths of the switch elements
-      var switchPadding    = 24,
+        //Get the padding and widths of the switch elements
+        var switchPadding = 24,
           onSwitchWidth = this.$(".true-label").width(),
-          offSwitchWidth  = this.$(".false-label").width(),
-          totalSwitchWidth = onSwitchWidth + offSwitchWidth + (switchPadding * 2) + 2,
+          offSwitchWidth = this.$(".false-label").width(),
+          totalSwitchWidth =
+            onSwitchWidth + offSwitchWidth + switchPadding * 2 + 2,
           isChecked = this.$("input[type='checkbox']").prop("checked");
 
-      //Set the width on the whole view
-      this.$el.width(totalSwitchWidth + "px");
-
-      //Get the toggle switch element
-      var toggleSwitch = this.$(".can-toggle-switch");
-
-      //Add an identifier to the toggle switch element
-      toggleSwitch.attr("id", "toggle-" + this.model.cid);
-
-      //Change the width of the toggle switch
-      toggleSwitch.css("flex", "0 0 " + totalSwitchWidth + "px");
-
-      //Create CSS for the :before and :after pseudo elements, which is best done
-      // by adding a style tag directly to the DOM
-      if( isChecked ){
-        var newCSS = "#" + "toggle-" + this.model.cid + ":before{ " +
-                       "transform: translate3d(" + (onSwitchWidth + switchPadding) + "px, 0, 0);" +
-                       "width: " + (offSwitchWidth + switchPadding) + "px ;" +
-                     "}" +
-                     "#" + "toggle-" + this.model.cid + ":after{ " +
-                      "width: " + (onSwitchWidth + switchPadding) + "px;" +
-                      "transform: translate3d(0px, 0, 0);" +
-                    "}";
-      }
-      else{
-        var newCSS = "#" + "toggle-" + this.model.cid + ":before{ " +
-                       "width: " + (offSwitchWidth + switchPadding) + "px ;" +
-                       "left: 0px ;" +
-                     "}" +
-                     "#" + "toggle-" + this.model.cid + ":after{ " +
-                      "width: " + (onSwitchWidth + switchPadding) + "px;" +
-                      "transform: translate3d(" + (offSwitchWidth + switchPadding) + "px, 0, 0);" +
-                    "}";
-      }
-
-      //Get or create a style tag
-      var styleTag = toggleSwitch.children("style");
-      if( !styleTag.length ){
-        styleTag = $(document.createElement("style"));
-        toggleSwitch.append(styleTag);
-      }
-
-      //Add the CSS to the style tag
-      styleTag.html(newCSS);
-    },
+        //Set the width on the whole view
+        this.$el.width(totalSwitchWidth + "px");
+
+        //Get the toggle switch element
+        var toggleSwitch = this.$(".can-toggle-switch");
+
+        //Add an identifier to the toggle switch element
+        toggleSwitch.attr("id", "toggle-" + this.model.cid);
+
+        //Change the width of the toggle switch
+        toggleSwitch.css("flex", "0 0 " + totalSwitchWidth + "px");
+
+        //Create CSS for the :before and :after pseudo elements, which is best done
+        // by adding a style tag directly to the DOM
+        if (isChecked) {
+          var newCSS =
+            "#" +
+            "toggle-" +
+            this.model.cid +
+            ":before{ " +
+            "transform: translate3d(" +
+            (onSwitchWidth + switchPadding) +
+            "px, 0, 0);" +
+            "width: " +
+            (offSwitchWidth + switchPadding) +
+            "px ;" +
+            "}" +
+            "#" +
+            "toggle-" +
+            this.model.cid +
+            ":after{ " +
+            "width: " +
+            (onSwitchWidth + switchPadding) +
+            "px;" +
+            "transform: translate3d(0px, 0, 0);" +
+            "}";
+        } else {
+          var newCSS =
+            "#" +
+            "toggle-" +
+            this.model.cid +
+            ":before{ " +
+            "width: " +
+            (offSwitchWidth + switchPadding) +
+            "px ;" +
+            "left: 0px ;" +
+            "}" +
+            "#" +
+            "toggle-" +
+            this.model.cid +
+            ":after{ " +
+            "width: " +
+            (onSwitchWidth + switchPadding) +
+            "px;" +
+            "transform: translate3d(" +
+            (offSwitchWidth + switchPadding) +
+            "px, 0, 0);" +
+            "}";
+        }
 
-    /**
-    * Updates the value set on the ToggleFilter Model associated with this view.
-    * The filter value is grabbed from the checkbox element in this view.
-    *
-    */
-      updateModel: function () {
+        //Get or create a style tag
+        var styleTag = toggleSwitch.children("style");
+        if (!styleTag.length) {
+          styleTag = $(document.createElement("style"));
+          toggleSwitch.append(styleTag);
+        }
 
-      //Check if the checkbox is checked
-      var isChecked = this.$("input").prop("checked");
+        //Add the CSS to the style tag
+        styleTag.html(newCSS);
+      },
 
-      //If the toggle is checked, then set the true toggle value on the model
-      if( isChecked ){
-        if( this.model.get("values")[0] !== this.model.get("trueValue") ){
-          this.model.set("values", [ this.model.get("trueValue") ]);
+      /**
+       * Updates the value set on the ToggleFilter Model associated with this view.
+       * The filter value is grabbed from the checkbox element in this view.
+       *
+       */
+      updateModel: function () {
+        //Check if the checkbox is checked
+        var isChecked = this.$("input").prop("checked");
+
+        //If the toggle is checked, then set the true toggle value on the model
+        if (isChecked) {
+          if (this.model.get("values")[0] !== this.model.get("trueValue")) {
+            this.model.set("values", [this.model.get("trueValue")]);
+          }
         }
-      }
-      //If the toggle is not checked and there is no false value specified,
-      // then remove the value from the model completely
-      else if(!this.model.get("falseValue")){
-        if( this.model.get("values").length > 0 ){
-          this.model.set("values", []);
+        //If the toggle is not checked and there is no false value specified,
+        // then remove the value from the model completely
+        else if (!this.model.get("falseValue")) {
+          if (this.model.get("values").length > 0) {
+            this.model.set("values", []);
+          }
         }
-      }
-      //If the toggle is not checked and there is a false value specified,
-      // then set the false toggle value on the model
-      else{
-        if( this.model.get("values")[0] !== this.model.get("falseValue") ){
-          this.model.set("values", [ this.model.get("falseValue") ]);
+        //If the toggle is not checked and there is a false value specified,
+        // then set the false toggle value on the model
+        else {
+          if (this.model.get("values")[0] !== this.model.get("falseValue")) {
+            this.model.set("values", [this.model.get("falseValue")]);
+          }
         }
-      }
-
-    }
-
-  });
+      },
+    },
+  );
   return ToggleFilterView;
 });
 
diff --git a/docs/docs/src_js_views_maps_CesiumWidgetView.js.html b/docs/docs/src_js_views_maps_CesiumWidgetView.js.html index 7f398bf6a..ac4242e02 100644 --- a/docs/docs/src_js_views_maps_CesiumWidgetView.js.html +++ b/docs/docs/src_js_views_maps_CesiumWidgetView.js.html @@ -65,7 +65,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

MapAsset, Cesium3DTileset, Feature, - Template + Template, ) { /** * @class CesiumWidgetView @@ -281,8 +281,10 @@

Source: src/js/views/maps/CesiumWidgetView.js

view.scene = view.widget.scene; view.camera = view.widget.camera; - if (typeof this.model.get("globeBaseColor") === 'string') { - const baseColor = Cesium.Color.fromCssColorString(this.model.get("globeBaseColor")); + if (typeof this.model.get("globeBaseColor") === "string") { + const baseColor = Cesium.Color.fromCssColorString( + this.model.get("globeBaseColor"), + ); if (baseColor) { view.scene.globe.baseColor = baseColor; } @@ -382,7 +384,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

view.scene.preRender.addEventListener(function (scene, time) { view.scene.light.direction = Cesium.Cartesian3.clone( scene.camera.directionWC, - view.scene.light.direction + view.scene.light.direction, ); }); }, @@ -408,7 +410,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

scene.postProcessStages.add( Cesium.PostProcessStageLibrary.createSilhouetteStage([ view.silhouettes, - ]) + ]), ); } catch (e) { console.log("Error initializing picking in a CesiumWidgetView", e); @@ -426,7 +428,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

// Listen for addition or removal of layers TODO: Add similar listeners // for terrain - _.each(layerGroups, layers => { + _.each(layerGroups, (layers) => { if (layers) { view.stopListening(layers); view.listenTo(layers, "add", view.addAsset); @@ -436,10 +438,14 @@

Source: src/js/views/maps/CesiumWidgetView.js

// etc. has been updated. Re-render the scene when this happens. view.listenTo(layers, "appearanceChanged", view.requestRender); } - }) + }); // Reset asset listeners if the layers collection is replaced view.stopListening(model, "change:layers change:layerCategories"); - view.listenTo(model, "change:layers change:layerCategories", view.setAssetListeners); + view.listenTo( + model, + "change:layers change:layerCategories", + view.setAssetListeners, + ); }, /** @@ -463,7 +469,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

// Zoom functions executed after each scene render this.removePostRenderListener = this.scene.postRender.addEventListener( this.postRender, - this + this, ); this.listenTo(this.interactions, "change:zoomTarget", function () { const target = this.interactions.get("zoomTarget"); @@ -727,7 +733,6 @@

Source: src/js/views/maps/CesiumWidgetView.js

*/ completeFlight: function (target, options) { try { - // A target is required if (!target) return; @@ -761,7 +766,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

options.offset = new Cesium.HeadingPitchRange( 0.0, -0.5, - assetBoundingSphere.radius + assetBoundingSphere.radius, ); } view.flyTo(assetBoundingSphere, options); @@ -786,7 +791,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

view.listenToOnce(layer, "change:displayReady", function () { view.flyTo(target, options); }); - return + return; } view.flyTo(target.get("featureObject"), options); return; @@ -797,11 +802,12 @@

Source: src/js/views/maps/CesiumWidgetView.js

const entity = target instanceof Cesium.Entity ? target : target.id; if (entity instanceof Cesium.Entity) { - - view.dataSourceDisplay._ready = true - view.getBoundingSphereFromEntity(entity).then(function (entityBoundingSphere) { - view.flyTo(entityBoundingSphere, options); - }); + view.dataSourceDisplay._ready = true; + view + .getBoundingSphereFromEntity(entity) + .then(function (entityBoundingSphere) { + view.flyTo(entityBoundingSphere, options); + }); return; } @@ -839,14 +845,14 @@

Source: src/js/views/maps/CesiumWidgetView.js

}, getBoundingSphereFromEntity: function (entity) { - const view = this + const view = this; const entityBoundingSphere = new Cesium.BoundingSphere(); const readyState = Cesium.BoundingSphereState.DONE; function getBS() { return view.dataSourceDisplay.getBoundingSphere( entity, false, - entityBoundingSphere + entityBoundingSphere, ); } // Return a promise that resolves to bounding box when it's ready. @@ -861,11 +867,12 @@

Source: src/js/views/maps/CesiumWidgetView.js

// Search for the entity again in case it was removed and // re-added to the data source display. entity = view.getEntityById(entity.id, entity.entityCollection); - if(!entity) { + if (!entity) { clearInterval(interval); - reject("Failed to get bounding sphere for entity, entity not found."); + reject( + "Failed to get bounding sphere for entity, entity not found.", + ); } - } else { clearInterval(interval); resolve(entityBoundingSphere); @@ -875,7 +882,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

reject("Failed to get bounding sphere for entity."); } }, 100); - }) + }); }, /** @@ -940,7 +947,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

target.destination = Cesium.Cartesian3.fromDegrees( position.longitude, position.latitude, - position.height + position.height, ); if ( @@ -1020,7 +1027,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

} var rect = camera.computeViewRectangle( scene.globe.ellipsoid, - view.scratchRectangle + view.scratchRectangle, ); coords.north = Cesium.Math.toDegrees(rect.north); coords.east = Cesium.Math.toDegrees(rect.east); @@ -1049,7 +1056,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

Object.values(edges).forEach(function (point) { if (point) { edgeLatitudes.push( - view.getDegreesFromCartesian(point).latitude + view.getDegreesFromCartesian(point).latitude, ); } }); @@ -1068,10 +1075,10 @@

Source: src/js/views/maps/CesiumWidgetView.js

// If not focused directly on one of the poles, then also limit the // east and west sides of the bounding box const northPointLat = view.getDegreesFromCartesian( - edges.top + edges.top, ).latitude; const southPointLat = view.getDegreesFromCartesian( - edges.bottom + edges.bottom, ).latitude; if (northPointLat > 25 && southPointLat < -25) { @@ -1134,7 +1141,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

coordinates.forEach(function (coordinate) { if (Cesium.defined(cartographic[coordinate])) { degrees[coordinate] = Cesium.Math.toDegrees( - cartographic[coordinate] + cartographic[coordinate], ); } }); @@ -1181,7 +1188,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

console.log( "There was an error finding the edge points in a CesiumWidgetView" + ". Error details: " + - error + error, ); } }, @@ -1215,7 +1222,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

const p3a = Cesium.Cartesian3.fromRadians( midPt.longitude, midPt.latitude, - 0.0 + 0.0, ); return p3a; @@ -1223,7 +1230,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

console.log( "There was an error finding a midpoint in a CesiumWidgetView" + ". Error details: " + - error + error, ); } }, @@ -1346,10 +1353,10 @@

Source: src/js/views/maps/CesiumWidgetView.js

const height = scene.canvas.clientHeight; const left = camera.getPickRay( - new Cesium.Cartesian2((width / 2) | 0, height - 1) + new Cesium.Cartesian2((width / 2) | 0, height - 1), ); const right = camera.getPickRay( - new Cesium.Cartesian2((1 + width / 2) | 0, height - 1) + new Cesium.Cartesian2((1 + width / 2) | 0, height - 1), ); const leftPosition = globe.pick(left, scene); @@ -1376,7 +1383,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

console.log( "Failed to get a pixel to meters measurement in a CesiumWidgetView" + ". Error details: " + - error + error, ); return false; } @@ -1413,7 +1420,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

if (!renderFunction || typeof renderFunction !== "function") { mapAsset.set( "statusDetails", - "This type of resource is not supported in the map widget." + "This type of resource is not supported in the map widget.", ); mapAsset.set("status", "error"); return; @@ -1443,7 +1450,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

console.error("Error rendering an asset", e, mapAsset); mapAsset.set( "statusDetails", - "There was a problem rendering this resource in the map widget." + "There was a problem rendering this resource in the map widget.", ); mapAsset.set("status", "error"); } @@ -1460,11 +1467,11 @@

Source: src/js/views/maps/CesiumWidgetView.js

const cesiumModel = mapAsset.get("cesiumModel"); if (!cesiumModel) return; // Find the remove function for this type of asset - const removeFunctionName = this.mapAssetRenderFunctions.find(function ( - option - ) { - return option.types.includes(mapAsset.get("type")); - })?.removeFunction; + const removeFunctionName = this.mapAssetRenderFunctions.find( + function (option) { + return option.types.includes(mapAsset.get("type")); + }, + )?.removeFunction; const removeFunction = this[removeFunctionName]; // If there is a function for this type of asset, call it if (removeFunction && typeof removeFunction === "function") { @@ -1472,7 +1479,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

} else { console.log( "No remove function found for this type of asset", - mapAsset + mapAsset, ); } }, @@ -1557,10 +1564,14 @@

Source: src/js/views/maps/CesiumWidgetView.js

*/ sortImagery: function () { const imageryInMap = this.scene.imageryLayers; - const imageryModels = _.reduce(this.model.getLayerGroups(), (models, layers) => { + const imageryModels = _.reduce( + this.model.getLayerGroups(), + (models, layers) => { models.push(...layers.getAll("CesiumImagery")); return models; - }, []); + }, + [], + ); // If there are no imagery layers, or just one, return if ( @@ -1598,7 +1609,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

*/ showImageryGrid: function ( color = "#ffffff", - tilingScheme = "GeographicTilingScheme" + tilingScheme = "GeographicTilingScheme", ) { try { const view = this; @@ -1607,7 +1618,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

console.log( `${color} is an invalid color for imagery grid. ` + `Must be a hex color starting with '#'. ` + - `Setting color to white: '#ffffff'` + `Setting color to white: '#ffffff'`, ); color = "#ffffff"; } @@ -1620,7 +1631,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

if (availableTS.indexOf(tilingScheme) == -1) { console.log( `${tilingScheme} is not a valid tiling scheme ` + - `for the imagery grid. Using WebMercatorTilingScheme` + `for the imagery grid. Using WebMercatorTilingScheme`, ); tilingScheme = "WebMercatorTilingScheme"; } @@ -1633,7 +1644,7 @@

Source: src/js/views/maps/CesiumWidgetView.js

const gridOutlines = new Cesium.GridImageryProvider(gridOpts); const gridCoords = new Cesium.TileCoordinatesImageryProvider( - gridOpts + gridOpts, ); view.scene.imageryLayers.addImageryProvider(gridOutlines); view.scene.imageryLayers.addImageryProvider(gridCoords); @@ -1641,11 +1652,11 @@

Source: src/js/views/maps/CesiumWidgetView.js

console.log( "There was an error showing the imagery grid in a CesiumWidgetView" + ". Error details: " + - error + error, ); } }, - } + }, ); return CesiumWidgetView; diff --git a/docs/docs/src_js_views_maps_DrawToolView.js.html b/docs/docs/src_js_views_maps_DrawToolView.js.html index afb7c7807..30b5d52fc 100644 --- a/docs/docs/src_js_views_maps_DrawToolView.js.html +++ b/docs/docs/src_js_views_maps_DrawToolView.js.html @@ -46,12 +46,12 @@

Source: src/js/views/maps/DrawToolView.js

"use strict";
 
-define(["backbone", "models/connectors/GeoPoints-CesiumPolygon", "models/connectors/GeoPoints-CesiumPoints", "collections/maps/GeoPoints"], function (
-  Backbone,
-  GeoPointsVectorData,
-  GeoPointsCesiumPoints,
-  GeoPoints
-) {
+define([
+  "backbone",
+  "models/connectors/GeoPoints-CesiumPolygon",
+  "models/connectors/GeoPoints-CesiumPoints",
+  "collections/maps/GeoPoints",
+], function (Backbone, GeoPointsVectorData, GeoPointsCesiumPoints, GeoPoints) {
   /**
    * @class DrawTool
    * @classdesc The DrawTool view allows a user to draw an arbitrary polygon on
@@ -248,7 +248,7 @@ 

Source: src/js/views/maps/DrawToolView.js

}, ], }, - }) + }); }, /** @@ -258,7 +258,7 @@

Source: src/js/views/maps/DrawToolView.js

* @returns {GeoPointsVectorData} The connector */ setUpConnectors: function () { - const points = this.points = new GeoPoints(); + const points = (this.points = new GeoPoints()); this.polygonConnector = new GeoPointsVectorData({ layer: this.layer, geoPoints: points, @@ -315,7 +315,7 @@

Source: src/js/views/maps/DrawToolView.js

* @returns {DrawTool} Returns the view */ render: function () { - if(!this.mapModel) { + if (!this.mapModel) { this.showError("No map model was provided."); return this; } @@ -329,7 +329,8 @@

Source: src/js/views/maps/DrawToolView.js

* @param {string} [message] - The error message to show to the user. */ showError: function (message) { - const str = `<i class="icon-warning-sign icon-left"></i>` + + const str = + `<i class="icon-warning-sign icon-left"></i>` + `<span> The draw tool is not available. ${message}</span>`; this.el.innerHTML = str; }, @@ -342,16 +343,16 @@

Source: src/js/views/maps/DrawToolView.js

const el = this.el; // Create the buttons - view.buttons.forEach(options => { + view.buttons.forEach((options) => { const button = document.createElement("button"); button.className = this.buttonClass; button.innerHTML = `<i class="icon icon-${options.icon}"></i> ${options.label}`; button.addEventListener("click", function () { const method = options.method; - if(method) view[method](); + if (method) view[method](); else view.toggleMode(options.name); }); - if(!view.buttonEls) view.buttonEls = {}; + if (!view.buttonEls) view.buttonEls = {}; view.buttonEls[options.name + "Button"] = button; el.appendChild(button); }); @@ -365,7 +366,7 @@

Source: src/js/views/maps/DrawToolView.js

*/ save: function (callback) { this.setMode(false); - if(callback && typeof callback === "function") { + if (callback && typeof callback === "function") { callback(this.points.toJSON()); } }, @@ -406,7 +407,7 @@

Source: src/js/views/maps/DrawToolView.js

*/ activateButton: function (buttonName) { const buttonEl = this.buttonEls[buttonName + "Button"]; - if(!buttonEl) return; + if (!buttonEl) return; this.resetButtonStyles(); buttonEl.classList.add(this.buttonClassActive); }, @@ -456,7 +457,7 @@

Source: src/js/views/maps/DrawToolView.js

"change:latitude change:longitude", () => { view.handleClick(); - } + }, ); this.listeningForClicks = true; // When the clickedPosition GeoPoint model or the MapInteractions model @@ -469,7 +470,7 @@

Source: src/js/views/maps/DrawToolView.js

view.handleClick(); view.setClickListeners(); } - } + }, ); handler.listenToOnce(this.mapModel, "change:interactions", function () { if (view.listeningForClicks) { @@ -511,7 +512,7 @@

Source: src/js/views/maps/DrawToolView.js

this.removeLayer(); this.removeClickListeners(); }, - } + }, ); return DrawTool; diff --git a/docs/docs/src_js_views_maps_FeatureInfoView.js.html b/docs/docs/src_js_views_maps_FeatureInfoView.js.html index 650f3c2ad..ad8ff8122 100644 --- a/docs/docs/src_js_views_maps_FeatureInfoView.js.html +++ b/docs/docs/src_js_views_maps_FeatureInfoView.js.html @@ -44,204 +44,198 @@

Source: src/js/views/maps/FeatureInfoView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/Feature',
-    'text!templates/maps/feature-info/feature-info.html'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    Feature,
-    Template
-  ) {
-
-    /**
-    * @class FeatureInfoView
-    * @classdesc An info-box / panel that shows more details about a specific geo-spatial
-    * feature that is highlighted or in focus in a Map View, as specified by a given
-    * {@link Feature} model. The format and content of the info-box varies based on which
-    * template is configured in the parent {@link MapAsset} model, but at a minimum a link
-    * is included that opens the associated {@link LayerInfoView}. Unless otherwise
-    * configured, the title of the panel will use the value of the feature's 'name',
-    * 'title', 'id', 'identifier', or 'assetId' property, if it has one (case
-    * insensitive).
-    * @classcategory Views/Maps
-    * @name FeatureInfoView
-    * @extends Backbone.View
-    * @screenshot views/maps/FeatureInfoView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var FeatureInfoView = Backbone.View.extend(
-      /** @lends FeatureInfoView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'FeatureInfoView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'feature-info',
-
-        /**
-        * The model that this view uses
-        * @type {Feature}
-        */
-        model: undefined,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * A ContentTemplate object specifies a single template designed to render
-         * information about the Feature.
-         * @typedef {Object} ContentTemplate
-         * @since 2.19.0
-         * @property {string} [name] - An identifier for this template.
-         * @property {string[]} [options] - The list of keys (option names) that are
-         * allowed for the given template. Only options with these keys will be passed to
-         * the underscore.js template, regardless of what is configured in the
-         * {@link MapConfig#FeatureTemplate}. When no options are specified, then the
-         * entire Feature model will be passed to the template as JSON.
-         * @property {string} template - The path to the HTML template. This will be used
-         * with require() to load the template as needed.
-         */
-
-        /**
-         * The list of available templates that format information about the Feature. The
-         * last template in the list is the default template. It will be used when a
-         * matching template is not found or one is not specified.
-         * @type {ContentTemplate[]}
-         * @since 2.19.0
-         */
-        contentTemplates: [
-          {
-            name: 'story',
-            template: 'text!templates/maps/feature-info/story.html',
-            options: ['title', 'subtitle', 'description', 'thumbnail', 'url', 'urlText']
-          },
-          {
-            name: 'table',
-            template: 'text!templates/maps/feature-info/table.html'
-          }
-        ],
-
-        /**
-        * Creates an object that gives the events this view will listen to and the
-        * associated function to call. Each entry in the object has the format 'event
-        * selector': 'function'.
-        * @returns {Object}
-        */
-        events: function () {
-          var events = {};
-          // Close the layer details panel when the toggle button is clicked. Get the
-          // class of the toggle button from the classes property set in this view.
-          events['click .' + this.classes.toggle] = 'close'
-          // Open the Layer Details panel
-          events['click .' + this.classes.layerDetailsButton] = 'showLayerDetails'
-          events['click .' + this.classes.zoomButton] = 'zoomToFeature'
-          return events
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/Feature",
+  "text!templates/maps/feature-info/feature-info.html",
+], function ($, _, Backbone, Feature, Template) {
+  /**
+   * @class FeatureInfoView
+   * @classdesc An info-box / panel that shows more details about a specific geo-spatial
+   * feature that is highlighted or in focus in a Map View, as specified by a given
+   * {@link Feature} model. The format and content of the info-box varies based on which
+   * template is configured in the parent {@link MapAsset} model, but at a minimum a link
+   * is included that opens the associated {@link LayerInfoView}. Unless otherwise
+   * configured, the title of the panel will use the value of the feature's 'name',
+   * 'title', 'id', 'identifier', or 'assetId' property, if it has one (case
+   * insensitive).
+   * @classcategory Views/Maps
+   * @name FeatureInfoView
+   * @extends Backbone.View
+   * @screenshot views/maps/FeatureInfoView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var FeatureInfoView = Backbone.View.extend(
+    /** @lends FeatureInfoView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "FeatureInfoView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "feature-info",
+
+      /**
+       * The model that this view uses
+       * @type {Feature}
+       */
+      model: undefined,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * A ContentTemplate object specifies a single template designed to render
+       * information about the Feature.
+       * @typedef {Object} ContentTemplate
+       * @since 2.19.0
+       * @property {string} [name] - An identifier for this template.
+       * @property {string[]} [options] - The list of keys (option names) that are
+       * allowed for the given template. Only options with these keys will be passed to
+       * the underscore.js template, regardless of what is configured in the
+       * {@link MapConfig#FeatureTemplate}. When no options are specified, then the
+       * entire Feature model will be passed to the template as JSON.
+       * @property {string} template - The path to the HTML template. This will be used
+       * with require() to load the template as needed.
+       */
+
+      /**
+       * The list of available templates that format information about the Feature. The
+       * last template in the list is the default template. It will be used when a
+       * matching template is not found or one is not specified.
+       * @type {ContentTemplate[]}
+       * @since 2.19.0
+       */
+      contentTemplates: [
+        {
+          name: "story",
+          template: "text!templates/maps/feature-info/story.html",
+          options: [
+            "title",
+            "subtitle",
+            "description",
+            "thumbnail",
+            "url",
+            "urlText",
+          ],
         },
-
-        /**
-         * Classes that are used to identify the HTML elements that comprise this view.
-         * @type {Object}
-         * @property {string} open The class to add to the outermost HTML element for this
-         * view when the layer details view is open/expanded (not hidden)
-         * @property {string} toggle The element in the template that acts as a toggle to
-         * close/hide the info view
-         * @property {string} layerDetailsButton The layer details button is added to the
-         * view when the selected feature is associated with a layer (a MapAsset like a 3D
-         * tileset). When clicked, it opens the LayerDetailsView for that layer.
-         * @property {string} contentContainer The iframe that holds the content rendered
-         * by the {@link FeatureInfoView#ContentTemplate}
-         * @property {string} title The label/title at the very top of the Feature panel,
-         * next to the close button.
-         */
-        classes: {
-          open: 'feature-info--open',
-          toggle: 'feature-info__toggle',
-          layerDetailsButton: 'feature-info__layer-details-button',
-          zoomButton: 'feature-info__zoom-button',
-          contentContainer: 'feature-info__content',
-          title: 'feature-info__label'
+        {
+          name: "table",
+          template: "text!templates/maps/feature-info/table.html",
         },
-
-        /**
-         * Whether or not the layer details view is open
-         * @type {Boolean}
-         */
-        isOpen: false,
-
-        /**
-        * Executed when a new FeatureInfoView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+      ],
+
+      /**
+       * Creates an object that gives the events this view will listen to and the
+       * associated function to call. Each entry in the object has the format 'event
+       * selector': 'function'.
+       * @returns {Object}
+       */
+      events: function () {
+        var events = {};
+        // Close the layer details panel when the toggle button is clicked. Get the
+        // class of the toggle button from the classes property set in this view.
+        events["click ." + this.classes.toggle] = "close";
+        // Open the Layer Details panel
+        events["click ." + this.classes.layerDetailsButton] =
+          "showLayerDetails";
+        events["click ." + this.classes.zoomButton] = "zoomToFeature";
+        return events;
+      },
+
+      /**
+       * Classes that are used to identify the HTML elements that comprise this view.
+       * @type {Object}
+       * @property {string} open The class to add to the outermost HTML element for this
+       * view when the layer details view is open/expanded (not hidden)
+       * @property {string} toggle The element in the template that acts as a toggle to
+       * close/hide the info view
+       * @property {string} layerDetailsButton The layer details button is added to the
+       * view when the selected feature is associated with a layer (a MapAsset like a 3D
+       * tileset). When clicked, it opens the LayerDetailsView for that layer.
+       * @property {string} contentContainer The iframe that holds the content rendered
+       * by the {@link FeatureInfoView#ContentTemplate}
+       * @property {string} title The label/title at the very top of the Feature panel,
+       * next to the close button.
+       */
+      classes: {
+        open: "feature-info--open",
+        toggle: "feature-info__toggle",
+        layerDetailsButton: "feature-info__layer-details-button",
+        zoomButton: "feature-info__zoom-button",
+        contentContainer: "feature-info__content",
+        title: "feature-info__label",
+      },
+
+      /**
+       * Whether or not the layer details view is open
+       * @type {Boolean}
+       */
+      isOpen: false,
+
+      /**
+       * Executed when a new FeatureInfoView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-          } catch (e) {
-            console.log('A FeatureInfoView failed to initialize. Error message: ' + e);
+          }
+        } catch (e) {
+          console.log(
+            "A FeatureInfoView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {FeatureInfoView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          const view = this;
+          const classes = view.classes;
+
+          // Show the feature info box as open if the view is set to have it open
+          // already
+          if (view.isOpen) {
+            view.el.classList.add(view.classes.open);
           }
 
-        },
-
-        /**
-        * Renders this view
-        * @return {FeatureInfoView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            const view = this
-            const classes = view.classes
-
-            // Show the feature info box as open if the view is set to have it open
-            // already
-            if (view.isOpen) {
-              view.el.classList.add(view.classes.open);
-            }
-
-            // Insert the principal template into the view
-            view.$el.html(view.template({
-              classes: classes
-            }));
+          // Insert the principal template into the view
+          view.$el.html(
+            view.template({
+              classes: classes,
+            }),
+          );
 
-            const iFrame = view.el
-              .querySelector('.' + classes.contentContainer);
+          const iFrame = view.el.querySelector("." + classes.contentContainer);
 
-            // Select the iFrame
-            const iFrameDoc = iFrame
-              .contentWindow
-              .document
+          // Select the iFrame
+          const iFrameDoc = iFrame.contentWindow.document;
 
-            // Add a script that gets all of the CSS stylesheets from the parent and
-            // applies them within the iFrame. Create a div within the iFrame to hold the
-            // feature info template content.
-            iFrameDoc.open();
-            iFrameDoc.write(`
+          // Add a script that gets all of the CSS stylesheets from the parent and
+          // applies them within the iFrame. Create a div within the iFrame to hold the
+          // feature info template content.
+          iFrameDoc.open();
+          iFrameDoc.write(`
               <div id="content"></div>
               <script type="text/javascript">
               window.onload = function() {
@@ -263,404 +257,406 @@ 

Source: src/js/views/maps/FeatureInfoView.js

} </style> `); - iFrameDoc.close(); - - // Identify the elements from the template that will be updated when the - // Feature model changes - view.elements = { - title: view.el.querySelector('.' + classes.title), - iFrame: iFrame, - iFrameContentContainer: iFrameDoc.getElementById('content'), - layerDetailsButton: view.el.querySelector('.' + classes.layerDetailsButton), - zoomButton: view.el.querySelector('.' + classes.zoomButton), - } - - view.update(); - - // Ensure the view's main element has the given class name - view.el.classList.add(view.className); - - // When the model changes, update the view - view.stopListening(view.model, 'change') - view.listenTo(view.model, 'change', view.update) - - return view - - } - catch (error) { - console.log( - 'There was an error rendering a FeatureInfoView' + - '. Error details: ' + error - ); + iFrameDoc.close(); + + // Identify the elements from the template that will be updated when the + // Feature model changes + view.elements = { + title: view.el.querySelector("." + classes.title), + iFrame: iFrame, + iFrameContentContainer: iFrameDoc.getElementById("content"), + layerDetailsButton: view.el.querySelector( + "." + classes.layerDetailsButton, + ), + zoomButton: view.el.querySelector("." + classes.zoomButton), + }; + + view.update(); + + // Ensure the view's main element has the given class name + view.el.classList.add(view.className); + + // When the model changes, update the view + view.stopListening(view.model, "change"); + view.listenTo(view.model, "change", view.update); + + return view; + } catch (error) { + console.log( + "There was an error rendering a FeatureInfoView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Updates the view with information from the current Feature model + */ + updateContent: function () { + try { + const view = this; + + // Elements to update + const title = this.getFeatureTitle(); + const iFrame = this.elements.iFrame; + const iFrameDiv = this.elements.iFrameContentContainer; + const layerDetailsButton = this.elements.layerDetailsButton; + const zoomButton = this.elements.zoomButton; + const mapAsset = this.model.get("mapAsset"); + let mapAssetLabel = mapAsset ? mapAsset.get("label") : null; + const layerButtonDisplay = mapAsset ? null : "none"; + const layerButtonText = "See " + mapAssetLabel + " layer details"; + // The Cesium Map Widget can't zoom to Cesium3DTileFeatures, so for now, hide + // the 'zoom to feature' button + const zoomButtonDisplay = + !mapAsset || mapAsset.get("type") === "Cesium3DTileset" + ? "none" + : null; + + // Insert the title into the title element + this.elements.title.innerHTML = title; + + // Update the iFrame content + this.getContent().then(function (html) { + iFrameDiv.innerHTML = html; + iFrame.style.height = 0; + iFrame.style.opacity = 0; + // Not the ideal solution, but check the height of the iFrame + // again after some time to allow external content to load. This + // is necessary for content that loads asynchronously, like + // images. Difficult to set listeners for this, since the content + // may be from a different domain. + setTimeout(function () { + view.updateIFrameHeight(); + }, 500); + }); + + // Show or hide the layer details button, update the text + layerDetailsButton.style.display = layerButtonDisplay; + layerDetailsButton.innerText = layerButtonText; + + // Show or hide the zoom to feature button + zoomButton.style.display = zoomButtonDisplay; + } catch (error) { + console.log( + "There was an error rendering the content of a FeatureInfoView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Update the height of the iFrame to match the height of the content + * within it. + * @since 2.27.0 + */ + updateIFrameHeight: function () { + const iFrame = this.elements?.iFrame; + // 336 includes the maximum height of the top bars, bottom padding, and other + // content of feature info like label on top and buttons at the bottom. This is + // only an estimate and a temporary approach. Eventually we should move away + // from iFrame so that css layout can figure this out without us doing math + // here. + const maxHeight = window.innerHeight - 336; + const height = Math.min( + maxHeight, + iFrame.contentWindow.document.getElementById("content").scrollHeight, + ); + iFrame.style.height = height / 16 + "rem"; + iFrame.style.opacity = 1; + }, + + /** + * Get the inner HTML content to insert into the iFrame. The content will vary + * based on the feature and if there is a template set on the parent Map Asset + * model. + * @since 2.19.0 + * @returns {Promise|null} Returns a promise that resolves to the content HTML + * when ready, otherwise null + */ + getContent: function () { + try { + let content = null; + let templateOptions = this.model.toJSON(); + const mapAsset = this.model.get("mapAsset"); + const featureProperties = this.model.get("properties"); + const templateConfig = mapAsset + ? mapAsset.get("featureTemplate") + : null; + const propertyMap = templateConfig ? templateConfig.options : {}; + const templateName = templateConfig ? templateConfig.template : null; + const contentTemplates = this.contentTemplates; + + // Given the name of a template configured in the MapAsset model, find the + // matching template from the contentTemplates set on this view + let contentTemplate = contentTemplates.find( + (template) => template.name == templateName, + ); + if (!contentTemplate) { + contentTemplate = contentTemplates[contentTemplates.length - 1]; } - }, - /** - * Updates the view with information from the current Feature model - */ - updateContent: function () { - - try { - - const view = this; - - // Elements to update - const title = this.getFeatureTitle() - const iFrame = this.elements.iFrame - const iFrameDiv = this.elements.iFrameContentContainer - const layerDetailsButton = this.elements.layerDetailsButton - const zoomButton = this.elements.zoomButton - const mapAsset = this.model.get('mapAsset') - let mapAssetLabel = mapAsset ? mapAsset.get('label') : null - const layerButtonDisplay = mapAsset ? null : 'none' - const layerButtonText = 'See ' + mapAssetLabel + ' layer details' - // The Cesium Map Widget can't zoom to Cesium3DTileFeatures, so for now, hide - // the 'zoom to feature' button - const zoomButtonDisplay = - (!mapAsset || mapAsset.get('type') === 'Cesium3DTileset') ? 'none' : null; - - // Insert the title into the title element - this.elements.title.innerHTML = title - - // Update the iFrame content - this.getContent().then(function (html) { - iFrameDiv.innerHTML = html; - iFrame.style.height = 0; - iFrame.style.opacity = 0; - // Not the ideal solution, but check the height of the iFrame - // again after some time to allow external content to load. This - // is necessary for content that loads asynchronously, like - // images. Difficult to set listeners for this, since the content - // may be from a different domain. - setTimeout(function () { - view.updateIFrameHeight(); - }, 500); - }) - - // Show or hide the layer details button, update the text - layerDetailsButton.style.display = layerButtonDisplay - layerDetailsButton.innerText = layerButtonText - - // Show or hide the zoom to feature button - zoomButton.style.display = zoomButtonDisplay - - } - catch (error) { - console.log( - 'There was an error rendering the content of a FeatureInfoView' + - '. Error details: ' + error - ); + // To get variables to pass to the template, there must be properties set on + // the feature and the selected content template must accept options + if ( + contentTemplate && + contentTemplate.options && + templateConfig && + templateConfig.options + ) { + templateOptions = {}; + contentTemplate.options.forEach(function (prop) { + const key = propertyMap[prop]; + templateOptions[prop] = featureProperties[key] || ""; + }); } - }, - - /** - * Update the height of the iFrame to match the height of the content - * within it. - * @since 2.27.0 - */ - updateIFrameHeight: function () { - const iFrame = this.elements?.iFrame; - // 336 includes the maximum height of the top bars, bottom padding, and other - // content of feature info like label on top and buttons at the bottom. This is - // only an estimate and a temporary approach. Eventually we should move away - // from iFrame so that css layout can figure this out without us doing math - // here. - const maxHeight = window.innerHeight - 336; - const height = Math.min(maxHeight, - iFrame.contentWindow.document.getElementById("content").scrollHeight); - iFrame.style.height = height / 16 + "rem"; - iFrame.style.opacity = 1; - }, - - /** - * Get the inner HTML content to insert into the iFrame. The content will vary - * based on the feature and if there is a template set on the parent Map Asset - * model. - * @since 2.19.0 - * @returns {Promise|null} Returns a promise that resolves to the content HTML - * when ready, otherwise null - */ - getContent: function () { - try { - - let content = null; - let templateOptions = this.model.toJSON(); - const mapAsset = this.model.get('mapAsset') - const featureProperties = this.model.get('properties') - const templateConfig = mapAsset ? mapAsset.get('featureTemplate') : null - const propertyMap = templateConfig ? templateConfig.options : {} - const templateName = templateConfig ? templateConfig.template : null; - const contentTemplates = this.contentTemplates; - - // Given the name of a template configured in the MapAsset model, find the - // matching template from the contentTemplates set on this view - let contentTemplate = contentTemplates.find( - template => template.name == templateName - ); - if (!contentTemplate) { - contentTemplate = contentTemplates[contentTemplates.length - 1]; - } - // To get variables to pass to the template, there must be properties set on - // the feature and the selected content template must accept options - if ( - contentTemplate && contentTemplate.options && - templateConfig && templateConfig.options - ) { - templateOptions = {} - contentTemplate.options.forEach(function (prop) { - const key = propertyMap[prop] - templateOptions[prop] = featureProperties[key] || '' - }) + // Return a promise that resolves to the content HTML + return new Promise(function (resolve, reject) { + if (contentTemplate) { + require([contentTemplate.template], function (template) { + content = _.template(template)(templateOptions); + resolve(content); + }); + } else { + resolve(null); } - - // Return a promise that resolves to the content HTML - return new Promise(function (resolve, reject) { - if (contentTemplate) { - require([contentTemplate.template], function (template) { - content = _.template(template)(templateOptions); - resolve(content); - }) - } else { - resolve(null); - } - }) - - } - catch (error) { - console.log( - 'There was an error getting the content of a FeatureInfoView' + - '. Error details: ' + error - ); - } - }, - - /** - * Create a title for the feature info box - * @since 2.19.0 - * @returns {string} The title for the feature info box - */ - getFeatureTitle: function () { - try { - let title = ''; - let suffix = ''; - - if (this.model) { - - // Get the layer/mapAsset model - const mapAsset = this.model.get('mapAsset') - - const featureTemplate = mapAsset ? mapAsset.get('featureTemplate') : null; - const properties = this.model.get('properties') ?? {}; - const assetName = mapAsset ? mapAsset.get('label') : null; - let name = featureTemplate ? properties[featureTemplate.label] : this.model.get('label'); - - // Build a title if the feature has no label. Check if the feature has a name, - // title, ID, or identifier property. Search for these properties independent - // of case. If none of these properties exist, use the feature ID provided by - // the model. - if (!name) { - - title = 'Feature'; - - let searchKeys = ['name', 'title', 'id', 'identifier'] - searchKeys = searchKeys.map(key => key.toLowerCase()); - const propKeys = Object.keys(properties) - const propKeysLower = propKeys.map(key => key.toLowerCase()); - - // Search by search key, since search keys are in order of preference. Find - // the first matching key. - const nameKeyLower = searchKeys.find(function (searchKey) { - return propKeysLower.includes(searchKey) - }); - - // Then figure out which of the original property keys matches (we need it - // in the original case). - const nameKey = propKeys[propKeysLower.indexOf(nameKeyLower)] - - name = properties[nameKey] ?? this.model.get('featureID'); - - if (assetName) { - suffix = ' from ' + assetName + ' Layer' - } - - } - if (name) { - title = title + ' ' + name - } - if (suffix) { - title = title + suffix + }); + } catch (error) { + console.log( + "There was an error getting the content of a FeatureInfoView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Create a title for the feature info box + * @since 2.19.0 + * @returns {string} The title for the feature info box + */ + getFeatureTitle: function () { + try { + let title = ""; + let suffix = ""; + + if (this.model) { + // Get the layer/mapAsset model + const mapAsset = this.model.get("mapAsset"); + + const featureTemplate = mapAsset + ? mapAsset.get("featureTemplate") + : null; + const properties = this.model.get("properties") ?? {}; + const assetName = mapAsset ? mapAsset.get("label") : null; + let name = featureTemplate + ? properties[featureTemplate.label] + : this.model.get("label"); + + // Build a title if the feature has no label. Check if the feature has a name, + // title, ID, or identifier property. Search for these properties independent + // of case. If none of these properties exist, use the feature ID provided by + // the model. + if (!name) { + title = "Feature"; + + let searchKeys = ["name", "title", "id", "identifier"]; + searchKeys = searchKeys.map((key) => key.toLowerCase()); + const propKeys = Object.keys(properties); + const propKeysLower = propKeys.map((key) => key.toLowerCase()); + + // Search by search key, since search keys are in order of preference. Find + // the first matching key. + const nameKeyLower = searchKeys.find(function (searchKey) { + return propKeysLower.includes(searchKey); + }); + + // Then figure out which of the original property keys matches (we need it + // in the original case). + const nameKey = propKeys[propKeysLower.indexOf(nameKeyLower)]; + + name = properties[nameKey] ?? this.model.get("featureID"); + + if (assetName) { + suffix = " from " + assetName + " Layer"; } - } - - // Do some basic sanitization of the title - title = title.replace(/&/g, '&amp;') - title = title.replace(/</g, '&lt;') - title = title.replace(/>/g, '&gt;') - title = title.replace(/"/g, '&quot;') - title = title.replace(/'/g, '&#039;') - - return title - } - catch (error) { - console.log( - 'There was an error making a title for the FeatureInfoView' + - '. Error details: ' + error - ); - return 'Feature' - } - }, - - /** - * Show details about the layer that contains this feature. The function does this - * by setting the associated layer model's 'selected' attribute to true. The - * parent Map view has a listener set to show the Layer Details view when this - * attribute is changed. - */ - showLayerDetails: function () { - try { - if (this.model && this.model.get('mapAsset')) { - this.model.get('mapAsset').set('selected', true) + if (name) { + title = title + " " + name; } - } - catch (error) { - console.log( - 'There was an error showing the layer details panel from a FeatureInfoView' + - '. Error details: ' + error - ); - } - }, - - /** - * Trigger an event from the parent Map model that tells the Map Widget to - * zoom to the full extent of this feature in the map. Also make sure that the Map - * Asset layer is visible in the map. - */ - zoomToFeature: function () { - try { - const model = this.model; - const mapAsset = model ? model.get('mapAsset') : false; - if (mapAsset) { - mapAsset.zoomTo(model) + if (suffix) { + title = title + suffix; } } - catch (error) { - console.log( - 'There was an error zooming to a feature from a FeatureInfoView' + - '. Error details: ' + error - ); - } - }, - /** - * Shows the feature info box - */ - open: function () { - try { - this.el.classList.add(this.classes.open); - this.isOpen = true; - } - catch (error) { - console.log( - 'There was an error showing the FeatureInfoView' + - '. Error details: ' + error - ); + // Do some basic sanitization of the title + title = title.replace(/&/g, "&amp;"); + title = title.replace(/</g, "&lt;"); + title = title.replace(/>/g, "&gt;"); + title = title.replace(/"/g, "&quot;"); + title = title.replace(/'/g, "&#039;"); + + return title; + } catch (error) { + console.log( + "There was an error making a title for the FeatureInfoView" + + ". Error details: " + + error, + ); + return "Feature"; + } + }, + + /** + * Show details about the layer that contains this feature. The function does this + * by setting the associated layer model's 'selected' attribute to true. The + * parent Map view has a listener set to show the Layer Details view when this + * attribute is changed. + */ + showLayerDetails: function () { + try { + if (this.model && this.model.get("mapAsset")) { + this.model.get("mapAsset").set("selected", true); } - }, - - /** - * Hide the feature info box from view - */ - close: function () { - try { - this.el.classList.remove(this.classes.open); - this.isOpen = false; - // When the feature info panel is closed, remove the Feature model from the - // Features collection. This will trigger the map widget to remove - // highlighting from the feature. - if (this.model && this.model.collection) { - this.model.collection.remove(this.model); - } + } catch (error) { + console.log( + "There was an error showing the layer details panel from a FeatureInfoView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Trigger an event from the parent Map model that tells the Map Widget to + * zoom to the full extent of this feature in the map. Also make sure that the Map + * Asset layer is visible in the map. + */ + zoomToFeature: function () { + try { + const model = this.model; + const mapAsset = model ? model.get("mapAsset") : false; + if (mapAsset) { + mapAsset.zoomTo(model); } - catch (error) { - console.log( - 'There was an error hiding the FeatureInfoView' + - '. Error details: ' + error - ); + } catch (error) { + console.log( + "There was an error zooming to a feature from a FeatureInfoView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Shows the feature info box + */ + open: function () { + try { + this.el.classList.add(this.classes.open); + this.isOpen = true; + } catch (error) { + console.log( + "There was an error showing the FeatureInfoView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Hide the feature info box from view + */ + close: function () { + try { + this.el.classList.remove(this.classes.open); + this.isOpen = false; + // When the feature info panel is closed, remove the Feature model from the + // Features collection. This will trigger the map widget to remove + // highlighting from the feature. + if (this.model && this.model.collection) { + this.model.collection.remove(this.model); } - }, - - /** - * Update the content that's displayed in a feature info box, based on the - * information in the Feature model. Open the panel if there is a Feature model, - * or close it if there is no model or the model has only default values. - */ - update: function () { - try { - if (!this.model || this.model.isDefault()) { - if (this.isOpen) { - this.close() - } - } else { - this.open() - this.updateContent() + } catch (error) { + console.log( + "There was an error hiding the FeatureInfoView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Update the content that's displayed in a feature info box, based on the + * information in the Feature model. Open the panel if there is a Feature model, + * or close it if there is no model or the model has only default values. + */ + update: function () { + try { + if (!this.model || this.model.isDefault()) { + if (this.isOpen) { + this.close(); } + } else { + this.open(); + this.updateContent(); } - catch (error) { - console.log( - 'There was an error updating the content of a FeatureInfoView' + - '. Error details: ' + error - ); - } - }, - - /** - * Stops listening to the previously set model, replaces it with a new Feature - * model, re-sets the listeners and re-renders the content in this view based on - * the new model. - * @param {Feature} newModel The new Feature model to display content for - */ - changeModel: function (newModel) { - - const view = this - const currentModel = view.model - const currentMapAsset = currentModel ? currentModel.get('mapAsset') : null; - - // Stop listening to the current Feature & Map Asset models before they're removed - view.stopListening(currentModel, 'change') - if (currentMapAsset) { - view.stopListening(currentMapAsset, 'change:visible') - } - - // Update the model - view.model = newModel - // Listen to the new model - view.stopListening(newModel, 'change') - view.listenTo(newModel, 'change', view.update) - - // If the Map Asset layer is ever hidden, then de-select the Feature and close - // the view view - const newMapAsset = newModel ? newModel.get('mapAsset') : null; - if (newMapAsset) { - view.listenTo(newMapAsset, 'change:visible', function () { - if (!newMapAsset.get('visible')) { - view.close() - } - }) - } - - // Update - view.update() + } catch (error) { + console.log( + "There was an error updating the content of a FeatureInfoView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Stops listening to the previously set model, replaces it with a new Feature + * model, re-sets the listeners and re-renders the content in this view based on + * the new model. + * @param {Feature} newModel The new Feature model to display content for + */ + changeModel: function (newModel) { + const view = this; + const currentModel = view.model; + const currentMapAsset = currentModel + ? currentModel.get("mapAsset") + : null; + + // Stop listening to the current Feature & Map Asset models before they're removed + view.stopListening(currentModel, "change"); + if (currentMapAsset) { + view.stopListening(currentMapAsset, "change:visible"); } - } - ); + // Update the model + view.model = newModel; + // Listen to the new model + view.stopListening(newModel, "change"); + view.listenTo(newModel, "change", view.update); + + // If the Map Asset layer is ever hidden, then de-select the Feature and close + // the view view + const newMapAsset = newModel ? newModel.get("mapAsset") : null; + if (newMapAsset) { + view.listenTo(newMapAsset, "change:visible", function () { + if (!newMapAsset.get("visible")) { + view.close(); + } + }); + } - return FeatureInfoView; + // Update + view.update(); + }, + }, + ); - } -); + return FeatureInfoView; +});
diff --git a/docs/docs/src_js_views_maps_HelpPanelView.js.html b/docs/docs/src_js_views_maps_HelpPanelView.js.html index e2b0870db..373103c9a 100644 --- a/docs/docs/src_js_views_maps_HelpPanelView.js.html +++ b/docs/docs/src_js_views_maps_HelpPanelView.js.html @@ -48,7 +48,7 @@

Source: src/js/views/maps/HelpPanelView.js

define(["backbone", "text!templates/maps/cesium-nav-help.html"], function ( Backbone, - NavHelpTemplate + NavHelpTemplate, ) { /** * @class MapHelpPanel @@ -95,7 +95,6 @@

Source: src/js/views/maps/HelpPanelView.js

this[option] = options[option]; } }); - }, /** @@ -193,7 +192,7 @@

Source: src/js/views/maps/HelpPanelView.js

sectionEl.innerHTML = `<h3 class="toolbar__content-header">${section.title}</h3> <div class="${contentContainerClass}"></div>`; const contentEl = sectionEl.querySelector( - "." + contentContainerClass + "." + contentContainerClass, ); renderMethod.call(view, contentEl); @@ -295,7 +294,7 @@

Source: src/js/views/maps/HelpPanelView.js

if (!buttonEl) return; buttonEl.classList.add("map-view__button--active"); }, - } + }, ); return MapHelpPanel; diff --git a/docs/docs/src_js_views_maps_LayerCategoryItemView.js.html b/docs/docs/src_js_views_maps_LayerCategoryItemView.js.html index 317d3d52f..6703d3f5d 100644 --- a/docs/docs/src_js_views_maps_LayerCategoryItemView.js.html +++ b/docs/docs/src_js_views_maps_LayerCategoryItemView.js.html @@ -44,185 +44,182 @@

Source: src/js/views/maps/LayerCategoryItemView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'text!templates/maps/layer-category-item.html',
-    'models/maps/AssetCategory',
-    'common/IconUtilities',
-    // Sub-views
-    'views/maps/LayerListView',
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    Template,
-    AssetCategory,
-    IconUtilities,
-    // Sub-views
-    LayerListView,
-  ) {
-    const BASE_CLASS = 'layer-category-item';
-    const CLASS_NAMES = {
-      metadata: `${BASE_CLASS}__metadata`,
-      icon: `${BASE_CLASS}__icon`,
-      expanded: `${BASE_CLASS}__expanded`,
-      collapsed: `${BASE_CLASS}__collapsed`,
-      layers: `${BASE_CLASS}__layers`,
-    };
-
-    /**
-    * @class LayerCategoryItemView
-    * @classdesc One item in a Category List: shows some basic information about the
-    * layer category, including label and icon. Also has a button that expands the
-    * nested layers list.
-    * @classcategory Views/Maps
-    * @name LayerCategoryItemView
-    * @extends Backbone.View
-    * @screenshot views/maps/LayerCategoryItemView.png
-    * @since 2.28.0
-    * @constructs
-    */
-    const LayerCategoryItemView = Backbone.View.extend(
-      /** @lends LayerCategoryItemView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerCategoryItemView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: BASE_CLASS,
-
-        /**
-        * The model that this view uses
-        * @type {AssetCategory}
-        */
-        model: undefined,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /** @inheritdoc */
-        events() {
-          return {[`click .${CLASS_NAMES.metadata}`]: 'toggleExpanded'};
-        },
-
-        /**
-        * Executed when a new LayerCategoryItemView is created
-        * @param {Object} options - A literal object with options to pass to the view
-        */
-        initialize(options) {
-          if (options?.model instanceof AssetCategory) {
-            this.model = options.model;
-          }
-        },
-
-        /**
-        * Renders this view
-        * @return {LayerCategoryItemView} Returns the rendered view element
-        */
-        render() {
-          if (!this.model) {
-            return;
-          }
-
-          // Insert the template into the view
-          this.$el.html(this.template({
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/maps/layer-category-item.html",
+  "models/maps/AssetCategory",
+  "common/IconUtilities",
+  // Sub-views
+  "views/maps/LayerListView",
+], function (
+  $,
+  _,
+  Backbone,
+  Template,
+  AssetCategory,
+  IconUtilities,
+  // Sub-views
+  LayerListView,
+) {
+  const BASE_CLASS = "layer-category-item";
+  const CLASS_NAMES = {
+    metadata: `${BASE_CLASS}__metadata`,
+    icon: `${BASE_CLASS}__icon`,
+    expanded: `${BASE_CLASS}__expanded`,
+    collapsed: `${BASE_CLASS}__collapsed`,
+    layers: `${BASE_CLASS}__layers`,
+  };
+
+  /**
+   * @class LayerCategoryItemView
+   * @classdesc One item in a Category List: shows some basic information about the
+   * layer category, including label and icon. Also has a button that expands the
+   * nested layers list.
+   * @classcategory Views/Maps
+   * @name LayerCategoryItemView
+   * @extends Backbone.View
+   * @screenshot views/maps/LayerCategoryItemView.png
+   * @since 2.28.0
+   * @constructs
+   */
+  const LayerCategoryItemView = Backbone.View.extend(
+    /** @lends LayerCategoryItemView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerCategoryItemView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: BASE_CLASS,
+
+      /**
+       * The model that this view uses
+       * @type {AssetCategory}
+       */
+      model: undefined,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /** @inheritdoc */
+      events() {
+        return { [`click .${CLASS_NAMES.metadata}`]: "toggleExpanded" };
+      },
+
+      /**
+       * Executed when a new LayerCategoryItemView is created
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize(options) {
+        if (options?.model instanceof AssetCategory) {
+          this.model = options.model;
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerCategoryItemView} Returns the rendered view element
+       */
+      render() {
+        if (!this.model) {
+          return;
+        }
+
+        // Insert the template into the view
+        this.$el.html(
+          this.template({
             classNames: CLASS_NAMES,
-            label: this.model.get('label'),
-          }));
-
-          // Insert the icon on the left
-          this.insertIcon();
-
-          this.layerListView = new LayerListView({
-            collection: this.model.get('mapAssets'),
-            isCategorized: true,
-          });
-          this.layerListView.render();
-          this.$(`.${CLASS_NAMES.layers}`).append(this.layerListView.el);
-
-          // Show the category as expanded or collapsed depending on the model
-          // properties.
-          this.updateLayerList();
-          this.listenTo(this.model, 'change:expanded', this.updateLayerList);
-
-          return this;
-        },
-
-        /**
-         * Inserts the icon before the label.
-         */
-        insertIcon() {
-          const icon = this.model.get('icon');
-          if (icon && typeof icon === 'string' && IconUtilities.isSVG(icon)) {
-            this.$(`.${CLASS_NAMES.icon}`).html(icon);
-          }
-        },
-
-        /**
-         * Sets the model's 'expanded' status attribute to true if it's false, and
-         * to false if it's true. Executed when a user clicks on this CategoryItem in a
-         * CategoryListView.
-         */
-        toggleExpanded() {
-          this.model.set('expanded', !this.model.get('expanded'));
-        },
-
-        /**
-         * Show or hide the layer list based on the category's expand status.
-         */
-        updateLayerList() {
-          const expanded = this.$(`.${CLASS_NAMES.expanded}`);
-          const collapsed = this.$(`.${CLASS_NAMES.collapsed}`);
-          const layers = this.$(`.${CLASS_NAMES.layers}`);
-          if (this.model.get('expanded')) {
-            expanded.show();
-            collapsed.hide();
-            layers.addClass('open');
-          } else {
-            expanded.hide();
-            collapsed.show();
-            layers.removeClass('open');
-          }
-        },
-
-        /**
-         * Searches and only displays self if layers match the text.
-         * @param {string} [text] - The search text from user input.
-         * @returns {boolean} - True if a layer item matches the text
-         */
-        search(text) {
-          const matched = this.layerListView.search(text);
-          if (matched) {
-            this.$el.show();
-            this.model.set('expanded', text !== '');
-          } else {
-            this.$el.hide();
-            this.model.set('expanded', false);
-          }
-          return matched;
-        },
-      }
-    );
-
-    return LayerCategoryItemView;
-  }
-);
+            label: this.model.get("label"),
+          }),
+        );
+
+        // Insert the icon on the left
+        this.insertIcon();
+
+        this.layerListView = new LayerListView({
+          collection: this.model.get("mapAssets"),
+          isCategorized: true,
+        });
+        this.layerListView.render();
+        this.$(`.${CLASS_NAMES.layers}`).append(this.layerListView.el);
+
+        // Show the category as expanded or collapsed depending on the model
+        // properties.
+        this.updateLayerList();
+        this.listenTo(this.model, "change:expanded", this.updateLayerList);
+
+        return this;
+      },
+
+      /**
+       * Inserts the icon before the label.
+       */
+      insertIcon() {
+        const icon = this.model.get("icon");
+        if (icon && typeof icon === "string" && IconUtilities.isSVG(icon)) {
+          this.$(`.${CLASS_NAMES.icon}`).html(icon);
+        }
+      },
+
+      /**
+       * Sets the model's 'expanded' status attribute to true if it's false, and
+       * to false if it's true. Executed when a user clicks on this CategoryItem in a
+       * CategoryListView.
+       */
+      toggleExpanded() {
+        this.model.set("expanded", !this.model.get("expanded"));
+      },
+
+      /**
+       * Show or hide the layer list based on the category's expand status.
+       */
+      updateLayerList() {
+        const expanded = this.$(`.${CLASS_NAMES.expanded}`);
+        const collapsed = this.$(`.${CLASS_NAMES.collapsed}`);
+        const layers = this.$(`.${CLASS_NAMES.layers}`);
+        if (this.model.get("expanded")) {
+          expanded.show();
+          collapsed.hide();
+          layers.addClass("open");
+        } else {
+          expanded.hide();
+          collapsed.show();
+          layers.removeClass("open");
+        }
+      },
+
+      /**
+       * Searches and only displays self if layers match the text.
+       * @param {string} [text] - The search text from user input.
+       * @returns {boolean} - True if a layer item matches the text
+       */
+      search(text) {
+        const matched = this.layerListView.search(text);
+        if (matched) {
+          this.$el.show();
+          this.model.set("expanded", text !== "");
+        } else {
+          this.$el.hide();
+          this.model.set("expanded", false);
+        }
+        return matched;
+      },
+    },
+  );
+
+  return LayerCategoryItemView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayerCategoryListView.js.html b/docs/docs/src_js_views_maps_LayerCategoryListView.js.html index 3c5a30830..e714e3a6f 100644 --- a/docs/docs/src_js_views_maps_LayerCategoryListView.js.html +++ b/docs/docs/src_js_views_maps_LayerCategoryListView.js.html @@ -44,101 +44,103 @@

Source: src/js/views/maps/LayerCategoryListView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'collections/maps/AssetCategories',
-    // Sub-views
-    'views/maps/LayerCategoryItemView',
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    AssetCategories,
-    // Sub-views
-    LayerCategoryItemView,
-  ) {
-
-    /**
-    * @class LayerCategoryListView
-    * @classdesc A LayerCategoryListView shows a collection of AssetCategories, each with
-    * a MapAssets collection nested under it.
-    * @classcategory Views/Maps
-    * @name LayerCategoryListView
-    * @screenshot views/maps/LayerCategoryListView.png
-    * @extends Backbone.View
-    * @since 2.28.0
-    * @constructs
-    */
-    const LayerCategoryListView = Backbone.View.extend(
-      /** @lends LayerCategoryListView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerCategoryListView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'layer-category-list',
-
-        /**
-        * The array of layer categories to display in the list
-        * @type {LayerCategoryItemView[]}
-        */
-        layerCategoryItemViews: undefined,
-
-        /**
-        * Executed when a new LayerCategoryListView is created
-        * @param {Object} options - A literal object with options to pass to the view
-        */
-        initialize(options) {
-          if (options.collection instanceof AssetCategories) {
-            this.layerCategoryItemViews = options.collection.map(categoryModel => {
-              return new LayerCategoryItemView({model: categoryModel});
-            });
-          }
-        },
-
-        /**
-        * Renders this view
-        * @return {LayerCategoryListView} Returns the rendered view element
-        */
-        render() {
-          this.layerCategoryItemViews = _.forEach(this.layerCategoryItemViews, layerCategoryItemView => {
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/maps/AssetCategories",
+  // Sub-views
+  "views/maps/LayerCategoryItemView",
+], function (
+  $,
+  _,
+  Backbone,
+  AssetCategories,
+  // Sub-views
+  LayerCategoryItemView,
+) {
+  /**
+   * @class LayerCategoryListView
+   * @classdesc A LayerCategoryListView shows a collection of AssetCategories, each with
+   * a MapAssets collection nested under it.
+   * @classcategory Views/Maps
+   * @name LayerCategoryListView
+   * @screenshot views/maps/LayerCategoryListView.png
+   * @extends Backbone.View
+   * @since 2.28.0
+   * @constructs
+   */
+  const LayerCategoryListView = Backbone.View.extend(
+    /** @lends LayerCategoryListView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerCategoryListView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "layer-category-list",
+
+      /**
+       * The array of layer categories to display in the list
+       * @type {LayerCategoryItemView[]}
+       */
+      layerCategoryItemViews: undefined,
+
+      /**
+       * Executed when a new LayerCategoryListView is created
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize(options) {
+        if (options.collection instanceof AssetCategories) {
+          this.layerCategoryItemViews = options.collection.map(
+            (categoryModel) => {
+              return new LayerCategoryItemView({ model: categoryModel });
+            },
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerCategoryListView} Returns the rendered view element
+       */
+      render() {
+        this.layerCategoryItemViews = _.forEach(
+          this.layerCategoryItemViews,
+          (layerCategoryItemView) => {
             layerCategoryItemView.render();
             this.el.appendChild(layerCategoryItemView.el);
-          });
-
-          return this;
-        },
-
-        /**
-         * Searches and only dispays categories and layers that match the text.
-         * @param {string} [text] - The search text from user input.
-         * @returns {boolean} - True if a layer item matches the text
-         */
-        search(text) {
-          return _.reduce(this.layerCategoryItemViews, (matched, layerCategoryItem) => {
+          },
+        );
+
+        return this;
+      },
+
+      /**
+       * Searches and only dispays categories and layers that match the text.
+       * @param {string} [text] - The search text from user input.
+       * @returns {boolean} - True if a layer item matches the text
+       */
+      search(text) {
+        return _.reduce(
+          this.layerCategoryItemViews,
+          (matched, layerCategoryItem) => {
             return layerCategoryItem.search(text) || matched;
-          }, false);
-        },
-      }
-    );
-
-    return LayerCategoryListView;
-
-  }
-);
+          },
+          false,
+        );
+      },
+    },
+  );
+
+  return LayerCategoryListView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayerDetailView.js.html b/docs/docs/src_js_views_maps_LayerDetailView.js.html index 58ae7d9ae..ed6e907dd 100644 --- a/docs/docs/src_js_views_maps_LayerDetailView.js.html +++ b/docs/docs/src_js_views_maps_LayerDetailView.js.html @@ -44,238 +44,224 @@

Source: src/js/views/maps/LayerDetailView.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/assets/MapAsset',
-    'text!templates/maps/layer-detail.html',
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    MapAsset,
-    Template
-  ) {
-
-    /**
-    * @class LayerDetailView
-    * @classdesc A LayerDetailView creates a section to be inserted into a
-    * LayerDetailsView. It renders a label and a toggle to collapse and expand the
-    * section's contents. The contents of the Detail section can be rendered by any other
-    * view, but the view should be one that shows details about a MapAsset or allows
-    * editing elements of the MapAsset.
-    * @classcategory Views/Maps
-    * @name LayerDetailView
-    * @extends Backbone.View
-    * @screenshot views/maps/LayerDetailView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var LayerDetailView = Backbone.View.extend(
-      /** @lends LayerDetailView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerDetailView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'layer-detail',
-
-        /**
-        * The model that this view uses
-        * @type {MapAsset}
-        */
-        model: undefined,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * CSS classes for HTML elements within this view.
-         * @property {string} toggle The element in the template that acts as a toggle to
-         * expand or collapse this Layer Detail section.
-         * @property {string} open The class to add to the view when the contents are
-         * visible (i.e. the section is expanded)
-         * @property {string} noHeader The class to add to the view when there is no
-         * title/label and the view is not collapsible.
-         * @property {string} label The element that holds the view's label text
-         * @property {string} contentContainer The container into which the contentView's
-         * rendered content will be placed
-         */
-        classes: {
-          open: 'layer-detail--open',
-          noHeader: 'layer-detail--no-header',
-          label: 'layer-detail__label',
-          toggle: 'layer-detail__toggle',
-          contentContainer: 'layer-detail__content'
-        },
-
-        /**
-         * Indicates whether this section is collapsed or expanded
-         * @type {Boolean}
-         */
-        isOpen: true,
-
-        /**
-         * The name to display for the Layer Detail section
-         * @type {string}
-         */
-        label: null,
-
-        /**
-         * The sub-view that will show details about, or allow editing of, the given Layer
-         * model. The contentView will be passed the Layer model.
-         * @type {Backbone.View}
-         */
-        contentView: null,
-
-        /**
-        * Creates an object that gives the events this view will listen to and the
-        * associated function to call. Each entry in the object has the format 'event
-        * selector': 'function'.
-        * @returns {Object}
-        */
-        events: function () {
-          var events = {};
-          // Collapse or expand this Detail section when the toggle button is clicked. Get
-          // the class of the toggle button from the classes property set in this view.
-          events['click .' + this.classes.toggle] = 'toggle'
-          return events
-        },
-
-        /**
-        * Executed when a new LayerDetailView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/assets/MapAsset",
+  "text!templates/maps/layer-detail.html",
+], function ($, _, Backbone, MapAsset, Template) {
+  /**
+   * @class LayerDetailView
+   * @classdesc A LayerDetailView creates a section to be inserted into a
+   * LayerDetailsView. It renders a label and a toggle to collapse and expand the
+   * section's contents. The contents of the Detail section can be rendered by any other
+   * view, but the view should be one that shows details about a MapAsset or allows
+   * editing elements of the MapAsset.
+   * @classcategory Views/Maps
+   * @name LayerDetailView
+   * @extends Backbone.View
+   * @screenshot views/maps/LayerDetailView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var LayerDetailView = Backbone.View.extend(
+    /** @lends LayerDetailView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerDetailView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "layer-detail",
+
+      /**
+       * The model that this view uses
+       * @type {MapAsset}
+       */
+      model: undefined,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * CSS classes for HTML elements within this view.
+       * @property {string} toggle The element in the template that acts as a toggle to
+       * expand or collapse this Layer Detail section.
+       * @property {string} open The class to add to the view when the contents are
+       * visible (i.e. the section is expanded)
+       * @property {string} noHeader The class to add to the view when there is no
+       * title/label and the view is not collapsible.
+       * @property {string} label The element that holds the view's label text
+       * @property {string} contentContainer The container into which the contentView's
+       * rendered content will be placed
+       */
+      classes: {
+        open: "layer-detail--open",
+        noHeader: "layer-detail--no-header",
+        label: "layer-detail__label",
+        toggle: "layer-detail__toggle",
+        contentContainer: "layer-detail__content",
+      },
+
+      /**
+       * Indicates whether this section is collapsed or expanded
+       * @type {Boolean}
+       */
+      isOpen: true,
+
+      /**
+       * The name to display for the Layer Detail section
+       * @type {string}
+       */
+      label: null,
+
+      /**
+       * The sub-view that will show details about, or allow editing of, the given Layer
+       * model. The contentView will be passed the Layer model.
+       * @type {Backbone.View}
+       */
+      contentView: null,
+
+      /**
+       * Creates an object that gives the events this view will listen to and the
+       * associated function to call. Each entry in the object has the format 'event
+       * selector': 'function'.
+       * @returns {Object}
+       */
+      events: function () {
+        var events = {};
+        // Collapse or expand this Detail section when the toggle button is clicked. Get
+        // the class of the toggle button from the classes property set in this view.
+        events["click ." + this.classes.toggle] = "toggle";
+        return events;
+      },
+
+      /**
+       * Executed when a new LayerDetailView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-          } catch (e) {
-            console.log('A LayerDetailView failed to initialize. Error message: ' + e);
+          }
+        } catch (e) {
+          console.log(
+            "A LayerDetailView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerDetailView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+
+          // Display the section's contents depending on the view's initial setting
+          if (this.isOpen) {
+            this.el.classList.add(this.classes.open);
           }
 
-        },
-
-        /**
-        * Renders this view
-        * @return {LayerDetailView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-            
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-
-            // Display the section's contents depending on the view's initial setting
-            if (this.isOpen) {
-              this.el.classList.add(this.classes.open);
-            }
-
-            // Insert the template into the view
-            this.$el.html(this.template({
+          // Insert the template into the view
+          this.$el.html(
+            this.template({
               label: this.label,
               collapsible: this.collapsible,
-              showTitle: this.showTitle
-            }));
-
-            // Render the content for this Layer Detail section
-            if (this.contentView) {
-              var contentContainer = this.el.querySelector(
-                '.' + this.classes.contentContainer
-              )
-              this.renderedContentView = new this.contentView({
-                model: this.model
-              })
-              contentContainer.append(this.renderedContentView.el)
-              this.renderedContentView.render()
-            }
-
-            if (!this.collapsible && !this.showTitle) {
-              this.el.classList.add(this.classes.noHeader);
-            }
-
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a LayerDetailView' +
-              '. Error details: ' + error
+              showTitle: this.showTitle,
+            }),
+          );
+
+          // Render the content for this Layer Detail section
+          if (this.contentView) {
+            var contentContainer = this.el.querySelector(
+              "." + this.classes.contentContainer,
             );
+            this.renderedContentView = new this.contentView({
+              model: this.model,
+            });
+            contentContainer.append(this.renderedContentView.el);
+            this.renderedContentView.render();
           }
-        },
-
-        /**
-         * Show or hide this section's contents by adding or removing the open class.
-         */
-        toggle : function(){
-          try {
-            this.el.classList.toggle(this.classes.open)
-            if (this.isOpen) {
-              this.isOpen = false
-            } else {
-              this.isOpen = true
-            }
-          }
-          catch (error) {
-            console.log(
-              'There was an error toggling a LayerDetailView' +
-              '. Error details: ' + error
-            );
+
+          if (!this.collapsible && !this.showTitle) {
+            this.el.classList.add(this.classes.noHeader);
           }
-        },
-
-        /**
-         * Perform clean-up functions when this view is about to be removed from the page
-         * or navigated away from.
-         */
-        onClose: function () {
-          try {
-            if (
-              this.renderedContentView &&
-              typeof this.renderedContentView.onClose === 'function'
-            ) {
-              this.renderedContentView.onClose()
-            }
+
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a LayerDetailView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Show or hide this section's contents by adding or removing the open class.
+       */
+      toggle: function () {
+        try {
+          this.el.classList.toggle(this.classes.open);
+          if (this.isOpen) {
+            this.isOpen = false;
+          } else {
+            this.isOpen = true;
           }
-          catch (error) {
-            console.log(
-              'There was an error performing clean up functions in a LayerDetailView' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error toggling a LayerDetailView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Perform clean-up functions when this view is about to be removed from the page
+       * or navigated away from.
+       */
+      onClose: function () {
+        try {
+          if (
+            this.renderedContentView &&
+            typeof this.renderedContentView.onClose === "function"
+          ) {
+            this.renderedContentView.onClose();
           }
+        } catch (error) {
+          console.log(
+            "There was an error performing clean up functions in a LayerDetailView" +
+              ". Error details: " +
+              error,
+          );
         }
+      },
+    },
+  );
 
-      }
-    );
-
-    return LayerDetailView;
-
-  }
-);
+  return LayerDetailView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayerDetailsView.js.html b/docs/docs/src_js_views_maps_LayerDetailsView.js.html index a2830c5b4..759c7bab9 100644 --- a/docs/docs/src_js_views_maps_LayerDetailsView.js.html +++ b/docs/docs/src_js_views_maps_LayerDetailsView.js.html @@ -44,382 +44,378 @@

Source: src/js/views/maps/LayerDetailsView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/assets/MapAsset',
-    'text!templates/maps/layer-details.html',
-    // Sub-Views
-    'views/maps/LayerDetailView',
-    'views/maps/LayerOpacityView',
-    'views/maps/LayerInfoView',
-    'views/maps/LayerNavigationView',
-    'views/maps/LegendView'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    MapAsset,
-    Template,
-    // Sub-Views
-    LayerDetailView,
-    LayerOpacityView,
-    LayerInfoView,
-    LayerNavigationView,
-    LegendView
-  ) {
-
-    /**
-    * @class LayerDetailsView
-    * @classdesc A panel with additional information about a Layer (a Map Asset like
-    * imagery or vector data), plus some UI for updating the appearance of the Layer on
-    * the map, such as the opacity.
-    * @classcategory Views/Maps
-    * @name LayerDetailsView
-    * @extends Backbone.View
-    * @screenshot views/maps/LayerDetailsView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var LayerDetailsView = Backbone.View.extend(
-      /** @lends LayerDetailsView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerDetailsView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'layer-details',
-
-        /**
-        * The MapAsset model that this view uses
-        * @type {MapAsset}
-        */
-        model: undefined,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * Classes that are used to identify the HTML elements that comprise this view.
-         * @type {Object}
-         * @property {string} open The class to add to the outermost HTML element for this
-         * view when the layer details view is open/expanded (not hidden)
-         * @property {string} toggle The element in the template that acts as a toggle to
-         * close/hide the details view
-         * @property {string} sections The container for all of the LayerDetailViews.
-         * @property {string} label The label element for the layer that displays a title
-         * in the header of the details view
-         * @property {string} notification The element that holds the notification message,
-         * if there is one. Inserted before all the details sections.
-         * @property {string} badge The class to add to the badge element that is shown
-         * when the layer has a notification message.
-         */
-        classes: {
-          open: 'layer-details--open',
-          toggle: 'layer-details__toggle',
-          sections: 'layer-details__sections',
-          label: 'layer-details__label',
-          notification: 'layer-details__notification',
-          badge: 'map-view__badge'
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/assets/MapAsset",
+  "text!templates/maps/layer-details.html",
+  // Sub-Views
+  "views/maps/LayerDetailView",
+  "views/maps/LayerOpacityView",
+  "views/maps/LayerInfoView",
+  "views/maps/LayerNavigationView",
+  "views/maps/LegendView",
+], function (
+  $,
+  _,
+  Backbone,
+  MapAsset,
+  Template,
+  // Sub-Views
+  LayerDetailView,
+  LayerOpacityView,
+  LayerInfoView,
+  LayerNavigationView,
+  LegendView,
+) {
+  /**
+   * @class LayerDetailsView
+   * @classdesc A panel with additional information about a Layer (a Map Asset like
+   * imagery or vector data), plus some UI for updating the appearance of the Layer on
+   * the map, such as the opacity.
+   * @classcategory Views/Maps
+   * @name LayerDetailsView
+   * @extends Backbone.View
+   * @screenshot views/maps/LayerDetailsView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var LayerDetailsView = Backbone.View.extend(
+    /** @lends LayerDetailsView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerDetailsView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "layer-details",
+
+      /**
+       * The MapAsset model that this view uses
+       * @type {MapAsset}
+       */
+      model: undefined,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * Classes that are used to identify the HTML elements that comprise this view.
+       * @type {Object}
+       * @property {string} open The class to add to the outermost HTML element for this
+       * view when the layer details view is open/expanded (not hidden)
+       * @property {string} toggle The element in the template that acts as a toggle to
+       * close/hide the details view
+       * @property {string} sections The container for all of the LayerDetailViews.
+       * @property {string} label The label element for the layer that displays a title
+       * in the header of the details view
+       * @property {string} notification The element that holds the notification message,
+       * if there is one. Inserted before all the details sections.
+       * @property {string} badge The class to add to the badge element that is shown
+       * when the layer has a notification message.
+       */
+      classes: {
+        open: "layer-details--open",
+        toggle: "layer-details__toggle",
+        sections: "layer-details__sections",
+        label: "layer-details__label",
+        notification: "layer-details__notification",
+        badge: "map-view__badge",
+      },
+
+      /**
+       * Configuration for a Layer Detail section to show within this Layer Details
+       * view.
+       * @typedef {Object} DetailSectionOption
+       * @property {string} label The name to display for this section
+       * @property {Backbone.View} view Any view that will render content for the Layer
+       * Detail section. This view will be passed the MapAsset model. The view should
+       * display information about the MapAsset and/or allow some aspect of the
+       * MapAsset's appearance to be edited - e.g. a LayerInfoView or a
+       * LayerOpacityView.
+       * @property {boolean} collapsible Whether or not this section should be
+       * expandable and collapsible.
+       * @property {boolean} showTitle Whether or not to show the title/label for this
+       * section.
+       * @property {boolean} hideIfError Set to true to hide this section when there is
+       * an error loading the layer. Example: we should hide the opacity slider for
+       * layers that are not visible on the map
+       */
+
+      /**
+       * A list of sections to render within this view that give details about the
+       * MapAsset, or allow editing of the MapAsset appearance. Each section will have a
+       * title and its content will be collapsible.
+       * @type {DetailSectionOption[]}
+       */
+      sections: [
+        {
+          label: "Navigation",
+          view: LayerNavigationView,
+          collapsible: false,
+          showTitle: false,
+          hideIfError: true,
         },
-
-        /**
-         * Configuration for a Layer Detail section to show within this Layer Details
-         * view.
-         * @typedef {Object} DetailSectionOption
-         * @property {string} label The name to display for this section
-         * @property {Backbone.View} view Any view that will render content for the Layer
-         * Detail section. This view will be passed the MapAsset model. The view should
-         * display information about the MapAsset and/or allow some aspect of the
-         * MapAsset's appearance to be edited - e.g. a LayerInfoView or a
-         * LayerOpacityView.
-         * @property {boolean} collapsible Whether or not this section should be
-         * expandable and collapsible.
-         * @property {boolean} showTitle Whether or not to show the title/label for this
-         * section.
-         * @property {boolean} hideIfError Set to true to hide this section when there is
-         * an error loading the layer. Example: we should hide the opacity slider for
-         * layers that are not visible on the map
-         */
-
-        /**
-         * A list of sections to render within this view that give details about the
-         * MapAsset, or allow editing of the MapAsset appearance. Each section will have a
-         * title and its content will be collapsible.
-         * @type {DetailSectionOption[]}
-         */
-        sections: [
-          {
-            label: 'Navigation',
-            view: LayerNavigationView,
-            collapsible: false,
-            showTitle: false,
-            hideIfError: true
-          },
-          {
-            label: "Legend",
-            view: LegendView,
-            collapsible: false,
-            showTitle: true,
-            hideIfError: true
-          },
-          {
-            label: 'Opacity',
-            view: LayerOpacityView,
-            collapsible: false,
-            showTitle: true,
-            hideIfError: true
-          },
-          {
-            label: 'Info & Data',
-            view: LayerInfoView,
-            collapsible: true,
-            showTitle: true,
-            hideIfError: false
-          }
-        ],
-
-        /**
-        * Creates an object that gives the events this view will listen to and the
-        * associated function to call. Each entry in the object has the format 'event
-        * selector': 'function'.
-        * @returns {Object}
-        */
-        events: function () {
-          var events = {};
-          // Close the layer details panel when the toggle button is clicked. Get the
-          // class of the toggle button from the classes property set in this view.
-          events['click .' + this.classes.toggle] = 'close'
-          return events
+        {
+          label: "Legend",
+          view: LegendView,
+          collapsible: false,
+          showTitle: true,
+          hideIfError: true,
         },
-
-        /**
-         * Whether or not the layer details view is open
-         * @type {Boolean}
-         */
-        isOpen: false,
-
-        /**
-        * Executed when a new LayerDetailsView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
-            }
-          } catch (e) {
-            console.log('A LayerDetailsView failed to initialize. Error message: ' + e);
-          }
-
+        {
+          label: "Opacity",
+          view: LayerOpacityView,
+          collapsible: false,
+          showTitle: true,
+          hideIfError: true,
         },
-
-        /**
-        * Renders this view
-        * @return {LayerDetailsView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-            var model = this.model;
-
-            // Show the layer details box as open if the view is set to have it open
-            // already
-            if (this.isOpen) {
-              this.el.classList.add(this.classes.open);
+        {
+          label: "Info & Data",
+          view: LayerInfoView,
+          collapsible: true,
+          showTitle: true,
+          hideIfError: false,
+        },
+      ],
+
+      /**
+       * Creates an object that gives the events this view will listen to and the
+       * associated function to call. Each entry in the object has the format 'event
+       * selector': 'function'.
+       * @returns {Object}
+       */
+      events: function () {
+        var events = {};
+        // Close the layer details panel when the toggle button is clicked. Get the
+        // class of the toggle button from the classes property set in this view.
+        events["click ." + this.classes.toggle] = "close";
+        return events;
+      },
+
+      /**
+       * Whether or not the layer details view is open
+       * @type {Boolean}
+       */
+      isOpen: false,
+
+      /**
+       * Executed when a new LayerDetailsView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
+          }
+        } catch (e) {
+          console.log(
+            "A LayerDetailsView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerDetailsView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+          var model = this.model;
+
+          // Show the layer details box as open if the view is set to have it open
+          // already
+          if (this.isOpen) {
+            this.el.classList.add(this.classes.open);
+          }
 
-            // Insert the template into the view
-            this.$el.html(this.template({
-              label: model ? model.get('label') || '' : ''
-            }));
-
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-
-            // Select elements in the template that we will need to manipulate
-            const sectionsContainer = this.el.querySelector('.' + this.classes.sections)
-            const labelEl = this.el.querySelector('.' + this.classes.label)
-
-            // Render each section in the Details panel
-            this.renderedSections = _.clone(this.sections)
-
-            this.renderedSections.forEach(function (section) {
-              var detailSection = new LayerDetailView({
-                label: section.label,
-                contentView: section.view,
-                model: model,
-                collapsible: section.collapsible,
-                showTitle: section.showTitle
-              })
-              sectionsContainer.append(detailSection.el)
-              detailSection.render()
-              // Hide the section if there is an error with the asset, and this section
-              // does make sense to show for a layer that can't be displayed
-              if (section.hideIfError && model) {
-                if (model && model.get('status') === 'error') {
-                  detailSection.el.style.display = 'none'
-                }
-              }
-              section.renderedView = detailSection
-            })
-
-            // Hide/show sections with the 'hideIfError' property when the status of the
-            // MapAsset changes
-            this.stopListening(model, 'change:status')
-            this.listenTo(model, 'change:status', function (model, status) {
-              const hideIfErrorSections = _.filter(this.renderedSections, function (section) {
-                return section.hideIfError
-              })
-              let displayProperty = ''
-              if (status === 'error') {
-                displayProperty = 'none'
+          // Insert the template into the view
+          this.$el.html(
+            this.template({
+              label: model ? model.get("label") || "" : "",
+            }),
+          );
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+
+          // Select elements in the template that we will need to manipulate
+          const sectionsContainer = this.el.querySelector(
+            "." + this.classes.sections,
+          );
+          const labelEl = this.el.querySelector("." + this.classes.label);
+
+          // Render each section in the Details panel
+          this.renderedSections = _.clone(this.sections);
+
+          this.renderedSections.forEach(function (section) {
+            var detailSection = new LayerDetailView({
+              label: section.label,
+              contentView: section.view,
+              model: model,
+              collapsible: section.collapsible,
+              showTitle: section.showTitle,
+            });
+            sectionsContainer.append(detailSection.el);
+            detailSection.render();
+            // Hide the section if there is an error with the asset, and this section
+            // does make sense to show for a layer that can't be displayed
+            if (section.hideIfError && model) {
+              if (model && model.get("status") === "error") {
+                detailSection.el.style.display = "none";
               }
-              hideIfErrorSections.forEach(function (section) {
-                section.renderedView.el.style.display = displayProperty
-              })
-            })
-
-            // If this layer has a notification, show the badge and notification
+            }
+            section.renderedView = detailSection;
+          });
+
+          // Hide/show sections with the 'hideIfError' property when the status of the
+          // MapAsset changes
+          this.stopListening(model, "change:status");
+          this.listenTo(model, "change:status", function (model, status) {
+            const hideIfErrorSections = _.filter(
+              this.renderedSections,
+              function (section) {
+                return section.hideIfError;
+              },
+            );
+            let displayProperty = "";
+            if (status === "error") {
+              displayProperty = "none";
+            }
+            hideIfErrorSections.forEach(function (section) {
+              section.renderedView.el.style.display = displayProperty;
+            });
+          });
+
+          // If this layer has a notification, show the badge and notification
+          // message
+          const notice = model ? model.get("notification") : null;
+          if (notice && (notice.message || notice.badge)) {
             // message
-            const notice = model ? model.get('notification') : null
-            if (notice && (notice.message || notice.badge)) {
-              // message
-              if (notice.message) {
-                const noticeEl = document.createElement('div')
-                noticeEl.classList.add(this.classes.notification)
-                noticeEl.innerText = notice.message
-                if (notice.style) {
-                  const badgeClass = this.classes.notification + '--' + notice.style
-                  noticeEl.classList.add(badgeClass)
-                }
-                sectionsContainer.prepend(noticeEl)
+            if (notice.message) {
+              const noticeEl = document.createElement("div");
+              noticeEl.classList.add(this.classes.notification);
+              noticeEl.innerText = notice.message;
+              if (notice.style) {
+                const badgeClass =
+                  this.classes.notification + "--" + notice.style;
+                noticeEl.classList.add(badgeClass);
               }
-              // badge
-              if (notice.badge) {
-                const badge = document.createElement('span')
-                badge.classList.add(this.classes.badge)
-                badge.innerText = notice.badge
-                if (notice.style) {
-                  const badgeClass = this.classes.badge + '--' + notice.style
-                  badge.classList.add(badgeClass)
-                }
-                labelEl.append(badge)
+              sectionsContainer.prepend(noticeEl);
+            }
+            // badge
+            if (notice.badge) {
+              const badge = document.createElement("span");
+              badge.classList.add(this.classes.badge);
+              badge.innerText = notice.badge;
+              if (notice.style) {
+                const badgeClass = this.classes.badge + "--" + notice.style;
+                badge.classList.add(badgeClass);
               }
-              
+              labelEl.append(badge);
             }
-
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a LayerDetailsView' +
-              '. Error details: ' + error
-            );
           }
-        },
 
-        /**
-         * Show/expand the Layer Details panel. Opening the panel also changes the
-         * MapAsset model's 'selected attribute' to true.
-         */
-        open: function () {
-          try {
-            this.el.classList.add(this.classes.open);
-            this.isOpen = true;
-            // Ensure that the model is marked as selected
-            if (this.model) {
-              this.model.set('selected', true)
-            }
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a LayerDetailsView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Show/expand the Layer Details panel. Opening the panel also changes the
+       * MapAsset model's 'selected attribute' to true.
+       */
+      open: function () {
+        try {
+          this.el.classList.add(this.classes.open);
+          this.isOpen = true;
+          // Ensure that the model is marked as selected
+          if (this.model) {
+            this.model.set("selected", true);
           }
-          catch (error) {
-            console.log(
-              'There was an error opening the LayerDetailsView' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error opening the LayerDetailsView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Hide/collapse the Layer Details panel. Closing the panel also changes the
+       * MapAsset model's 'selected attribute' to false.
+       */
+      close: function () {
+        try {
+          this.el.classList.remove(this.classes.open);
+          this.isOpen = false;
+          // Ensure that the model is not marked as selected
+          if (this.model) {
+            this.model.set("selected", false);
           }
-        },
-
-        /**
-         * Hide/collapse the Layer Details panel. Closing the panel also changes the
-         * MapAsset model's 'selected attribute' to false.
-         */
-        close: function () {
-          try {
-            this.el.classList.remove(this.classes.open);
-            this.isOpen = false;
-            // Ensure that the model is not marked as selected
-            if (this.model) {
-              this.model.set('selected', false)
+        } catch (error) {
+          console.log(
+            "There was an error closing the LayerDetailsView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Updates the MapAsset model set on the view then re-renders the view and
+       * displays information about the new model.
+       * @param {MapAsset|null} newModel the new MapAsset model to use to render the
+       * view. If set to null, then the view will be rendered without any layer
+       * information.
+       */
+      updateModel: function (newModel) {
+        try {
+          // Remove listeners from sub-views
+          this.renderedSections.forEach(function (section) {
+            if (
+              section.renderedView &&
+              typeof section.renderedView.onClose === "function"
+            ) {
+              section.renderedView.onClose();
             }
-          }
-          catch (error) {
-            console.log(
-              'There was an error closing the LayerDetailsView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Updates the MapAsset model set on the view then re-renders the view and
-         * displays information about the new model.
-         * @param {MapAsset|null} newModel the new MapAsset model to use to render the
-         * view. If set to null, then the view will be rendered without any layer
-         * information.
-         */
-        updateModel: function (newModel) {
-          try {
-            // Remove listeners from sub-views
-            this.renderedSections.forEach(function (section) {
-              if (
-                section.renderedView &&
-                typeof section.renderedView.onClose === 'function'
-              ) {
-                section.renderedView.onClose()
-              }
-            })
-            this.model = newModel;
-            this.render()
-          }
-          catch (error) {
-            console.log(
-              'There was an error updating the MapAsset model in a LayerDetailsView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-      }
-    );
-
-    return LayerDetailsView;
-
-  }
-);
+          });
+          this.model = newModel;
+          this.render();
+        } catch (error) {
+          console.log(
+            "There was an error updating the MapAsset model in a LayerDetailsView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+    },
+  );
+
+  return LayerDetailsView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayerInfoView.js.html b/docs/docs/src_js_views_maps_LayerInfoView.js.html index 62a1824df..4cb2c43fa 100644 --- a/docs/docs/src_js_views_maps_LayerInfoView.js.html +++ b/docs/docs/src_js_views_maps_LayerInfoView.js.html @@ -44,127 +44,109 @@

Source: src/js/views/maps/LayerInfoView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/assets/MapAsset',
-    'text!templates/maps/layer-info.html'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    MapAsset,
-    Template
-  ) {
-
-    /**
-    * @class LayerInfoView
-    * @classdesc A view that shows some of the basic info from a MapAsset model, like the
-    * description, attribution, and link to more information.
-    * @classcategory Views/Maps
-    * @name LayerInfoView
-    * @extends Backbone.View
-    * @screenshot views/maps/LayerInfoView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var LayerInfoView = Backbone.View.extend(
-      /** @lends LayerInfoView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerInfoView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'layer-info',
-
-        /**
-        * The model that this view uses
-        * @type {MapAsset}
-        */
-        model: undefined,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * @type {Object}
-        */
-        events: {
-          // 'event selector': 'function',
-        },
-
-        /**
-        * Executed when a new LayerInfoView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/assets/MapAsset",
+  "text!templates/maps/layer-info.html",
+], function ($, _, Backbone, MapAsset, Template) {
+  /**
+   * @class LayerInfoView
+   * @classdesc A view that shows some of the basic info from a MapAsset model, like the
+   * description, attribution, and link to more information.
+   * @classcategory Views/Maps
+   * @name LayerInfoView
+   * @extends Backbone.View
+   * @screenshot views/maps/LayerInfoView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var LayerInfoView = Backbone.View.extend(
+    /** @lends LayerInfoView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerInfoView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "layer-info",
+
+      /**
+       * The model that this view uses
+       * @type {MapAsset}
+       */
+      model: undefined,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        // 'event selector': 'function',
+      },
+
+      /**
+       * Executed when a new LayerInfoView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-          } catch (e) {
-            console.log('A LayerInfoView failed to initialize. Error message: ' + e);
           }
-
-        },
-
-        /**
-        * Renders this view
-        * @return {LayerInfoView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-
-            // Insert the template into the view
-            var templateOptions = this.model ? this.model.toJSON() : {};
-            this.$el.html(this.template(templateOptions));
-
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a LayerInfoView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-
-      }
-    );
-
-    return LayerInfoView;
-
-  }
-);
+        } catch (e) {
+          console.log(
+            "A LayerInfoView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerInfoView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+
+          // Insert the template into the view
+          var templateOptions = this.model ? this.model.toJSON() : {};
+          this.$el.html(this.template(templateOptions));
+
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a LayerInfoView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+    },
+  );
+
+  return LayerInfoView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayerItemView.js.html b/docs/docs/src_js_views_maps_LayerItemView.js.html index be3dad6ba..9f74bdd7c 100644 --- a/docs/docs/src_js_views_maps_LayerItemView.js.html +++ b/docs/docs/src_js_views_maps_LayerItemView.js.html @@ -44,545 +44,556 @@

Source: src/js/views/maps/LayerItemView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/assets/MapAsset',
-    'common/IconUtilities',
-    'text!templates/maps/layer-item.html',
-    // Sub-views
-    'views/maps/LegendView'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    MapAsset,
-    IconUtilities,
-    Template,
-    // Sub-views
-    Legend
-  ) {
-    /**
-    * @class LayerItemView
-    * @classdesc One item in a Layer List: shows some basic information about the Map
-    * Asset (Layer), including label and icon. Also has a button that changes the
-    * visibility of the Layer of the map (by updating the 'visibility' attribute in the
-    * MapAsset model). Clicking on the Layer Item opens the Layer Details panel (by
-    * setting the 'selected' attribute to true in the Layer model.) Additionally, shows a
-    * small preview of a legend for the data that's on the map.
-    * @classcategory Views/Maps
-    * @name LayerItemView
-    * @extends Backbone.View
-    * @screenshot views/maps/LayerItemView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var LayerItemView = Backbone.View.extend(
-      /** @lends LayerItemView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerItemView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'layer-item',
-
-        /**
-        * The model that this view uses
-        * @type {MapAsset}
-        */
-        model: undefined,
-
-        /**
-        * Whether the layer item is a under a category. Flat layer item and categorized
-        * layer item are styled differently.
-        * @type {boolean}
-        */
-        isCategorized: undefined,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * Classes that are used to identify or create the HTML elements that comprise this
-         * view.
-         * @type {Object}
-         * @property {string} label The element that contains the layer's name/label
-         * @property {string} icon The span element that contains the SVG icon
-         * @property {string} visibilityToggle The element that acts like a button to
-         * switch the Layer's visibility on and off
-         * @property {string} legendContainer The element that the legend preview will be
-         * inserted into.
-         * @property {string} selected The class that gets added to the view when the Layer
-         * Item is selected
-         * @property {string} shown The class that gets added to the view when the Layer
-         * Item is visible
-         * @property {string} badge The class to add to the badge element that is shown
-         * when the layer has a notification message
-         * @property {string} tooltip Class added to tooltips used in this view
-         */
-        classes: {
-          label: 'layer-item__label',
-          icon: 'layer-item__icon',
-          visibilityToggle: 'layer-item__visibility-toggle',
-          legendContainer: 'layer-item__legend-container',
-          selected: 'layer-item--selected',
-          shown: 'layer-item--shown',
-          labelText: 'layer-item__label-text',
-          highlightedText: 'layer-item__highlighted-text',
-          categorized: 'layer-item__categorized',
-          legendAndSettings: 'layer-item__legend-and-settings',
-          badge: 'map-view__badge',
-          tooltip: 'map-tooltip',
-        },
-
-        /**
-         * The text to show in a tooltip when the MapAsset's status is set to 'error'. If
-         * the model also has a 'statusMessage', that will be appended to the end of this
-         * error message.
-         * @type {string}
-         */
-        errorMessage: 'There was a problem showing this layer.',
-
-        /**
-        * A function that gives the events this view will listen to and the associated
-        * function to call.
-        * @returns {Object} Returns an object with events in the format 'event selector':
-        * 'function'
-        */
-        events: function () {
-          try {
-            var events = {}
-            events['click .' + this.classes.legendAndSettings] = 'toggleSelected';
-            events['click'] = 'toggleVisibility';
-            return events
-          }
-          catch (error) {
-            console.log(
-              'There was an error setting the events object in a LayerItemView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-        * Executed when a new LayerItemView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/assets/MapAsset",
+  "common/IconUtilities",
+  "text!templates/maps/layer-item.html",
+  // Sub-views
+  "views/maps/LegendView",
+], function (
+  $,
+  _,
+  Backbone,
+  MapAsset,
+  IconUtilities,
+  Template,
+  // Sub-views
+  Legend,
+) {
+  /**
+   * @class LayerItemView
+   * @classdesc One item in a Layer List: shows some basic information about the Map
+   * Asset (Layer), including label and icon. Also has a button that changes the
+   * visibility of the Layer of the map (by updating the 'visibility' attribute in the
+   * MapAsset model). Clicking on the Layer Item opens the Layer Details panel (by
+   * setting the 'selected' attribute to true in the Layer model.) Additionally, shows a
+   * small preview of a legend for the data that's on the map.
+   * @classcategory Views/Maps
+   * @name LayerItemView
+   * @extends Backbone.View
+   * @screenshot views/maps/LayerItemView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var LayerItemView = Backbone.View.extend(
+    /** @lends LayerItemView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerItemView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "layer-item",
+
+      /**
+       * The model that this view uses
+       * @type {MapAsset}
+       */
+      model: undefined,
+
+      /**
+       * Whether the layer item is a under a category. Flat layer item and categorized
+       * layer item are styled differently.
+       * @type {boolean}
+       */
+      isCategorized: undefined,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * Classes that are used to identify or create the HTML elements that comprise this
+       * view.
+       * @type {Object}
+       * @property {string} label The element that contains the layer's name/label
+       * @property {string} icon The span element that contains the SVG icon
+       * @property {string} visibilityToggle The element that acts like a button to
+       * switch the Layer's visibility on and off
+       * @property {string} legendContainer The element that the legend preview will be
+       * inserted into.
+       * @property {string} selected The class that gets added to the view when the Layer
+       * Item is selected
+       * @property {string} shown The class that gets added to the view when the Layer
+       * Item is visible
+       * @property {string} badge The class to add to the badge element that is shown
+       * when the layer has a notification message
+       * @property {string} tooltip Class added to tooltips used in this view
+       */
+      classes: {
+        label: "layer-item__label",
+        icon: "layer-item__icon",
+        visibilityToggle: "layer-item__visibility-toggle",
+        legendContainer: "layer-item__legend-container",
+        selected: "layer-item--selected",
+        shown: "layer-item--shown",
+        labelText: "layer-item__label-text",
+        highlightedText: "layer-item__highlighted-text",
+        categorized: "layer-item__categorized",
+        legendAndSettings: "layer-item__legend-and-settings",
+        badge: "map-view__badge",
+        tooltip: "map-tooltip",
+      },
+
+      /**
+       * The text to show in a tooltip when the MapAsset's status is set to 'error'. If
+       * the model also has a 'statusMessage', that will be appended to the end of this
+       * error message.
+       * @type {string}
+       */
+      errorMessage: "There was a problem showing this layer.",
+
+      /**
+       * A function that gives the events this view will listen to and the associated
+       * function to call.
+       * @returns {Object} Returns an object with events in the format 'event selector':
+       * 'function'
+       */
+      events: function () {
+        try {
+          var events = {};
+          events["click ." + this.classes.legendAndSettings] = "toggleSelected";
+          events["click"] = "toggleVisibility";
+          return events;
+        } catch (error) {
+          console.log(
+            "There was an error setting the events object in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Executed when a new LayerItemView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-          } catch (e) {
-            console.log('A LayerItemView failed to initialize. Error message: ' + e);
+          }
+        } catch (e) {
+          console.log(
+            "A LayerItemView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerItemView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          if (!this.model) {
+            return;
           }
 
-        },
-
-        /**
-        * Renders this view
-        * @return {LayerItemView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-
-            if (!this.model) {
-              return
-            }
-
-            // Insert the template into the view
-            this.$el.html(this.template({
-              label: this.model.get('label'),
+          // Insert the template into the view
+          this.$el.html(
+            this.template({
+              label: this.model.get("label"),
               classes: this.classes,
-            }));
-            // Save a reference to the label element
-            this.labelEl = this.el.querySelector('.' + this.classes.label)
-
-            // Insert the icon on the left
-            if (!this.isCategorized) {
-              this.insertIcon();
-            }
-
-            // Add a thumbnail / legend preview
-            const legendContainer = this.el.querySelector('.' + this.classes.legendContainer)
-            const legendPreview = new Legend({
-              model: this.model,
-              mode: 'preview'
-            })
-            legendContainer.append(legendPreview.render().el)
-
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-
-            // Show the item as hidden and/or selected depending on the model properties
-            // that are set initially
-            this.showVisibility()
-            this.showSelection()
-            // Show the current status of this layer
-            this.showStatus()
-
-            // When the Layer is selected, highlight this item in the Layer List. When
-            // it's no longer selected, then make sure it's no longer highlighted. Set a
-            // listener because the 'selected' attribute can be changed within this view,
-            // from the parent Layers collection, or from the Layer Details View.
-            this.stopListening(this.model, 'change:selected')
-            this.listenTo(this.model, 'change:selected', this.showSelection)
-
-            // Similar to above, add or remove the shown class when the layer's
-            // visibility changes
-            this.stopListening(this.model, 'change:visible')
-            this.listenTo(this.model, 'change:visible', this.showVisibility)
-
-            // Update the item in the list to show when it is loading, loaded, or there's
-            // been an error.
-            this.stopListening(this.model, 'change:status')
-            this.listenTo(this.model, 'change:status', this.showStatus);
-
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a LayerItemView' +
-              '. Error details: ' + error
-            );
+            }),
+          );
+          // Save a reference to the label element
+          this.labelEl = this.el.querySelector("." + this.classes.label);
+
+          // Insert the icon on the left
+          if (!this.isCategorized) {
+            this.insertIcon();
           }
-        },
-
-        /**
-         * Waits for the icon attribute to be ready in the Map Asset model, then inserts
-         * the icon before the label.
-         */
-        insertIcon: function () {
-          try {
-            const model = this.model;
-            let icon = model.get('icon');
-            if (!icon || typeof icon !== 'string' || !IconUtilities.isSVG(icon)) {
-              icon = model.defaults().icon;
-            }
-            const iconContainer = document.createElement('span');
-            iconContainer.classList.add(this.classes.icon);
-            iconContainer.innerHTML = icon;
-            this.el.querySelector('.' + this.classes.visibilityToggle).replaceChildren(iconContainer);
-
-            const iconStatus = model.get('iconStatus');
-            if (iconStatus && iconStatus === 'fetching') {
-              this.listenToOnce(model, 'change:iconStatus', this.insertIcon);
-              return;
-            }
+
+          // Add a thumbnail / legend preview
+          const legendContainer = this.el.querySelector(
+            "." + this.classes.legendContainer,
+          );
+          const legendPreview = new Legend({
+            model: this.model,
+            mode: "preview",
+          });
+          legendContainer.append(legendPreview.render().el);
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+
+          // Show the item as hidden and/or selected depending on the model properties
+          // that are set initially
+          this.showVisibility();
+          this.showSelection();
+          // Show the current status of this layer
+          this.showStatus();
+
+          // When the Layer is selected, highlight this item in the Layer List. When
+          // it's no longer selected, then make sure it's no longer highlighted. Set a
+          // listener because the 'selected' attribute can be changed within this view,
+          // from the parent Layers collection, or from the Layer Details View.
+          this.stopListening(this.model, "change:selected");
+          this.listenTo(this.model, "change:selected", this.showSelection);
+
+          // Similar to above, add or remove the shown class when the layer's
+          // visibility changes
+          this.stopListening(this.model, "change:visible");
+          this.listenTo(this.model, "change:visible", this.showVisibility);
+
+          // Update the item in the list to show when it is loading, loaded, or there's
+          // been an error.
+          this.stopListening(this.model, "change:status");
+          this.listenTo(this.model, "change:status", this.showStatus);
+
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Waits for the icon attribute to be ready in the Map Asset model, then inserts
+       * the icon before the label.
+       */
+      insertIcon: function () {
+        try {
+          const model = this.model;
+          let icon = model.get("icon");
+          if (!icon || typeof icon !== "string" || !IconUtilities.isSVG(icon)) {
+            icon = model.defaults().icon;
           }
-          catch (error) {
-            console.log(
-              'There was an error inserting an icon in a LayerItemView' +
-              '. Error details: ' + error
-            );
+          const iconContainer = document.createElement("span");
+          iconContainer.classList.add(this.classes.icon);
+          iconContainer.innerHTML = icon;
+          this.el
+            .querySelector("." + this.classes.visibilityToggle)
+            .replaceChildren(iconContainer);
+
+          const iconStatus = model.get("iconStatus");
+          if (iconStatus && iconStatus === "fetching") {
+            this.listenToOnce(model, "change:iconStatus", this.insertIcon);
+            return;
           }
-        },
-
-        /**
-         * Sets the Layer model's 'selected' status attribute to true if it's false, and
-         * to false if it's true. Executed when a user clicks on this Layer Item in a
-         * Layer List view.
-         */
-        toggleSelected: function () {
-          try {
-            var layerModel = this.model;
-            if (layerModel.get('selected')) {
-              layerModel.set('selected', false);
-            } else {
-              layerModel.set('selected', true);
-            }
+        } catch (error) {
+          console.log(
+            "There was an error inserting an icon in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Sets the Layer model's 'selected' status attribute to true if it's false, and
+       * to false if it's true. Executed when a user clicks on this Layer Item in a
+       * Layer List view.
+       */
+      toggleSelected: function () {
+        try {
+          var layerModel = this.model;
+          if (layerModel.get("selected")) {
+            layerModel.set("selected", false);
+          } else {
+            layerModel.set("selected", true);
           }
-          catch (error) {
-            console.log(
-              'There was an error selecting or unselecting a layer in a LayerItemView' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error selecting or unselecting a layer in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Sets the Layer model's visibility status attribute to true if it's false, and
+       * to false if it's true. Executed when a user clicks on the visibility toggle.
+       */
+      toggleVisibility: function (event) {
+        try {
+          if (
+            this.$(`.${this.classes.legendAndSettings}`).is(event.target) ||
+            this.$(`.${this.classes.legendAndSettings}`).has(event.target)
+              .length > 0
+          ) {
+            return;
           }
-        },
-
-        /**
-         * Sets the Layer model's visibility status attribute to true if it's false, and
-         * to false if it's true. Executed when a user clicks on the visibility toggle.
-         */
-        toggleVisibility: function (event) {
-          try {
-            if (this.$(`.${this.classes.legendAndSettings}`).is(event.target) ||
-                this.$(`.${this.classes.legendAndSettings}`).has(event.target).length > 0) {
-              return;
-            }
 
-            const layerModel = this.model;
-            // Hide if visible
-            if (layerModel.get('visible')) {
-              layerModel.set('visible', false);
+          const layerModel = this.model;
+          // Hide if visible
+          if (layerModel.get("visible")) {
+            layerModel.set("visible", false);
             // Show if hidden
-            } else {
-              // If user is trying to make the layer visible, make sure the opacity is not 0
-              if (layerModel.get('opacity') === 0) {
-                layerModel.set('opacity', 0.5);
-              }
-              layerModel.set('visible', true);
+          } else {
+            // If user is trying to make the layer visible, make sure the opacity is not 0
+            if (layerModel.get("opacity") === 0) {
+              layerModel.set("opacity", 0.5);
             }
+            layerModel.set("visible", true);
           }
-          catch (error) {
-            console.log(
-              'There was an error selecting or unselecting a layer in a LayerItemView' +
-              '. Error details: ' + error
+        } catch (error) {
+          console.log(
+            "There was an error selecting or unselecting a layer in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Highlight/emphasize this item in the Layer List when it is selected (i.e. when
+       * the Layer model's 'selected' attribute is set to true). If it is not selected,
+       * then remove any highlighting. This function is executed whenever the model's
+       * 'selected' attribute changes. It can be changed from within this view (with the
+       * toggleSelected function), from the parent Layers collection, or from the
+       * Layer Details View.
+       */
+      showSelection: function () {
+        try {
+          var layerModel = this.model;
+          if (layerModel.get("selected")) {
+            this.$(`.${this.classes.legendAndSettings}`).addClass(
+              this.classes.selected,
             );
-          }
-        },
-
-        /**
-         * Highlight/emphasize this item in the Layer List when it is selected (i.e. when
-         * the Layer model's 'selected' attribute is set to true). If it is not selected,
-         * then remove any highlighting. This function is executed whenever the model's
-         * 'selected' attribute changes. It can be changed from within this view (with the
-         * toggleSelected function), from the parent Layers collection, or from the
-         * Layer Details View.
-         */
-        showSelection: function () {
-          try {
-            var layerModel = this.model;
-            if (layerModel.get('selected')) {
-              this.$(`.${this.classes.legendAndSettings}`).addClass(this.classes.selected)
-            } else {
-              this.$(`.${this.classes.legendAndSettings}`).removeClass(this.classes.selected)
-            }
-          }
-          catch (error) {
-            console.log(
-              'There was an error changing the highlighting in a LayerItemView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Add or remove styles that indicate that the layer is shown based on what is
-         * set in the Layer model's 'visible' attribute. Executed whenever the 'visible'
-         * attribute changes.
-         */
-        showVisibility: function () {
-          try {
-            var layerModel = this.model;
-            if (layerModel.get('visible')) {
-              this.$el.addClass(this.classes.shown);
-            } else {
-              this.$el.removeClass(this.classes.shown);
-            }
-          }
-          catch (error) {
-            console.log(
-              'There was an error changing the shown styles in a LayerItemView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Gets the Map Asset model's status and updates this Layer Item View to reflect
-         * that status to the user.
-         */
-        showStatus: function () {
-          try {
-            var layerModel = this.model;
-            var status = layerModel.get('status');
-            if (status === 'error') {
-              const errorMessage = layerModel.get('statusDetails')
-              this.showError(errorMessage)
-            } else if (status === 'ready') {
-              this.removeStatuses()
-              const notice = layerModel.get('notification')
-              const badge = notice ? notice.badge : null
-              if (badge) {
-                this.showBadge(badge, notice.style)
-              }
-            } else if (status === 'loading') {
-              this.showLoading()
-            }
-          }
-          catch (error) {
-            console.log(
-              'There was an error showing the status in a LayerItemView' +
-              '. Error details: ' + error
+          } else {
+            this.$(`.${this.classes.legendAndSettings}`).removeClass(
+              this.classes.selected,
             );
           }
-        },
-
-        /**
-         * Remove any icons, tooltips, or other visual indicators of a Map Asset's error
-         * or loading status in this view
-         */
-        removeStatuses: function () {
-          try {
-            if (this.statusIcon) {
-              this.statusIcon.remove()
-            }
-            if (this.badge) {
-              this.badge.remove()
-            }
-            this.$el.tooltip('destroy')
-          }
-          catch (error) {
-            console.log(
-              'There was an error removing status indicators in a LayerItemView' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error changing the highlighting in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Add or remove styles that indicate that the layer is shown based on what is
+       * set in the Layer model's 'visible' attribute. Executed whenever the 'visible'
+       * attribute changes.
+       */
+      showVisibility: function () {
+        try {
+          var layerModel = this.model;
+          if (layerModel.get("visible")) {
+            this.$el.addClass(this.classes.shown);
+          } else {
+            this.$el.removeClass(this.classes.shown);
           }
-        },
-
-        /**
-         * Create a badge element and insert it to the right of the layer label.
-         * @param {string} text - The text to display in the badge
-         * @param {string} [style] - The style of the badge. Can be any of the styles
-         * defined in the {@link MapConfig#Notification} style property, e.g. 'green'
-         */
-        showBadge: function (text, style) {
-          try {
-            if (!text) {
-              return
-            }
+        } catch (error) {
+          console.log(
+            "There was an error changing the shown styles in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Gets the Map Asset model's status and updates this Layer Item View to reflect
+       * that status to the user.
+       */
+      showStatus: function () {
+        try {
+          var layerModel = this.model;
+          var status = layerModel.get("status");
+          if (status === "error") {
+            const errorMessage = layerModel.get("statusDetails");
+            this.showError(errorMessage);
+          } else if (status === "ready") {
             this.removeStatuses();
-            this.badge = document.createElement('span')
-            this.badge.classList.add(this.classes.badge)
-            this.badge.innerText = text
-            this.labelEl.append(this.badge)
-            if (style) {
-              const badgeClass = this.classes.badge + '--' + style
-              this.badge.classList.add(badgeClass)
+            const notice = layerModel.get("notification");
+            const badge = notice ? notice.badge : null;
+            if (badge) {
+              this.showBadge(badge, notice.style);
             }
-          } catch (error) {
-            console.log(
-              'There was an error showing the badge in a LayerItemView' +
-              '. Error details: ' + error
-            );
+          } else if (status === "loading") {
+            this.showLoading();
           }
-        },
-
-        /**
-         * Indicate to the user that there was a problem showing or loading this error.
-         * Shows a 'warning' icon to the right of the label for the asset and a tooltip
-         * with more details
-         * @param {string} message The error message to show in the tooltip.
-         */
-        showError: function (message='') {
-          try {
-            const view = this
-
-            // Remove any style elements for other statuses
-            this.removeStatuses()
-
-            // Show a warning icon
-            this.statusIcon = document.createElement('span')
-            this.statusIcon.innerHTML = `<i class="icon-warning-sign icon icon-on-right"></i>`
-            this.statusIcon.style.opacity = '0.6'
-            this.labelEl.append(this.statusIcon)
-
-            // Show a tooltip with the error message
-            let fullMessage = this.errorMessage
-            if (message) {
-              fullMessage = fullMessage + ' Error details: ' + message
-            }
-            this.$el.tooltip({
-              placement: 'top',
-              trigger: 'hover',
-              title: fullMessage,
-              container: 'body',
-              animation: false,
-              template: '<div class="tooltip ' +
-                view.classes.tooltip +
-                '"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
-              delay: { show: 250, hide: 5 }
-            })
-
+        } catch (error) {
+          console.log(
+            "There was an error showing the status in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Remove any icons, tooltips, or other visual indicators of a Map Asset's error
+       * or loading status in this view
+       */
+      removeStatuses: function () {
+        try {
+          if (this.statusIcon) {
+            this.statusIcon.remove();
           }
-          catch (error) {
-            console.log(
-              'Failed to show the error status in a LayerItemView' +
-              '. Error details: ' + error
-            );
+          if (this.badge) {
+            this.badge.remove();
           }
-        },
-
-        /**
-         * Show a spinner icon to the right of the Map Asset label to indicate that this
-         * layer is loading
-         */
-        showLoading: function() {
-          try {
-            // Remove any style elements for other statuses
-            this.removeStatuses()
-
-            // Show a spinner icon
-            this.statusIcon = document.createElement('span')
-            this.statusIcon.innerHTML = `<i class="icon-spinner icon-spin icon-small loading icon icon-on-right"></i>`
-            this.statusIcon.style.opacity = '0.6'
-            this.labelEl.append(this.statusIcon)
+          this.$el.tooltip("destroy");
+        } catch (error) {
+          console.log(
+            "There was an error removing status indicators in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Create a badge element and insert it to the right of the layer label.
+       * @param {string} text - The text to display in the badge
+       * @param {string} [style] - The style of the badge. Can be any of the styles
+       * defined in the {@link MapConfig#Notification} style property, e.g. 'green'
+       */
+      showBadge: function (text, style) {
+        try {
+          if (!text) {
+            return;
           }
-          catch (error) {
-            console.log(
-              'There was an error showing the loading status in a LayerItemView' +
-              '. Error details: ' + error
-            );
+          this.removeStatuses();
+          this.badge = document.createElement("span");
+          this.badge.classList.add(this.classes.badge);
+          this.badge.innerText = text;
+          this.labelEl.append(this.badge);
+          if (style) {
+            const badgeClass = this.classes.badge + "--" + style;
+            this.badge.classList.add(badgeClass);
+          }
+        } catch (error) {
+          console.log(
+            "There was an error showing the badge in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Indicate to the user that there was a problem showing or loading this error.
+       * Shows a 'warning' icon to the right of the label for the asset and a tooltip
+       * with more details
+       * @param {string} message The error message to show in the tooltip.
+       */
+      showError: function (message = "") {
+        try {
+          const view = this;
+
+          // Remove any style elements for other statuses
+          this.removeStatuses();
+
+          // Show a warning icon
+          this.statusIcon = document.createElement("span");
+          this.statusIcon.innerHTML = `<i class="icon-warning-sign icon icon-on-right"></i>`;
+          this.statusIcon.style.opacity = "0.6";
+          this.labelEl.append(this.statusIcon);
+
+          // Show a tooltip with the error message
+          let fullMessage = this.errorMessage;
+          if (message) {
+            fullMessage = fullMessage + " Error details: " + message;
           }
-        },
-
-        /**
-         * Searches and only displays self if layer label matches the text. Highlights the
-         * matched text.
-         * @param {string} [text] - The search text from user input.
-         * @returns {boolean} - True if a layer label matches the text
-         */
-        search(text) {
-          let newLabel = this.model.get('label');
-          if (text) {
-            const regex = new RegExp(text, "ig");
-            newLabel = this.model.get('label').replaceAll(regex, matchedText => {
-              return $('<span />').addClass(this.classes.highlightedText).html(matchedText).prop('outerHTML');
+          this.$el.tooltip({
+            placement: "top",
+            trigger: "hover",
+            title: fullMessage,
+            container: "body",
+            animation: false,
+            template:
+              '<div class="tooltip ' +
+              view.classes.tooltip +
+              '"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+            delay: { show: 250, hide: 5 },
+          });
+        } catch (error) {
+          console.log(
+            "Failed to show the error status in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Show a spinner icon to the right of the Map Asset label to indicate that this
+       * layer is loading
+       */
+      showLoading: function () {
+        try {
+          // Remove any style elements for other statuses
+          this.removeStatuses();
+
+          // Show a spinner icon
+          this.statusIcon = document.createElement("span");
+          this.statusIcon.innerHTML = `<i class="icon-spinner icon-spin icon-small loading icon icon-on-right"></i>`;
+          this.statusIcon.style.opacity = "0.6";
+          this.labelEl.append(this.statusIcon);
+        } catch (error) {
+          console.log(
+            "There was an error showing the loading status in a LayerItemView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Searches and only displays self if layer label matches the text. Highlights the
+       * matched text.
+       * @param {string} [text] - The search text from user input.
+       * @returns {boolean} - True if a layer label matches the text
+       */
+      search(text) {
+        let newLabel = this.model.get("label");
+        if (text) {
+          const regex = new RegExp(text, "ig");
+          newLabel = this.model
+            .get("label")
+            .replaceAll(regex, (matchedText) => {
+              return $("<span />")
+                .addClass(this.classes.highlightedText)
+                .html(matchedText)
+                .prop("outerHTML");
             });
 
-            // Label is unchanged.
-            if (newLabel === this.model.get('label')) {
-              this.$el.hide();
-              return false;
-            }
+          // Label is unchanged.
+          if (newLabel === this.model.get("label")) {
+            this.$el.hide();
+            return false;
           }
-
-          this.labelEl.querySelector(`.${this.classes.labelText}`).innerHTML = newLabel;
-          this.$el.show();
-          return true;
-        },
-      }
-    );
-
-    return LayerItemView;
-
-  }
-);
+        }
+
+        this.labelEl.querySelector(`.${this.classes.labelText}`).innerHTML =
+          newLabel;
+        this.$el.show();
+        return true;
+      },
+    },
+  );
+
+  return LayerItemView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayerListView.js.html b/docs/docs/src_js_views_maps_LayerListView.js.html index 33d3d5018..98ca60794 100644 --- a/docs/docs/src_js_views_maps_LayerListView.js.html +++ b/docs/docs/src_js_views_maps_LayerListView.js.html @@ -44,179 +44,172 @@

Source: src/js/views/maps/LayerListView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'text!templates/maps/layer-list.html',
-    // Sub-views
-    'views/maps/LayerItemView'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    Template,
-    // Sub-views
-    LayerItemView
-  ) {
-
-    /**
-    * @class LayerListView
-    * @classdesc A Layer List shows a collection of Map Assets, like imagery and vector
-    * layers. Each Map Asset in the collection is rendered as a single item in the list.
-    * Each item can be clicked for more details.
-    * @classcategory Views/Maps
-    * @name LayerListView
-    * @extends Backbone.View
-    * @screenshot views/maps/LayerListView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var LayerListView = Backbone.View.extend(
-      /** @lends LayerListView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerListView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'layer-list',
-
-        /**
-        * The collection of layers to display in the list
-        * @type {MapAssets}
-        */
-        collection: undefined,
-
-        /**
-        * Whether the layer list is a under a category. Flat layer list and categorized
-        * layer list are styled differently.
-        * @type {boolean}
-        */
-        isCategorized: undefined,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * @type {Object}
-        */
-        events: {
-          // 'event selector': 'function',
-        },
-
-        /**
-        * Executed when a new LayerListView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/maps/layer-list.html",
+  // Sub-views
+  "views/maps/LayerItemView",
+], function (
+  $,
+  _,
+  Backbone,
+  Template,
+  // Sub-views
+  LayerItemView,
+) {
+  /**
+   * @class LayerListView
+   * @classdesc A Layer List shows a collection of Map Assets, like imagery and vector
+   * layers. Each Map Asset in the collection is rendered as a single item in the list.
+   * Each item can be clicked for more details.
+   * @classcategory Views/Maps
+   * @name LayerListView
+   * @extends Backbone.View
+   * @screenshot views/maps/LayerListView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var LayerListView = Backbone.View.extend(
+    /** @lends LayerListView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerListView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "layer-list",
+
+      /**
+       * The collection of layers to display in the list
+       * @type {MapAssets}
+       */
+      collection: undefined,
+
+      /**
+       * Whether the layer list is a under a category. Flat layer list and categorized
+       * layer list are styled differently.
+       * @type {boolean}
+       */
+      isCategorized: undefined,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        // 'event selector': 'function',
+      },
+
+      /**
+       * Executed when a new LayerListView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-            this.setListeners();
-          } catch (e) {
-            console.log('A LayerListView failed to initialize. Error message: ' + e);
           }
-
-        },
-
-        /**
-         * Remove any event listeners on the collection
-         * @since 2.27.0
-         */
-        removeListeners: function () {
-          try {
-            if (this.collection) {
-              this.stopListening(this.collection);
-            }
-          } catch (e) {
-            console.log('Failed to remove listeners:', e);
+          this.setListeners();
+        } catch (e) {
+          console.log(
+            "A LayerListView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Remove any event listeners on the collection
+       * @since 2.27.0
+       */
+      removeListeners: function () {
+        try {
+          if (this.collection) {
+            this.stopListening(this.collection);
           }
-        },
-
-        /**
-         * Add or remove items from the list when the collection changes
-         * @since 2.27.0
-         */
-        setListeners: function () {
-          try {
-            if (this.collection) {
-              this.listenTo(this.collection, 'add remove reset', this.render);
-            }
-          } catch (e) {
-            console.log('Failed to set listeners:', e);
+        } catch (e) {
+          console.log("Failed to remove listeners:", e);
+        }
+      },
+
+      /**
+       * Add or remove items from the list when the collection changes
+       * @since 2.27.0
+       */
+      setListeners: function () {
+        try {
+          if (this.collection) {
+            this.listenTo(this.collection, "add remove reset", this.render);
           }
-        },
-
-        /**
-        * Renders this view
-        * @return {LayerListView} Returns the rendered view element
-        */
-        render: function () {
-          this.$el.html(this.template({}));
-
-          // Ensure the view's main element has the given class name
-          this.el.classList.add(this.className);
-
-          if (!this.collection) {
-            return;
-          }
-
-          // Render a layer item for each layer in the collection
-          this.layerItemViews = this.collection.reduce((memo, layerModel) => {
-            if (layerModel.get('hideInLayerList') === true){
-              // skip this layer
-              return memo;
-            }
-            const layerItem = new LayerItemView({
-              model: layerModel,
-              isCategorized: this.isCategorized,
-            })
-            layerItem.render();
-            this.el.appendChild(layerItem.el);
-            memo.push(layerItem);
+        } catch (e) {
+          console.log("Failed to set listeners:", e);
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerListView} Returns the rendered view element
+       */
+      render: function () {
+        this.$el.html(this.template({}));
+
+        // Ensure the view's main element has the given class name
+        this.el.classList.add(this.className);
+
+        if (!this.collection) {
+          return;
+        }
+
+        // Render a layer item for each layer in the collection
+        this.layerItemViews = this.collection.reduce((memo, layerModel) => {
+          if (layerModel.get("hideInLayerList") === true) {
+            // skip this layer
             return memo;
-          }, []);
-
-          return this;
-        },
-
-        /**
-         * Searches and only displays layers that match the text.
-         * @param {string} [text] - The search text from user input.
-         * @returns {boolean} - True if a layer item matches the text
-         */
-        search(text) {
-          return this.layerItemViews.reduce((matched, layerItem) => {
-            return layerItem.search(text) || matched;
-          }, false);
-        },
-      }
-    );
-
-    return LayerListView;
-
-  }
-);
+          }
+          const layerItem = new LayerItemView({
+            model: layerModel,
+            isCategorized: this.isCategorized,
+          });
+          layerItem.render();
+          this.el.appendChild(layerItem.el);
+          memo.push(layerItem);
+          return memo;
+        }, []);
+
+        return this;
+      },
+
+      /**
+       * Searches and only displays layers that match the text.
+       * @param {string} [text] - The search text from user input.
+       * @returns {boolean} - True if a layer item matches the text
+       */
+      search(text) {
+        return this.layerItemViews.reduce((matched, layerItem) => {
+          return layerItem.search(text) || matched;
+        }, false);
+      },
+    },
+  );
+
+  return LayerListView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayerNavigationView.js.html b/docs/docs/src_js_views_maps_LayerNavigationView.js.html index 3005586ff..a7f6e9cba 100644 --- a/docs/docs/src_js_views_maps_LayerNavigationView.js.html +++ b/docs/docs/src_js_views_maps_LayerNavigationView.js.html @@ -44,157 +44,139 @@

Source: src/js/views/maps/LayerNavigationView.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/assets/MapAsset',
-    'text!templates/maps/layer-navigation.html'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    MapAsset,
-    Template
-  ) {
-
-    /**
-    * @class LayerNavigationView
-    * @classdesc A panel with buttons that control navigation to points of interest in a
-    * Layer or other Map Asset, including a button that zooms to the entire extent of the
-    * asset. This view may update the opacity and visibility of a MapAsset.
-    * @classcategory Views/Maps
-    * @name LayerNavigationView
-    * @extends Backbone.View
-    * @screenshot views/maps/LayerNavigationView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var LayerNavigationView = Backbone.View.extend(
-      /** @lends LayerNavigationView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerNavigationView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'layer-navigation',
-
-        /**
-        * The model that this view uses
-        * @type {MapAsset}
-        */
-        model: null,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * CSS classes assigned to the HTML elements that make up this view
-         * @type {Object}
-         * @property {string} extentButton The button that should zoom to the full extent
-         * of the asset/layer when clicked
-        */
-        classes: {
-          extentButton: 'layer-navigation__button--extent',
-        },
-
-        /**
-         * Creates an object that gives the events this view will listen to and the
-         * associated function to call. Each entry in the object has the format 'event
-         * selector': 'function'.
-         * @returns {Object}
-        */
-         events: function () {
-          var events = {};
-          // Trigger an event for parent views when the 'zoom to extent' button is clicked
-          events['click .' + this.classes.extentButton] = 'flyToExtent'
-          return events
-        },
-
-        /**
-        * Executed when a new LayerNavigationView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/assets/MapAsset",
+  "text!templates/maps/layer-navigation.html",
+], function ($, _, Backbone, MapAsset, Template) {
+  /**
+   * @class LayerNavigationView
+   * @classdesc A panel with buttons that control navigation to points of interest in a
+   * Layer or other Map Asset, including a button that zooms to the entire extent of the
+   * asset. This view may update the opacity and visibility of a MapAsset.
+   * @classcategory Views/Maps
+   * @name LayerNavigationView
+   * @extends Backbone.View
+   * @screenshot views/maps/LayerNavigationView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var LayerNavigationView = Backbone.View.extend(
+    /** @lends LayerNavigationView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerNavigationView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "layer-navigation",
+
+      /**
+       * The model that this view uses
+       * @type {MapAsset}
+       */
+      model: null,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * CSS classes assigned to the HTML elements that make up this view
+       * @type {Object}
+       * @property {string} extentButton The button that should zoom to the full extent
+       * of the asset/layer when clicked
+       */
+      classes: {
+        extentButton: "layer-navigation__button--extent",
+      },
+
+      /**
+       * Creates an object that gives the events this view will listen to and the
+       * associated function to call. Each entry in the object has the format 'event
+       * selector': 'function'.
+       * @returns {Object}
+       */
+      events: function () {
+        var events = {};
+        // Trigger an event for parent views when the 'zoom to extent' button is clicked
+        events["click ." + this.classes.extentButton] = "flyToExtent";
+        return events;
+      },
+
+      /**
+       * Executed when a new LayerNavigationView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-          } catch (e) {
-            console.log('A LayerNavigationView failed to initialize. Error message: ' + e);
           }
-
-        },
-
-        /**
-        * Renders this view
-        * @return {LayerNavigationView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-
-            // Insert the template into the view
-            this.$el.html(this.template({}));
-
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a LayerNavigationView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Trigger an event from the Map Asset model that tells the Map Widget to zoom to
-         * the full extent of this layer. Also make sure that the layer is visible. If
-         * it's not visible after the user clicks the "zoom" button, that could be
-         * confusing.
-         */
-        flyToExtent : function(){
-          try {
-            this.model.show()
-            this.model.zoomTo(this.model)
-          }
-          catch (e) {
-            console.log("Error flying to extent of a layer", e);
-          }
-        },
-
-
-      }
-    );
-
-    return LayerNavigationView;
-
-  }
-);
+        } catch (e) {
+          console.log(
+            "A LayerNavigationView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerNavigationView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // Insert the template into the view
+          this.$el.html(this.template({}));
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a LayerNavigationView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Trigger an event from the Map Asset model that tells the Map Widget to zoom to
+       * the full extent of this layer. Also make sure that the layer is visible. If
+       * it's not visible after the user clicks the "zoom" button, that could be
+       * confusing.
+       */
+      flyToExtent: function () {
+        try {
+          this.model.show();
+          this.model.zoomTo(this.model);
+        } catch (e) {
+          console.log("Error flying to extent of a layer", e);
+        }
+      },
+    },
+  );
+
+  return LayerNavigationView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayerOpacityView.js.html b/docs/docs/src_js_views_maps_LayerOpacityView.js.html index f9ea8d1a5..cb3f9b500 100644 --- a/docs/docs/src_js_views_maps_LayerOpacityView.js.html +++ b/docs/docs/src_js_views_maps_LayerOpacityView.js.html @@ -44,286 +44,276 @@

Source: src/js/views/maps/LayerOpacityView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/assets/MapAsset',
-    'text!templates/maps/layer-opacity.html'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    MapAsset,
-    Template
-  ) {
-
-    /**
-    * @class LayerOpacityView
-    * @classdesc A number slider that shows and updates the opacity in a MapAsset model.
-    * Changing the opacity of a layer will also make it visible, if it was not visible
-    * before (i.e. this view also updates the MapAsset's visible attribute.)
-    * @classcategory Views/Maps
-    * @name LayerOpacityView
-    * @extends Backbone.View
-    * @screenshot views/maps/LayerOpacityView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var LayerOpacityView = Backbone.View.extend(
-      /** @lends LayerOpacityView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LayerOpacityView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'layer-opacity',
-
-        /**
-        * The model that this view uses
-        * @type {MapAsset}
-        */
-        model: undefined,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * CSS classes assigned to the HTML elements that make up this view
-         * @type {Object}
-         * @property {string} sliderContainer The element that will be converted by this
-         * view into a number slider widget. An element with this class must exist in the
-         * template.
-         * @property {string} handle The class given to the element that acts as a
-         * handle for the slider UI. The handle is created during render and the class is
-         * set by the jquery slider widget.
-         * @property {string} range The class given to the element that shades the
-         * slider from 0 to the current opacity. The range is created during render and
-         * the class is set by the jquery slider widget.
-         * @property {string} label The element that displays the current opacity
-         * value as a percentage. This element is created during render.
-         */
-        classes: {
-          sliderContainer: 'layer-opacity__slider',
-          handle: 'layer-opacity__handle',
-          range: 'layer-opacity__range',
-          label: 'layer-opacity__label'
-        },
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * @type {Object}
-        */
-        events: {
-          // 'event selector': 'function',
-        },
-
-        /**
-        * Executed when a new LayerOpacityView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/assets/MapAsset",
+  "text!templates/maps/layer-opacity.html",
+], function ($, _, Backbone, MapAsset, Template) {
+  /**
+   * @class LayerOpacityView
+   * @classdesc A number slider that shows and updates the opacity in a MapAsset model.
+   * Changing the opacity of a layer will also make it visible, if it was not visible
+   * before (i.e. this view also updates the MapAsset's visible attribute.)
+   * @classcategory Views/Maps
+   * @name LayerOpacityView
+   * @extends Backbone.View
+   * @screenshot views/maps/LayerOpacityView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var LayerOpacityView = Backbone.View.extend(
+    /** @lends LayerOpacityView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LayerOpacityView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "layer-opacity",
+
+      /**
+       * The model that this view uses
+       * @type {MapAsset}
+       */
+      model: undefined,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * CSS classes assigned to the HTML elements that make up this view
+       * @type {Object}
+       * @property {string} sliderContainer The element that will be converted by this
+       * view into a number slider widget. An element with this class must exist in the
+       * template.
+       * @property {string} handle The class given to the element that acts as a
+       * handle for the slider UI. The handle is created during render and the class is
+       * set by the jquery slider widget.
+       * @property {string} range The class given to the element that shades the
+       * slider from 0 to the current opacity. The range is created during render and
+       * the class is set by the jquery slider widget.
+       * @property {string} label The element that displays the current opacity
+       * value as a percentage. This element is created during render.
+       */
+      classes: {
+        sliderContainer: "layer-opacity__slider",
+        handle: "layer-opacity__handle",
+        range: "layer-opacity__range",
+        label: "layer-opacity__label",
+      },
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        // 'event selector': 'function',
+      },
+
+      /**
+       * Executed when a new LayerOpacityView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-          } catch (e) {
-            console.log('A LayerOpacityView failed to initialize. Error message: ' + e);
           }
-
-        },
-
-        /**
-        * Renders this view
-        * @return {LayerOpacityView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-
-            // Insert the template into the view
-            this.$el.html(this.template({}));
-
-            var startOpacity = this.model ? this.model.get('opacity') || 1 : 1;
-
-            // Find the element that will contain the slider
-            view.sliderContainer = this.$el.find('.' + this.classes.sliderContainer).first()
-
-            // The model opacity may be updated by this or other views or models. Make
-            // sure that the UI reflects any of these changes.
-            view.stopListening(view.model, 'change:opacity')
-            view.listenTo(view.model, 'change:opacity', view.updateSlider)
-
-            // Create the jQuery slider widget. See https://api.jqueryui.com/slider/
-            view.sliderContainer.slider({
-              min: 0,
-              max: 1,
-              range: 'min',
-              value: startOpacity,
-              step: 0.01,
-              // classes to add to the slider elements
-              classes: {
-                'ui-slider': '',
-                'ui-slider-handle': view.classes.handle,
-                'ui-slider-range': view.classes.range
-              },
-              // event handling
-              slide: handleSliderEvent, // when the slider is moved by the user
-              change: handleSliderEvent // when the slider is changed programmatically
-            })
-
-            // What to do when the opacity slider is changed. The event handler needs the
-            // view context to call other functions that update the model and the label.
-            function handleSliderEvent (e, ui) {
-              const newOpacity = ui.value
-              const currentVisibility = view.model.get('visible')
-              // Update the model. This will trigger other UI updates in this view.
-              view.updateModel(newOpacity)
-              // If the opacity changes to anything but zero, then make sure the asset is
-              // also visible. (Why would a user change the opacity and not also want the
-              // layer visible?)
-              if (newOpacity > 0 && !currentVisibility) {
-                view.model.set('visible', true)
+        } catch (e) {
+          console.log(
+            "A LayerOpacityView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LayerOpacityView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+
+          // Insert the template into the view
+          this.$el.html(this.template({}));
+
+          var startOpacity = this.model ? this.model.get("opacity") || 1 : 1;
+
+          // Find the element that will contain the slider
+          view.sliderContainer = this.$el
+            .find("." + this.classes.sliderContainer)
+            .first();
+
+          // The model opacity may be updated by this or other views or models. Make
+          // sure that the UI reflects any of these changes.
+          view.stopListening(view.model, "change:opacity");
+          view.listenTo(view.model, "change:opacity", view.updateSlider);
+
+          // Create the jQuery slider widget. See https://api.jqueryui.com/slider/
+          view.sliderContainer.slider({
+            min: 0,
+            max: 1,
+            range: "min",
+            value: startOpacity,
+            step: 0.01,
+            // classes to add to the slider elements
+            classes: {
+              "ui-slider": "",
+              "ui-slider-handle": view.classes.handle,
+              "ui-slider-range": view.classes.range,
+            },
+            // event handling
+            slide: handleSliderEvent, // when the slider is moved by the user
+            change: handleSliderEvent, // when the slider is changed programmatically
+          });
+
+          // What to do when the opacity slider is changed. The event handler needs the
+          // view context to call other functions that update the model and the label.
+          function handleSliderEvent(e, ui) {
+            const newOpacity = ui.value;
+            const currentVisibility = view.model.get("visible");
+            // Update the model. This will trigger other UI updates in this view.
+            view.updateModel(newOpacity);
+            // If the opacity changes to anything but zero, then make sure the asset is
+            // also visible. (Why would a user change the opacity and not also want the
+            // layer visible?)
+            if (newOpacity > 0 && !currentVisibility) {
+              view.model.set("visible", true);
               // If the opacity is changed to zero, also set visibility to false. This
               // triggers the layer list to grey-out the layer item.
-              } else if (newOpacity === 0 && currentVisibility) {
-                view.model.set('visible', false)
-              }
-            }
-
-            // Create the element that will display the current opacity value as a
-            // percentage. Insert it into the slider handle so that it can be easily
-            // positioned just below the handle, even as the handle moves.
-            this.opacityLabel = document.createElement('div')
-            this.opacityLabel.className = view.classes.label
-            view.sliderContainer.slider('instance').handle.append(this.opacityLabel)
-            // Show the initial opacity value
-            view.updateLabel(startOpacity)
-
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a LayerOpacityView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Get the new opacity value from the model and update the slider handle position
-         * and label. This function is called whenever the model opacity is updated.
-         */
-        updateSlider: function () {
-          try {
-            const newOpacity = this.model.get('opacity')
-            // Only update if the value has actually changed
-            if (newOpacity !== this.displayedOpacity) {
-              this.updateLabel(newOpacity)
-              // If this function was triggered by any event other than a user sliding the
-              // handle, then the slider handle position will need to be updated
-              this.sliderContainer.slider('value', newOpacity)
-              this.displayedOpacity = newOpacity
-            }
-          }
-          catch (error) {
-            console.log(
-              'There was an error handling a slider event in a LayerOpacityView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Update the MapAsset model's opacity attribute with a new value.
-         * @param {Number} newOpacity A number between 0 and 1 indicating the new opacity
-         * value for the MapAsset model
-         */
-        updateModel: function (newOpacity) {
-          try {
-            if (!this.model || typeof newOpacity !== 'number') {
-              return
+            } else if (newOpacity === 0 && currentVisibility) {
+              view.model.set("visible", false);
             }
-            this.model.set('opacity', newOpacity)
           }
-          catch (error) {
-            console.log(
-              'There was an error updating the model in a LayerOpacityView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Update the label with the newOpacity displayed as a percentage
-         * @param {Number} newOpacity A number between 0 and 1 indicating the new opacity
-         * value for the MapAsset model
-         */
-        updateLabel: function (newOpacity) {
-
-          try {
-            if (!this.opacityLabel || (typeof newOpacity === 'undefined') || typeof newOpacity !== 'number') {
-              return
-            }
-            var opacityPercent = Math.round(newOpacity * 100);
-            this.opacityLabel.innerText = opacityPercent + '%'
-          }
-          catch (error) {
-            console.log(
-              'There was an error updating the opacity label in a LayerOpacityView' +
-              '. Error details: ' + error
-            );
+
+          // Create the element that will display the current opacity value as a
+          // percentage. Insert it into the slider handle so that it can be easily
+          // positioned just below the handle, even as the handle moves.
+          this.opacityLabel = document.createElement("div");
+          this.opacityLabel.className = view.classes.label;
+          view.sliderContainer
+            .slider("instance")
+            .handle.append(this.opacityLabel);
+          // Show the initial opacity value
+          view.updateLabel(startOpacity);
+
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a LayerOpacityView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Get the new opacity value from the model and update the slider handle position
+       * and label. This function is called whenever the model opacity is updated.
+       */
+      updateSlider: function () {
+        try {
+          const newOpacity = this.model.get("opacity");
+          // Only update if the value has actually changed
+          if (newOpacity !== this.displayedOpacity) {
+            this.updateLabel(newOpacity);
+            // If this function was triggered by any event other than a user sliding the
+            // handle, then the slider handle position will need to be updated
+            this.sliderContainer.slider("value", newOpacity);
+            this.displayedOpacity = newOpacity;
           }
-        },
-
-        /**
-         * Perform clean-up functions when this view is about to be removed from the page
-         * or navigated away from.
-         */
-        onClose: function () {
-          try {
-            this.stopListening(this.model, 'change:opacity')
+        } catch (error) {
+          console.log(
+            "There was an error handling a slider event in a LayerOpacityView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Update the MapAsset model's opacity attribute with a new value.
+       * @param {Number} newOpacity A number between 0 and 1 indicating the new opacity
+       * value for the MapAsset model
+       */
+      updateModel: function (newOpacity) {
+        try {
+          if (!this.model || typeof newOpacity !== "number") {
+            return;
           }
-          catch (error) {
-            console.log(
-              'There was an error performing clean up functions in a LayerOpacityView' +
-              '. Error details: ' + error
-            );
+          this.model.set("opacity", newOpacity);
+        } catch (error) {
+          console.log(
+            "There was an error updating the model in a LayerOpacityView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Update the label with the newOpacity displayed as a percentage
+       * @param {Number} newOpacity A number between 0 and 1 indicating the new opacity
+       * value for the MapAsset model
+       */
+      updateLabel: function (newOpacity) {
+        try {
+          if (
+            !this.opacityLabel ||
+            typeof newOpacity === "undefined" ||
+            typeof newOpacity !== "number"
+          ) {
+            return;
           }
+          var opacityPercent = Math.round(newOpacity * 100);
+          this.opacityLabel.innerText = opacityPercent + "%";
+        } catch (error) {
+          console.log(
+            "There was an error updating the opacity label in a LayerOpacityView" +
+              ". Error details: " +
+              error,
+          );
         }
+      },
+
+      /**
+       * Perform clean-up functions when this view is about to be removed from the page
+       * or navigated away from.
+       */
+      onClose: function () {
+        try {
+          this.stopListening(this.model, "change:opacity");
+        } catch (error) {
+          console.log(
+            "There was an error performing clean up functions in a LayerOpacityView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+    },
+  );
 
-      }
-    );
-
-    return LayerOpacityView;
-
-  }
-);
+  return LayerOpacityView;
+});
 
diff --git a/docs/docs/src_js_views_maps_LayersPanelView.js.html b/docs/docs/src_js_views_maps_LayersPanelView.js.html index 80d887edb..1bfde26f6 100644 --- a/docs/docs/src_js_views_maps_LayersPanelView.js.html +++ b/docs/docs/src_js_views_maps_LayersPanelView.js.html @@ -71,7 +71,7 @@

Source: src/js/views/maps/LayersPanelView.js

* @constructs LayersPanelView */ const LayersPanelView = Backbone.View.extend( - /** @lends LayersPanelView.prototype */{ + /** @lends LayersPanelView.prototype */ { /** * The type of View this is * @type {string} @@ -79,9 +79,9 @@

Source: src/js/views/maps/LayersPanelView.js

type: "LayersPanelView", /** - * The HTML classes to use for this view's element - * @type {string} - */ + * The HTML classes to use for this view's element + * @type {string} + */ className: "layers-panel", /** @@ -107,10 +107,14 @@

Source: src/js/views/maps/LayersPanelView.js

* is passed an object with relevant view state. * */ render() { - this.el.innerHTML = _.template(Template)({ classNames: this.classNames }); + this.el.innerHTML = _.template(Template)({ + classNames: this.classNames, + }); - if (this.map.get('layerCategories')?.length > 0) { - this.layersView = new LayerCategoryListView({ collection: this.map.get("layerCategories") }); + if (this.map.get("layerCategories")?.length > 0) { + this.layersView = new LayerCategoryListView({ + collection: this.map.get("layerCategories"), + }); } else { this.layersView = new LayerListView({ collection: this.map.get("layers"), @@ -122,7 +126,7 @@

Source: src/js/views/maps/LayersPanelView.js

this.searchInput = new SearchInputView({ placeholder: "Search all data layers", - search: text => this.search(text), + search: (text) => this.search(text), noMatchCallback: () => this.layersView.search(""), }); this.searchInput.render(); @@ -144,13 +148,14 @@

Source: src/js/views/maps/LayersPanelView.js

}, dismissLayerDetails() { - this.map.getLayerGroups().forEach(mapAssets => { - mapAssets.forEach(layerModel => { + this.map.getLayerGroups().forEach((mapAssets) => { + mapAssets.forEach((layerModel) => { layerModel.set("selected", false); }); }); }, - }); + }, + ); return LayersPanelView; }); diff --git a/docs/docs/src_js_views_maps_LegendView.js.html b/docs/docs/src_js_views_maps_LegendView.js.html index 436d482f0..fab08b2d8 100644 --- a/docs/docs/src_js_views_maps_LegendView.js.html +++ b/docs/docs/src_js_views_maps_LegendView.js.html @@ -44,463 +44,465 @@

Source: src/js/views/maps/LegendView.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'd3',
-    'models/maps/AssetColorPalette',
-    'text!templates/maps/legend.html',
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    d3,
-    AssetColorPalette,
-    Template
-  ) {
-
-    /**
-    * @class LegendView
-    * @classdesc Creates a legend for a given Map Asset (Work In Progress). Currently
-    * supports making 'preview' legends for CesiumImagery assets and Cesium3DTileset
-    * assets (only for color palettes that are type 'categorical'). Eventually, will
-    * support full-sized legend for these, and other assets, and all types of color
-    * palettes (including 'continuous' and 'classified')
-    * @classcategory Views/Maps
-    * @name LegendView
-    * @extends Backbone.View
-    * @screenshot views/maps/LegendView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var LegendView = Backbone.View.extend(
-      /** @lends LegendView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'LegendView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'map-legend',
-
-        /**
-        * The MapAsset model that this view uses - currently supports CesiumImagery and
-        * Cesium3DTileset models.
-        * @type {MapAsset}
-        */
-        model: null,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * @type {Object}
-        */
-        events: {
-          // 'event selector': 'function',
-        },
-
-        /**
-         * Which type of legend to show? Can be set to either 'full' for a complete legend
-         * with labels, title, and all color coding, or 'preview' for just a small
-         * thumbnail of the colors used in the full legend.
-         * @type {string}
-         */
-        mode: 'preview',
-
-        /**
-         * For vector preview legends, the relative dimensions to use. The SVG's
-         * dimensions are set with a viewBox property only, so the height and width
-         * represent an aspect ratio rather than absolute size.
-         * @type {Object}
-         * @property {number} previewSvgDimensions.width - The width of the entire SVG
-         * @property {number} previewSvgDimensions.height - The height of the entire SVG
-         * @property {number} squareSpacing - Maximum spacing between each of the squares
-         * in the preview legend. Squares will be spaced 20% closed than this when the
-         * legend is not hovered over.
-         */
-        previewSvgDimensions: {
-          width: 160,
-          height: 45,
-          squareSpacing: 20
-        },
-
-        /**
-         * Classes that are used to identify, or that are added to, the HTML elements that
-         * comprise this view.
-         * @type {Object}
-         * @property {string} preview Additional class to add to legend that are the
-         * preview/thumbnail version
-         * @property {string} previewSVG The SVG element that holds the shapes with all
-         * the legend colours in the preview legend.
-         * @property {string} previewImg The image element that represents a thumbnail of
-         * image layers, in preview legends
-         * @property {string} tooltip Class added to tooltips used in preview legends
-         */
-        classes: {
-          preview: 'map-legend--preview',
-          previewSVG: 'map-legend__svg--preview',
-          previewImg: 'map-legend__img--preview',
-          tooltip: 'map-tooltip',
-        },
-
-        /**
-        * Executed when a new LegendView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "d3",
+  "models/maps/AssetColorPalette",
+  "text!templates/maps/legend.html",
+], function ($, _, Backbone, d3, AssetColorPalette, Template) {
+  /**
+   * @class LegendView
+   * @classdesc Creates a legend for a given Map Asset (Work In Progress). Currently
+   * supports making 'preview' legends for CesiumImagery assets and Cesium3DTileset
+   * assets (only for color palettes that are type 'categorical'). Eventually, will
+   * support full-sized legend for these, and other assets, and all types of color
+   * palettes (including 'continuous' and 'classified')
+   * @classcategory Views/Maps
+   * @name LegendView
+   * @extends Backbone.View
+   * @screenshot views/maps/LegendView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var LegendView = Backbone.View.extend(
+    /** @lends LegendView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "LegendView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "map-legend",
+
+      /**
+       * The MapAsset model that this view uses - currently supports CesiumImagery and
+       * Cesium3DTileset models.
+       * @type {MapAsset}
+       */
+      model: null,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        // 'event selector': 'function',
+      },
+
+      /**
+       * Which type of legend to show? Can be set to either 'full' for a complete legend
+       * with labels, title, and all color coding, or 'preview' for just a small
+       * thumbnail of the colors used in the full legend.
+       * @type {string}
+       */
+      mode: "preview",
+
+      /**
+       * For vector preview legends, the relative dimensions to use. The SVG's
+       * dimensions are set with a viewBox property only, so the height and width
+       * represent an aspect ratio rather than absolute size.
+       * @type {Object}
+       * @property {number} previewSvgDimensions.width - The width of the entire SVG
+       * @property {number} previewSvgDimensions.height - The height of the entire SVG
+       * @property {number} squareSpacing - Maximum spacing between each of the squares
+       * in the preview legend. Squares will be spaced 20% closed than this when the
+       * legend is not hovered over.
+       */
+      previewSvgDimensions: {
+        width: 160,
+        height: 45,
+        squareSpacing: 20,
+      },
+
+      /**
+       * Classes that are used to identify, or that are added to, the HTML elements that
+       * comprise this view.
+       * @type {Object}
+       * @property {string} preview Additional class to add to legend that are the
+       * preview/thumbnail version
+       * @property {string} previewSVG The SVG element that holds the shapes with all
+       * the legend colours in the preview legend.
+       * @property {string} previewImg The image element that represents a thumbnail of
+       * image layers, in preview legends
+       * @property {string} tooltip Class added to tooltips used in preview legends
+       */
+      classes: {
+        preview: "map-legend--preview",
+        previewSVG: "map-legend__svg--preview",
+        previewImg: "map-legend__img--preview",
+        tooltip: "map-tooltip",
+      },
+
+      /**
+       * Executed when a new LegendView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-          } catch (e) {
-            console.log('A LegendView failed to initialize. Error message: ' + e);
+          }
+        } catch (e) {
+          console.log("A LegendView failed to initialize. Error message: " + e);
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {LegendView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          if (!this.model) {
+            return;
           }
 
-        },
-
-        /**
-        * Renders this view
-        * @return {LegendView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            if (!this.model) {
-              return;
-            }
+          // Save a reference to this view
+          var view = this;
 
-            // Save a reference to this view
-            var view = this;
+          // The color palette maps colors to attributes of the map asset
+          let colorPalette = null;
+          // For color palettes,
+          let paletteType = null;
+          const mode = this.mode;
 
-            // The color palette maps colors to attributes of the map asset
-            let colorPalette = null;
-            // For color palettes,
-            let paletteType = null;
-            const mode = this.mode;
+          // Insert the template into the view
+          this.$el.html(this.template({}));
 
-            // Insert the template into the view
-            this.$el.html(this.template({}));
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
 
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
+          // Add a modifier class if this is a preview of a legend
+          if (mode === "preview") {
+            this.el.classList.add(this.classes.preview);
+          }
 
-            // Add a modifier class if this is a preview of a legend
-            if (mode === 'preview') {
-              this.el.classList.add(this.classes.preview);
+          // Check for a color palette model in the Map Asset model. Even imagery layers
+          // may have a color palette configured, specifically to use to create a
+          // legend.
+          for (const attr in this.model.attributes) {
+            if (this.model.attributes[attr] instanceof AssetColorPalette) {
+              colorPalette = this.model.get(attr);
+              paletteType = colorPalette.get("paletteType");
             }
+          }
 
-            // Check for a color palette model in the Map Asset model. Even imagery layers
-            // may have a color palette configured, specifically to use to create a
-            // legend.
-            for (const attr in this.model.attributes) {
-              if (this.model.attributes[attr] instanceof AssetColorPalette) {
-                colorPalette = this.model.get(attr);
-                paletteType = colorPalette.get('paletteType')
-              }
+          if (mode === "preview") {
+            // For categorical vector color palettes, in preview mode
+            if (colorPalette && paletteType === "categorical") {
+              this.renderCategoricalPreviewLegend(colorPalette);
+            } else if (colorPalette && paletteType === "continuous") {
+              this.renderContinuousPreviewLegend(colorPalette);
             }
-
-            if (mode === 'preview') {
-              // For categorical vector color palettes, in preview mode
-              if (colorPalette && paletteType === 'categorical') {
-                this.renderCategoricalPreviewLegend(colorPalette)
-              } else if (colorPalette && paletteType === 'continuous') {
-                this.renderContinuousPreviewLegend(colorPalette)
-              }
-              // For imagery layers that do not have a color palette, in preview mode
-              else if (typeof this.model.getThumbnail === 'function') {
-                if (!this.model.get('thumbnail')) {
-                  this.listenToOnce(this.model, 'change:thumbnail', function () {
-                    this.renderImagePreviewLegend(this.model.get('thumbnail'))
-                  })
-                } else {
-                  this.renderImagePreviewLegend(this.model.get('thumbnail'))
-                }
+            // For imagery layers that do not have a color palette, in preview mode
+            else if (typeof this.model.getThumbnail === "function") {
+              if (!this.model.get("thumbnail")) {
+                this.listenToOnce(this.model, "change:thumbnail", function () {
+                  this.renderImagePreviewLegend(this.model.get("thumbnail"));
+                });
+              } else {
+                this.renderImagePreviewLegend(this.model.get("thumbnail"));
               }
             }
-            // TODO:
-            // - preview classified legend
-            // - full legends with labels, title, etc.
-
-            return this
-
           }
-          catch (error) {
-            console.log(
-              'There was an error rendering a Legend View' +
-              '. Error details: ' + error
-            );
+          // TODO:
+          // - preview classified legend
+          // - full legends with labels, title, etc.
+
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a Legend View" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Inserts a thumbnail in image into this view
+       * @param {string} thumbnailURL A url to use for the src property of the thumbnail
+       * image
+       */
+      renderImagePreviewLegend: function (thumbnailURL) {
+        try {
+          const img = new Image();
+          img.src = thumbnailURL;
+          img.classList.add(this.classes.previewImg);
+          this.el.append(img);
+        } catch (error) {
+          console.log(
+            "There was an error rendering an image preview legend in a LegendView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Creates a preview legend for categorical color palettes and inserts it into the
+       * view
+       * @param {AssetColorPalette} colorPalette - The AssetColorPalette that maps
+       * feature attributes to colors, used to create the legend
+       */
+      renderCategoricalPreviewLegend: function (colorPalette) {
+        try {
+          if (!colorPalette) {
+            return;
+          }
+          const view = this;
+          // Data to use in d3
+          let data = colorPalette.get("colors").toJSON().reverse();
+
+          if (data.length === 0) {
+            return;
           }
-        },
-
-        /**
-         * Inserts a thumbnail in image into this view
-         * @param {string} thumbnailURL A url to use for the src property of the thumbnail
-         * image
-         */
-        renderImagePreviewLegend: function (thumbnailURL) {
-          try {
-            const img = new Image()
-            img.src = thumbnailURL
-            img.classList.add(this.classes.previewImg)
-            this.el.append(img)
+          // The max width of the SVG, to be reduced if there are few colours
+          let width = this.previewSvgDimensions.width;
+          // The height of the SVG
+          const height = this.previewSvgDimensions.height;
+          // Height and width of the square is the height of the SVG, leaving some room
+          // for shadow to show
+          const squareSize = height * 0.92;
+          // Maximum spacing between squares. When not hovered, the squares will be
+          // spaced 80% of this value.
+          let squareSpacing = this.previewSvgDimensions.squareSpacing;
+          // The maximum number of squares that can fit on the SVG without any spilling
+          // over
+          const maxNumSquares = Math.floor(
+            (width - squareSize) / squareSpacing + 1,
+          );
+
+          // If there are more colors than fit in the max width of the SVG space, only
+          // show the first n squares that will fit
+          if (data.length > maxNumSquares) {
+            data = data.slice(0, maxNumSquares);
           }
-          catch (error) {
-            console.log(
-              'There was an error rendering an image preview legend in a LegendView' +
-              '. Error details: ' + error
-            );
+          // Add index to data for sorting later (also works as unique ID)
+          data.forEach(function (d, i) {
+            d.i = i;
+          });
+
+          // Don't create an SVG that is wider than it need to be.
+          width = squareSize + (data.length - 1) * squareSpacing;
+
+          // SVG element
+          const svg = this.createSVG({
+            dropshadowFilter: true,
+            width: width,
+            height: height,
+          });
+
+          // Add the preview class and dropshadow to the SVG
+          svg.classed(this.classes.previewSVG, true);
+          svg.style("filter", "url(#dropshadow)");
+
+          // Calculates the placement of the square along x-axis, when SVG is hovered
+          // and when it's not
+          function getSquareX(i, hovered) {
+            const multiplier = hovered ? 1 : 0.8;
+            return width - squareSize - i * (squareSpacing * multiplier);
           }
-        },
-
-        /**
-         * Creates a preview legend for categorical color palettes and inserts it into the
-         * view
-         * @param {AssetColorPalette} colorPalette - The AssetColorPalette that maps
-         * feature attributes to colors, used to create the legend
-         */
-        renderCategoricalPreviewLegend: function (colorPalette) {
-          try {
-            if (!colorPalette) {
-              return
-            }
-            const view = this
-            // Data to use in d3
-            let data = colorPalette.get('colors').toJSON().reverse();
-
-            if (data.length === 0) {
-              return;
-            }
-            // The max width of the SVG, to be reduced if there are few colours
-            let width = this.previewSvgDimensions.width
-            // The height of the SVG
-            const height = this.previewSvgDimensions.height
-            // Height and width of the square is the height of the SVG, leaving some room
-            // for shadow to show
-            const squareSize = height * 0.92
-            // Maximum spacing between squares. When not hovered, the squares will be
-            // spaced 80% of this value.
-            let squareSpacing = this.previewSvgDimensions.squareSpacing
-            // The maximum number of squares that can fit on the SVG without any spilling
-            // over
-            const maxNumSquares = Math.floor(((width - squareSize) / squareSpacing) + 1)
-
-            // If there are more colors than fit in the max width of the SVG space, only
-            // show the first n squares that will fit
-            if (data.length > maxNumSquares) {
-              data = data.slice(0, maxNumSquares);
-            }
-            // Add index to data for sorting later (also works as unique ID)
-            data.forEach(function (d, i) {
-              d.i = i;
-            });
 
-            // Don't create an SVG that is wider than it need to be.
-            width = squareSize + ((data.length - 1) * squareSpacing)
-
-            // SVG element
-            const svg = this.createSVG({
-              dropshadowFilter: true,
-              width: width,
-              height: height,
+          // Draw the legend (d3)
+          const legendSquares = svg
+            .selectAll("rect")
+            .data(data)
+            .enter()
+            .append("rect")
+            .attr("x", function (d, i) {
+              return getSquareX(i, false);
             })
-
-            // Add the preview class and dropshadow to the SVG
-            svg.classed(this.classes.previewSVG, true)
-            svg.style('filter', 'url(#dropshadow)')
-
-            // Calculates the placement of the square along x-axis, when SVG is hovered
-            // and when it's not
-            function getSquareX(i, hovered) {
-              const multiplier = hovered ? 1 : 0.8;
-              return ((width - squareSize) - (i * (squareSpacing * multiplier)))
-            }
-
-            // Draw the legend (d3)
-            const legendSquares = svg.selectAll('rect')
-              .data(data)
-              .enter()
-              .append('rect')
-              .attr('x', function (d, i) { return getSquareX(i, false) })
-              .attr('height', squareSize)
-              .attr('width', squareSize)
-              .attr('rx', (squareSize * 0.1))
-              .style('fill', function (d) {
-                return `rgb(${d.color.red * 255},${d.color.green * 255},${d.color.blue * 255})`
-              })
-              .style('filter', 'url(#dropshadow)')
-
-            // For legend with multiple colours, show a tooltip with the value/label when
-            // the user hovers over a square. Also bring that square to the fore-front of
-            // the legend when hovered. Only when MapAsset is visible though.
-            if (data.length > 1) {
-
-              // Space the squares further apart when they are hovered over
-              svg
-                .on('mouseenter', function () {
-                  if (view.model.get('visible')) {
-                    legendSquares
-                      .transition()
-                      .duration(250)
-                      .attr('x', function (d, i) { return getSquareX(i, true) })
-                  }
-                })
-                .on('mouseleave', function () {
+            .attr("height", squareSize)
+            .attr("width", squareSize)
+            .attr("rx", squareSize * 0.1)
+            .style("fill", function (d) {
+              return `rgb(${d.color.red * 255},${d.color.green * 255},${d.color.blue * 255})`;
+            })
+            .style("filter", "url(#dropshadow)");
+
+          // For legend with multiple colours, show a tooltip with the value/label when
+          // the user hovers over a square. Also bring that square to the fore-front of
+          // the legend when hovered. Only when MapAsset is visible though.
+          if (data.length > 1) {
+            // Space the squares further apart when they are hovered over
+            svg
+              .on("mouseenter", function () {
+                if (view.model.get("visible")) {
                   legendSquares
                     .transition()
-                    .duration(200)
-                    .attr('x', function (d, i) { return getSquareX(i, false) })
-                })
-
-              legendSquares.on('mouseenter', function (d) {
+                    .duration(250)
+                    .attr("x", function (d, i) {
+                      return getSquareX(i, true);
+                    });
+                }
+              })
+              .on("mouseleave", function () {
+                legendSquares
+                  .transition()
+                  .duration(200)
+                  .attr("x", function (d, i) {
+                    return getSquareX(i, false);
+                  });
+              });
+
+            legendSquares
+              .on("mouseenter", function (d) {
                 // Bring the hovered element to the front, while keeping other
                 // legendSquares in order
                 legendSquares.sort((a, b) => d3.ascending(a.i, b.i));
-                this.parentNode.appendChild(this)
+                this.parentNode.appendChild(this);
                 // Show tooltip
                 if (d.label || d.value || d.value === 0) {
-                  $(this).tooltip({
-                    placement: 'bottom',
-                    trigger: 'manual',
-                    title: d.label || d.value,
-                    container: view.$el,
-                    animation: false,
-                    template: '<div class="tooltip ' + view.classes.tooltip + '"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
-                  }).tooltip('show')
+                  $(this)
+                    .tooltip({
+                      placement: "bottom",
+                      trigger: "manual",
+                      title: d.label || d.value,
+                      container: view.$el,
+                      animation: false,
+                      template:
+                        '<div class="tooltip ' +
+                        view.classes.tooltip +
+                        '"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+                    })
+                    .tooltip("show");
                 }
               })
-                // Hide tooltip and return squares to regular z-ordering
-                .on('mouseleave', function (d) {
-                  $(this).tooltip('destroy');
-                  legendSquares.sort((a, b) => d3.ascending(a.i, b.i));
-                })
-            }
+              // Hide tooltip and return squares to regular z-ordering
+              .on("mouseleave", function (d) {
+                $(this).tooltip("destroy");
+                legendSquares.sort((a, b) => d3.ascending(a.i, b.i));
+              });
           }
-          catch (error) {
-            console.log(
-              'There was an error creating a categorical legend preview in a LegendView' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error creating a categorical legend preview in a LegendView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Creates a preview legend for continuous color palettes and inserts it into the
+       * view
+       * @param {AssetColorPalette} colorPalette - The AssetColorPalette that maps
+       * feature attributes to colors, used to create the legend
+       */
+      renderContinuousPreviewLegend: function (colorPalette) {
+        try {
+          if (!colorPalette) {
+            return;
+          }
+          const view = this;
+          // Data to use in d3
+          let data = colorPalette.get("colors").toJSON();
+          // The max width of the SVG
+          let width = this.previewSvgDimensions.width;
+          // The height of the SVG
+          const height = this.previewSvgDimensions.height;
+          // Height of the gradient rectangle, leaving some room for the drop shadow
+          const gradientHeight = height * 0.92;
+
+          // A unique ID for the gradient
+          const gradientId = "gradient-" + view.cid;
+
+          // Calculate the rounding precision we should use based on the
+          // range of the data. This determines how each value in the legend
+          // is displayed in the tooltip on mouseover. See the
+          // rect.on('mousemove'... function, below
+          data = data.sort((a, b) => a.value - b.value);
+          const min = data[0].value;
+          const max = data[data.length - 1].value;
+          const range = max - min;
+          let roundingConstant = 10; // Allow 1 decimal place by default
+          if (range < 0.0001 || range > 100000) {
+            roundingConstant = null; // Will use scientific notation
+          } else if (range < 0.001) {
+            roundingConstant = 100000; // Allow 5 decimal places
+          } else if (range < 0.01) {
+            roundingConstant = 10000; // Allow 4 decimal places
+          } else if (range < 0.1) {
+            roundingConstant = 1000; // Allow 3 decimal places
+          } else if (range < 1) {
+            roundingConstant = 100; // Allow 2 decimal places
+          } else if (range > 100) {
+            roundingConstant = 1; // No decimal places
           }
-        },
-
-        /**
-         * Creates a preview legend for continuous color palettes and inserts it into the
-         * view
-         * @param {AssetColorPalette} colorPalette - The AssetColorPalette that maps
-         * feature attributes to colors, used to create the legend
-         */
-        renderContinuousPreviewLegend: function (colorPalette) {
-          try {
-            if (!colorPalette) {
-              return
-            }
-            const view = this
-            // Data to use in d3
-            let data = colorPalette.get('colors').toJSON();
-            // The max width of the SVG
-            let width = this.previewSvgDimensions.width
-            // The height of the SVG
-            const height = this.previewSvgDimensions.height
-            // Height of the gradient rectangle, leaving some room for the drop shadow
-            const gradientHeight = height * 0.92
-
-            // A unique ID for the gradient
-            const gradientId = 'gradient-' + view.cid;
-
-            // Calculate the rounding precision we should use based on the
-            // range of the data. This determines how each value in the legend
-            // is displayed in the tooltip on mouseover. See the
-            // rect.on('mousemove'... function, below
-            data = data.sort((a, b) => a.value - b.value);
-            const min = data[0].value
-            const max = data[data.length - 1].value
-            const range = max - min
-            let roundingConstant = 10 // Allow 1 decimal place by default
-            if (range < 0.0001 || range > 100000) {
-              roundingConstant = null // Will use scientific notation
-            } else if (range < 0.001) {
-              roundingConstant = 100000 // Allow 5 decimal places
-            } else if (range < 0.01) {
-              roundingConstant = 10000 // Allow 4 decimal places
-            } else if (range < 0.1) {
-              roundingConstant = 1000 // Allow 3 decimal places
-            } else if (range < 1) {
-              roundingConstant = 100 // Allow 2 decimal places
-            } else if (range > 100) {
-              roundingConstant = 1 // No decimal places
-            }
-
-            // SVG element
-            const svg = this.createSVG({
-              dropshadowFilter: false,
-              width: width,
-              height: height,
-            })
-
-            // Add the preview class and dropshadow to the SVG
-            svg.classed(this.classes.previewSVG, true)
-            svg.style('filter', 'url(#dropshadow)')
-
-            // Create a gradient using the data
-            const gradient = svg.append('defs')
-              .append('linearGradient')
-              .attr('id', gradientId)
-              .attr('x1', '0%')
-              .attr('y1', '0%')
-
-            var getOffset = function (d, data) {
-              return (d.value - min) / (range) * 100 + '%'
-            }
-            var getStopColor = function (d) {
-              const r = d.color.red * 255
-              const g = d.color.green * 255
-              const b = d.color.blue * 255
-              return `rgb(${r},${g},${b})`
-            }
-
-            // Add the gradient stops
-            data.forEach(function (d, i) {
-              gradient.append('stop')
-                // offset should be relative to the value in the data
-                .attr('offset', getOffset(d, data))
-                .attr('stop-color', getStopColor(d))
-            })
 
-            // Create the rectangle
-            const rect = svg.append('rect')
-              .attr('x', 0)
-              .attr('y', 0)
-              .attr('width', width)
-              .attr('height', gradientHeight)
-              .attr('rx', (gradientHeight * 0.1))
-              .style('fill', 'url(#' + gradientId + ')')
-
-            // Create a proxy element to attach the tooltip to, so that we can move the
-            // tooltip to follow the mouse (by moving the proxy element to follow the mouse)
-            const proxyEl = svg.append('rect').attr('y', gradientHeight)
-
-            rect.on('mousemove', function () {
-              if (view.model.get('visible')) {
+          // SVG element
+          const svg = this.createSVG({
+            dropshadowFilter: false,
+            width: width,
+            height: height,
+          });
+
+          // Add the preview class and dropshadow to the SVG
+          svg.classed(this.classes.previewSVG, true);
+          svg.style("filter", "url(#dropshadow)");
+
+          // Create a gradient using the data
+          const gradient = svg
+            .append("defs")
+            .append("linearGradient")
+            .attr("id", gradientId)
+            .attr("x1", "0%")
+            .attr("y1", "0%");
+
+          var getOffset = function (d, data) {
+            return ((d.value - min) / range) * 100 + "%";
+          };
+          var getStopColor = function (d) {
+            const r = d.color.red * 255;
+            const g = d.color.green * 255;
+            const b = d.color.blue * 255;
+            return `rgb(${r},${g},${b})`;
+          };
+
+          // Add the gradient stops
+          data.forEach(function (d, i) {
+            gradient
+              .append("stop")
+              // offset should be relative to the value in the data
+              .attr("offset", getOffset(d, data))
+              .attr("stop-color", getStopColor(d));
+          });
+
+          // Create the rectangle
+          const rect = svg
+            .append("rect")
+            .attr("x", 0)
+            .attr("y", 0)
+            .attr("width", width)
+            .attr("height", gradientHeight)
+            .attr("rx", gradientHeight * 0.1)
+            .style("fill", "url(#" + gradientId + ")");
+
+          // Create a proxy element to attach the tooltip to, so that we can move the
+          // tooltip to follow the mouse (by moving the proxy element to follow the mouse)
+          const proxyEl = svg.append("rect").attr("y", gradientHeight);
+
+          rect
+            .on("mousemove", function () {
+              if (view.model.get("visible")) {
                 // Get the coordinates of the mouse relative to the rectangle
                 let xMouse = d3.mouse(this)[0];
                 if (xMouse < 0) {
@@ -512,74 +514,82 @@ 

Source: src/js/views/maps/LegendView.js

// Get the relative position of the mouse to the gradient const relativePosition = xMouse / width; // Get the value at the relative position by interpolating the data - let value = d3.interpolate(data[0].value, data[data.length - 1].value)(relativePosition); + let value = d3.interpolate( + data[0].value, + data[data.length - 1].value, + )(relativePosition); // Show tooltip with the value if (value || value === 0) { // Round or show in scientific notation if (roundingConstant) { - value = (Math.round(value * roundingConstant) / roundingConstant).toString() + value = ( + Math.round(value * roundingConstant) / roundingConstant + ).toString(); } else { - value = value.toExponential(2).toString() + value = value.toExponential(2).toString(); } // Move the proxy element to follow the mouse - proxyEl.attr('x', xMouse) + proxyEl.attr("x", xMouse); // Attach the tooltip to the proxy element. Tooltip needs to be // refreshed every time the mouse moves - $(proxyEl).tooltip('destroy'); - $(proxyEl).tooltip({ - placement: 'bottom', - trigger: 'manual', - title: value, - container: view.$el, - animation: false, - template: '<div class="tooltip ' + view.classes.tooltip + '"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' - }).tooltip('show') + $(proxyEl).tooltip("destroy"); + $(proxyEl) + .tooltip({ + placement: "bottom", + trigger: "manual", + title: value, + container: view.$el, + animation: false, + template: + '<div class="tooltip ' + + view.classes.tooltip + + '"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + }) + .tooltip("show"); } } }) - // Hide tooltip - .on('mouseleave', function () { - $(proxyEl).tooltip('destroy'); - }) - - } - catch (error) { - console.log( - 'There was an error rendering a continuous preview legend in a LegendView' + - '. Error details: ' + error - ); - } - }, - - /** - * Creates an SVG element and inserts it into the view - * @param {object} options Used to configure parts of the SVG - * @property {boolean} options.dropshadowFilter Set to true to create a filter - * element that creates a dropshadow behind any element it is applied to. It can - * be added to child elements of the SVG by setting a `filter: url(#dropshadow);` - * style rule on the child. - * @property {number} options.height The relative height of the SVG (for the - * viewBox property) - * @property {number} options.width The relative width of the SVG (for the viewBox - * property) - * @returns {SVG} Returns the SVG element that is in the view - */ - createSVG: function (options = {}) { - try { - // Create an SVG to hold legend elements - const container = this.el; - const width = options.width; - const height = options.height; - - const svg = d3.select(container) - .append('svg') - .attr('preserveAspectRatio', 'xMidYMid') - .attr('viewBox', [0, 0, width, height]); - - if (options.dropshadowFilter) { - - const filterText = - `<filter id="dropshadow" height="110%"> + // Hide tooltip + .on("mouseleave", function () { + $(proxyEl).tooltip("destroy"); + }); + } catch (error) { + console.log( + "There was an error rendering a continuous preview legend in a LegendView" + + ". Error details: " + + error, + ); + } + }, + + /** + * Creates an SVG element and inserts it into the view + * @param {object} options Used to configure parts of the SVG + * @property {boolean} options.dropshadowFilter Set to true to create a filter + * element that creates a dropshadow behind any element it is applied to. It can + * be added to child elements of the SVG by setting a `filter: url(#dropshadow);` + * style rule on the child. + * @property {number} options.height The relative height of the SVG (for the + * viewBox property) + * @property {number} options.width The relative width of the SVG (for the viewBox + * property) + * @returns {SVG} Returns the SVG element that is in the view + */ + createSVG: function (options = {}) { + try { + // Create an SVG to hold legend elements + const container = this.el; + const width = options.width; + const height = options.height; + + const svg = d3 + .select(container) + .append("svg") + .attr("preserveAspectRatio", "xMidYMid") + .attr("viewBox", [0, 0, width, height]); + + if (options.dropshadowFilter) { + const filterText = `<filter id="dropshadow" height="110%"> <feGaussianBlur in="SourceAlpha" stdDeviation="2"/> <!-- stdDeviation is how much to blur --> <feOffset dx="1" dy="1" result="offsetblur"/> <!-- how much to offset --> <feComponentTransfer> @@ -589,34 +599,32 @@

Source: src/js/views/maps/LegendView.js

<feMergeNode/> <!-- this contains the offset blurred image --> <feMergeNode in="SourceGraphic"/> <!-- this contains the element that the filter is applied to --> </feMerge> - </filter>` + </filter>`; - const filterEl = new DOMParser().parseFromString( - '<svg xmlns="http://www.w3.org/2000/svg">' + filterText + '</svg>', - 'application/xml' - ).documentElement.firstChild + const filterEl = new DOMParser().parseFromString( + '<svg xmlns="http://www.w3.org/2000/svg">' + + filterText + + "</svg>", + "application/xml", + ).documentElement.firstChild; - svg.node().appendChild(document.importNode(filterEl, true)) - } - - return svg + svg.node().appendChild(document.importNode(filterEl, true)); } - catch (error) { - console.log( - 'There was an error creating an SVG in a LegendView' + - '. Error details: ' + error - ); - } - } + return svg; + } catch (error) { + console.log( + "There was an error creating an SVG in a LegendView" + + ". Error details: " + + error, + ); + } + }, + }, + ); - } - ); - - return LegendView; - - } -); + return LegendView; +});
diff --git a/docs/docs/src_js_views_maps_MapView.js.html b/docs/docs/src_js_views_maps_MapView.js.html index e76426b7f..547f689fa 100644 --- a/docs/docs/src_js_views_maps_MapView.js.html +++ b/docs/docs/src_js_views_maps_MapView.js.html @@ -44,354 +44,358 @@

Source: src/js/views/maps/MapView.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'models/maps/Map',
-    'text!templates/maps/map.html',
-    // SubViews
-    'views/maps/CesiumWidgetView',
-    'views/maps/ToolbarView',
-    'views/maps/ScaleBarView',
-    'views/maps/FeatureInfoView',
-    'views/maps/LayerDetailsView',
-    // CSS
-    'text!' + MetacatUI.root + '/css/map-view.css',
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    Map,
-    Template,
-    // SubViews
-    CesiumWidgetView,
-    ToolbarView,
-    ScaleBarView,
-    FeatureInfoView,
-    LayerDetailsView,
-    // CSS
-    MapCSS
-  ) {
-    const CLASS_NAMES = {
-      mapWidgetContainer: 'map-view__map-widget-container',
-      scaleBarContainer: 'map-view__scale-bar-container',
-      featureInfoContainer: 'map-view__feature-info-container',
-      toolbarContainer: 'map-view__toolbar-container',
-      layerDetailsContainer: 'map-view__layer-details-container',
-      portalIndicator: 'map-view__portal',
-    };
-
-    /**
-    * @class MapView
-    * @classdesc An interactive 2D or 3D map that allows visualization of geo-spatial
-    * data.
-    * @classcategory Views/Maps
-    * @name MapView
-    * @extends Backbone.View
-    * @screenshot views/maps/MapView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var MapView = Backbone.View.extend(
-      /** @lends MapView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'MapView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'map-view',
-
-        /**
-        * The model that this view uses
-        * @type {Map}
-        */
-        model: null,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * @type {Object}
-        */
-        events: {
-          // 'event selector': 'function',
-        },
-
-        /**
-        * @typedef {Object} ViewfinderViewOptions
-        * @property {Map} model The map model that contains the configs for this map view.
-        * @property {boolean} isPortalMap Indicates whether the map view is a part of a
-        * portal, which is styled differently.
-        */
-
-        /**
-        * Executed when a new MapView is created
-        * @param {ViewfinderViewOptions} options
-        */
-        initialize: function (options) {
-          // Add the CSS required for this view and its sub-views.
-          MetacatUI.appModel.addCSS(MapCSS, 'mapView');
-
-          this.model = options?.model ? options.model : new Map();
-          this.isPortalMap = options?.isPortalMap;
-        },
-
-        /**
-        * Renders this view
-        * @return {MapView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-
-            // TODO: Add a nice loading animation?
-
-            // Insert the template into the view
-            this.$el.html(this.template());
-
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-            if (this.isPortalMap) {
-              this.el.classList.add(CLASS_NAMES.portalIndicator);
-            }
-
-            // Select the elements that will be updatable
-            this.subElements = {};
-            for (const [element, className] of Object.entries(CLASS_NAMES)) {
-              view.subElements[element] = document.querySelector('.' + className)
-            }
-
-            // Render the (Cesium) map
-            this.renderMapWidget();
-
-            // Optionally add the toolbar, layer details, scale bar, and feature info box.
-            if (this.model.get('showToolbar')) {
-              this.renderToolbar();
-              this.renderLayerDetails();
-            }
-            if (this.model.get('showScaleBar')) {
-              this.renderScaleBar();
-            }
-            if (
-              this.model.get('showFeatureInfo') &
-              this.model.get('clickFeatureAction') === 'showDetails'
-            ) {
-              this.renderFeatureInfo();
-            }
-
-            // Return this MapView
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a MapView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Renders the view that shows the map/globe and all of the geo-spatial data.
-         * Currently, this uses the CesiumWidgetView, but this function could be modified
-         * to use an alternative map widget in the future.
-         * @returns {CesiumWidgetView} Returns the rendered view
-         */
-        renderMapWidget: function () {
-          try {
-            this.mapWidget = new CesiumWidgetView({
-              el: this.subElements.mapWidgetContainer,
-              model: this.model
-            })
-            this.mapWidget.render()
-            return this.mapWidget
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/maps/Map",
+  "text!templates/maps/map.html",
+  // SubViews
+  "views/maps/CesiumWidgetView",
+  "views/maps/ToolbarView",
+  "views/maps/ScaleBarView",
+  "views/maps/FeatureInfoView",
+  "views/maps/LayerDetailsView",
+  // CSS
+  "text!" + MetacatUI.root + "/css/map-view.css",
+], function (
+  $,
+  _,
+  Backbone,
+  Map,
+  Template,
+  // SubViews
+  CesiumWidgetView,
+  ToolbarView,
+  ScaleBarView,
+  FeatureInfoView,
+  LayerDetailsView,
+  // CSS
+  MapCSS,
+) {
+  const CLASS_NAMES = {
+    mapWidgetContainer: "map-view__map-widget-container",
+    scaleBarContainer: "map-view__scale-bar-container",
+    featureInfoContainer: "map-view__feature-info-container",
+    toolbarContainer: "map-view__toolbar-container",
+    layerDetailsContainer: "map-view__layer-details-container",
+    portalIndicator: "map-view__portal",
+  };
+
+  /**
+   * @class MapView
+   * @classdesc An interactive 2D or 3D map that allows visualization of geo-spatial
+   * data.
+   * @classcategory Views/Maps
+   * @name MapView
+   * @extends Backbone.View
+   * @screenshot views/maps/MapView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var MapView = Backbone.View.extend(
+    /** @lends MapView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "MapView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "map-view",
+
+      /**
+       * The model that this view uses
+       * @type {Map}
+       */
+      model: null,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        // 'event selector': 'function',
+      },
+
+      /**
+       * @typedef {Object} ViewfinderViewOptions
+       * @property {Map} model The map model that contains the configs for this map view.
+       * @property {boolean} isPortalMap Indicates whether the map view is a part of a
+       * portal, which is styled differently.
+       */
+
+      /**
+       * Executed when a new MapView is created
+       * @param {ViewfinderViewOptions} options
+       */
+      initialize: function (options) {
+        // Add the CSS required for this view and its sub-views.
+        MetacatUI.appModel.addCSS(MapCSS, "mapView");
+
+        this.model = options?.model ? options.model : new Map();
+        this.isPortalMap = options?.isPortalMap;
+      },
+
+      /**
+       * Renders this view
+       * @return {MapView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // TODO: Add a nice loading animation?
+
+          // Insert the template into the view
+          this.$el.html(this.template());
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+          if (this.isPortalMap) {
+            this.el.classList.add(CLASS_NAMES.portalIndicator);
           }
-          catch (error) {
-            console.log(
-              'There was an error rendering the map widget in a MapView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Renders the toolbar element that contains sections for viewing and editing the
-         * layer list.
-         * @returns {ToolbarView} Returns the rendered view
-         */
-        renderToolbar: function () {
-          try {
-            this.toolbar = new ToolbarView({
-              el: this.subElements.toolbarContainer,
-              model: this.model
-            })
-            this.toolbar.render()
-            return this.toolbar
+
+          // Select the elements that will be updatable
+          this.subElements = {};
+          for (const [element, className] of Object.entries(CLASS_NAMES)) {
+            view.subElements[element] = document.querySelector("." + className);
           }
-          catch (error) {
-            console.log(
-              'There was an error rendering a toolbarView in a MapView' +
-              '. Error details: ' + error
-            );
+
+          // Render the (Cesium) map
+          this.renderMapWidget();
+
+          // Optionally add the toolbar, layer details, scale bar, and feature info box.
+          if (this.model.get("showToolbar")) {
+            this.renderToolbar();
+            this.renderLayerDetails();
           }
-        },
-
-        /**
-         * Renders the info box that is displayed when a user clicks on a feature on the
-         * map. If there are multiple features selected, this will show information for
-         * the first one only.
-         * @returns {FeatureInfoView}  Returns the rendered view
-         */
-        renderFeatureInfo: function () {
-          try {
-            const view = this;
-            const interactions = view.model.get('interactions')
-            const features = view.model.getSelectedFeatures();
-
-            view.featureInfo = new FeatureInfoView({
-              el: view.subElements.featureInfoContainer,
-              model: features.at(0)
-            }).render()
-
-            // When the selectedFeatures collection changes, update the feature
-            // info view
-            view.stopListening(features, 'update')
-            view.listenTo(features, 'update', function () {
-              view.featureInfo.changeModel(features.at(-1))
-            })
-
-            // If the Feature model is ever completely replaced for any reason,
-            // make the the Feature Info view gets updated.
-            const event = 'change:selectedFeatures'
-            view.stopListening(interactions, event)
-            view.listenTo(interactions, event, view.renderFeatureInfo);
-            return view.featureInfo
+          if (this.model.get("showScaleBar")) {
+            this.renderScaleBar();
           }
-          catch (e) {
-            console.log('Error rendering a FeatureInfoView in a MapView', e);
+          if (
+            this.model.get("showFeatureInfo") &
+            (this.model.get("clickFeatureAction") === "showDetails")
+          ) {
+            this.renderFeatureInfo();
           }
-        },
-
-        /**
-         * Renders the layer details view that is displayed when a user clicks on a layer
-         * in the toolbar.
-         * @returns {LayerDetailsView} Returns the rendered view
-         */
-        renderLayerDetails: function () {
-          this.layerDetails = new LayerDetailsView({
-            el: this.subElements.layerDetailsContainer
+
+          // Return this MapView
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a MapView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Renders the view that shows the map/globe and all of the geo-spatial data.
+       * Currently, this uses the CesiumWidgetView, but this function could be modified
+       * to use an alternative map widget in the future.
+       * @returns {CesiumWidgetView} Returns the rendered view
+       */
+      renderMapWidget: function () {
+        try {
+          this.mapWidget = new CesiumWidgetView({
+            el: this.subElements.mapWidgetContainer,
+            model: this.model,
+          });
+          this.mapWidget.render();
+          return this.mapWidget;
+        } catch (error) {
+          console.log(
+            "There was an error rendering the map widget in a MapView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Renders the toolbar element that contains sections for viewing and editing the
+       * layer list.
+       * @returns {ToolbarView} Returns the rendered view
+       */
+      renderToolbar: function () {
+        try {
+          this.toolbar = new ToolbarView({
+            el: this.subElements.toolbarContainer,
+            model: this.model,
+          });
+          this.toolbar.render();
+          return this.toolbar;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a toolbarView in a MapView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Renders the info box that is displayed when a user clicks on a feature on the
+       * map. If there are multiple features selected, this will show information for
+       * the first one only.
+       * @returns {FeatureInfoView}  Returns the rendered view
+       */
+      renderFeatureInfo: function () {
+        try {
+          const view = this;
+          const interactions = view.model.get("interactions");
+          const features = view.model.getSelectedFeatures();
+
+          view.featureInfo = new FeatureInfoView({
+            el: view.subElements.featureInfoContainer,
+            model: features.at(0),
+          }).render();
+
+          // When the selectedFeatures collection changes, update the feature
+          // info view
+          view.stopListening(features, "update");
+          view.listenTo(features, "update", function () {
+            view.featureInfo.changeModel(features.at(-1));
           });
-          this.layerDetails.render();
-
-          // When a layer is selected, show the layer details panel. When a layer is
-          // de-selected, close it. The Layer model's 'selected' attribute gets updated
-          // from the Layer Item View, and also from the Layers collection.
-          for (const layers of this.model.getLayerGroups()) {
-            this.stopListening(layers);
-            this.listenTo(layers, 'change:selected',
-              function (layerModel, selected) {
-                if (selected === false) {
-                  this.layerDetails.updateModel(null);
-                  this.layerDetails.close();
-                } else {
-                  this.layerDetails.updateModel(layerModel);
-                  this.layerDetails.open();
-                }
+
+          // If the Feature model is ever completely replaced for any reason,
+          // make the the Feature Info view gets updated.
+          const event = "change:selectedFeatures";
+          view.stopListening(interactions, event);
+          view.listenTo(interactions, event, view.renderFeatureInfo);
+          return view.featureInfo;
+        } catch (e) {
+          console.log("Error rendering a FeatureInfoView in a MapView", e);
+        }
+      },
+
+      /**
+       * Renders the layer details view that is displayed when a user clicks on a layer
+       * in the toolbar.
+       * @returns {LayerDetailsView} Returns the rendered view
+       */
+      renderLayerDetails: function () {
+        this.layerDetails = new LayerDetailsView({
+          el: this.subElements.layerDetailsContainer,
+        });
+        this.layerDetails.render();
+
+        // When a layer is selected, show the layer details panel. When a layer is
+        // de-selected, close it. The Layer model's 'selected' attribute gets updated
+        // from the Layer Item View, and also from the Layers collection.
+        for (const layers of this.model.getLayerGroups()) {
+          this.stopListening(layers);
+          this.listenTo(
+            layers,
+            "change:selected",
+            function (layerModel, selected) {
+              if (selected === false) {
+                this.layerDetails.updateModel(null);
+                this.layerDetails.close();
+              } else {
+                this.layerDetails.updateModel(layerModel);
+                this.layerDetails.open();
               }
-            );
-          }
+            },
+          );
+        }
 
-          return this.layerDetails;
-        },
-
-        /**
-         * Renders the scale bar view that shows the current position of the mouse on the
-         * map.
-         * @returns {ScaleBarView} Returns the rendered view
-         */
-        renderScaleBar: function () {
-          try {
-            const interactions = this.model.get('interactions')
-            if (!interactions) {
-              this.listenToOnce(this.model, 'change:interactions', this.renderScaleBar);
-              return
-            }
-            this.scaleBar = new ScaleBarView({
-              el: this.subElements.scaleBarContainer,
-              scaleModel: interactions.get('scale'),
-              pointModel: interactions.get('mousePosition')
-            })
-            this.scaleBar.render();
-
-            // If the interaction model or relevant sub-models are ever completely
-            // replaced for any reason, re-render the scale bar.
-            this.listenToOnce(interactions, 'change:scale change:mousePosition', this.renderScaleBar);
-            this.listenToOnce(this.model, 'change:interactions', this.renderScaleBar);
-
-            return this.scaleBar;
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a ScaleBarView in a MapView' +
-              '. Error details: ' + error
+        return this.layerDetails;
+      },
+
+      /**
+       * Renders the scale bar view that shows the current position of the mouse on the
+       * map.
+       * @returns {ScaleBarView} Returns the rendered view
+       */
+      renderScaleBar: function () {
+        try {
+          const interactions = this.model.get("interactions");
+          if (!interactions) {
+            this.listenToOnce(
+              this.model,
+              "change:interactions",
+              this.renderScaleBar,
             );
+            return;
           }
-        },
-
-        /**
-         * Get a list of the views that this view contains.
-         * @returns {Backbone.View[]} Returns an array of all of the sub-views.
-         * Some may be undefined if they have not been rendered yet.
-         * @since 2.27.0
-         */
-        getSubViews: function () {
-          return [
-            this.mapWidget,
-            this.toolbar,
-            this.featureInfo,
-            this.layerDetails,
-            this.scaleBar
-          ]
-        },
-
-        /**
-         * Executed when the view is closed. This will close all of the sub-views.
-         * @since 2.27.0
-         */
-        onClose: function () {
-          const subViews = this.getSubViews()
-          subViews.forEach(subView => {
-            if (subView && typeof subView.onClose === 'function') {
-              subView.onClose()
-            }
-          })
+          this.scaleBar = new ScaleBarView({
+            el: this.subElements.scaleBarContainer,
+            scaleModel: interactions.get("scale"),
+            pointModel: interactions.get("mousePosition"),
+          });
+          this.scaleBar.render();
+
+          // If the interaction model or relevant sub-models are ever completely
+          // replaced for any reason, re-render the scale bar.
+          this.listenToOnce(
+            interactions,
+            "change:scale change:mousePosition",
+            this.renderScaleBar,
+          );
+          this.listenToOnce(
+            this.model,
+            "change:interactions",
+            this.renderScaleBar,
+          );
+
+          return this.scaleBar;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a ScaleBarView in a MapView" +
+              ". Error details: " +
+              error,
+          );
         }
+      },
+
+      /**
+       * Get a list of the views that this view contains.
+       * @returns {Backbone.View[]} Returns an array of all of the sub-views.
+       * Some may be undefined if they have not been rendered yet.
+       * @since 2.27.0
+       */
+      getSubViews: function () {
+        return [
+          this.mapWidget,
+          this.toolbar,
+          this.featureInfo,
+          this.layerDetails,
+          this.scaleBar,
+        ];
+      },
+
+      /**
+       * Executed when the view is closed. This will close all of the sub-views.
+       * @since 2.27.0
+       */
+      onClose: function () {
+        const subViews = this.getSubViews();
+        subViews.forEach((subView) => {
+          if (subView && typeof subView.onClose === "function") {
+            subView.onClose();
+          }
+        });
+      },
+    },
+  );
 
-      }
-    );
-
-    return MapView;
-
-  }
-);
+  return MapView;
+});
 
diff --git a/docs/docs/src_js_views_maps_ScaleBarView.js.html b/docs/docs/src_js_views_maps_ScaleBarView.js.html index 29298815f..fbe0fd694 100644 --- a/docs/docs/src_js_views_maps_ScaleBarView.js.html +++ b/docs/docs/src_js_views_maps_ScaleBarView.js.html @@ -44,425 +44,374 @@

Source: src/js/views/maps/ScaleBarView.js

-

-'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'text!templates/maps/scale-bar.html'
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    Template
-  ) {
-
-    /**
-    * @class ScaleBarView
-    * @classdesc The scale bar is a legend for a map that shows the current longitude,
-    * latitude, and elevation, as well as a scale bar to indicate the relative size of
-    * geo-spatial features.
-    * @classcategory Views/Maps
-    * @name ScaleBarView
-    * @extends Backbone.View
-    * @screenshot views/maps/ScaleBarView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var ScaleBarView = Backbone.View.extend(
-      /** @lends ScaleBarView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'ScaleBarView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'scale-bar',
-
-        /**
-         * The model that holds the current scale of the map in pixels:meters
-         * @type {GeoScale}
-         * @since 2.27.0
-         */
-        scaleModel: null,
-
-        /**
-         * The model that holds the current position of the mouse on the map
-         * @type {GeoPoint}
-         * @since 2.27.0
-         */
-        pointModel: null,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * Classes that will be used to select elements from the template that will be
-         * updated with new coordinates and scale.
-         * @name ScaleBarView#classes
-         * @type {Object}
-         * @property {string} longitude The element that will contain the longitude
-         * measurement
-         * @property {string} latitude The element that will contain the latitude
-         * measurement
-         * @property {string} elevation The element that will contain the elevation
-         * measurement
-         * @property {string} bar The element that will be used as a scale bar
-         * @property {string} distance The element that will contain the distance
-         * measurement
-         */
-        classes: {
-          longitude: 'scale-bar__coord--longitude',
-          latitude: 'scale-bar__coord--latitude',
-          elevation: 'scale-bar__coord--elevation',
-          longitudeLabel: 'scale-bar__label--longitude',
-          latitudeLabel: 'scale-bar__label--latitude',
-          elevationLabel: 'scale-bar__label--elevation',
-          bar: 'scale-bar__bar',
-          distance: 'scale-bar__distance'
-        },
-
-        /**
-         * Allowed values for the displayed distance measurement in the scale bar. The
-         * length (in pixels) of the scale bar will be adjusted so that it is proportional
-         * to one of the listed numbers in meters.
-         * @type {number[]}
-         */
-        distances: [
-          0.1,
-          0.5,
-          1,
-          2,
-          3,
-          5,
-          10,
-          20,
-          30,
-          50,
-          100,
-          200,
-          300,
-          500,
-          1000,
-          2000,
-          3000,
-          5000,
-          10000,
-          20000,
-          30000,
-          50000,
-          100000,
-          200000,
-          300000,
-          500000,
-          1000000,
-          2000000,
-          3000000,
-          5000000,
-          10000000,
-          20000000,
-          30000000,
-          50000000
-        ],
-
-        /**
-         * The maximum width of the scale bar element, in pixels
-         * @type {number}
-         */
-        maxBarWidth: 100,
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * @type {Object}
-        */
-        events: {
-          // 'event selector': 'function',
-        },
-
-        /**
-        * Executed when a new ScaleBarView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/maps/scale-bar.html",
+], function ($, _, Backbone, Template) {
+  /**
+   * @class ScaleBarView
+   * @classdesc The scale bar is a legend for a map that shows the current longitude,
+   * latitude, and elevation, as well as a scale bar to indicate the relative size of
+   * geo-spatial features.
+   * @classcategory Views/Maps
+   * @name ScaleBarView
+   * @extends Backbone.View
+   * @screenshot views/maps/ScaleBarView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var ScaleBarView = Backbone.View.extend(
+    /** @lends ScaleBarView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "ScaleBarView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "scale-bar",
+
+      /**
+       * The model that holds the current scale of the map in pixels:meters
+       * @type {GeoScale}
+       * @since 2.27.0
+       */
+      scaleModel: null,
+
+      /**
+       * The model that holds the current position of the mouse on the map
+       * @type {GeoPoint}
+       * @since 2.27.0
+       */
+      pointModel: null,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * Classes that will be used to select elements from the template that will be
+       * updated with new coordinates and scale.
+       * @name ScaleBarView#classes
+       * @type {Object}
+       * @property {string} longitude The element that will contain the longitude
+       * measurement
+       * @property {string} latitude The element that will contain the latitude
+       * measurement
+       * @property {string} elevation The element that will contain the elevation
+       * measurement
+       * @property {string} bar The element that will be used as a scale bar
+       * @property {string} distance The element that will contain the distance
+       * measurement
+       */
+      classes: {
+        longitude: "scale-bar__coord--longitude",
+        latitude: "scale-bar__coord--latitude",
+        elevation: "scale-bar__coord--elevation",
+        longitudeLabel: "scale-bar__label--longitude",
+        latitudeLabel: "scale-bar__label--latitude",
+        elevationLabel: "scale-bar__label--elevation",
+        bar: "scale-bar__bar",
+        distance: "scale-bar__distance",
+      },
+
+      /**
+       * Allowed values for the displayed distance measurement in the scale bar. The
+       * length (in pixels) of the scale bar will be adjusted so that it is proportional
+       * to one of the listed numbers in meters.
+       * @type {number[]}
+       */
+      distances: [
+        0.1, 0.5, 1, 2, 3, 5, 10, 20, 30, 50, 100, 200, 300, 500, 1000, 2000,
+        3000, 5000, 10000, 20000, 30000, 50000, 100000, 200000, 300000, 500000,
+        1000000, 2000000, 3000000, 5000000, 10000000, 20000000, 30000000,
+        50000000,
+      ],
+
+      /**
+       * The maximum width of the scale bar element, in pixels
+       * @type {number}
+       */
+      maxBarWidth: 100,
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        // 'event selector': 'function',
+      },
+
+      /**
+       * Executed when a new ScaleBarView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
-          } catch (e) {
-            console.log('A ScaleBarView failed to initialize.', e);
           }
-
-        },
-
-        /**
-        * Renders this view
-        * @return {ScaleBarView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-
-            // Insert the template into the view
-            this.$el.html(this.template());
-
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-
-            // Select the elements that will be updatable
-            this.subElements = {};
-            for (const [element, className] of Object.entries(view.classes)) {
-              view.subElements[element] = document.querySelector('.' + className)
-            }
-
-            // Start with empty values
-            this.updateCoordinates()
-            this.updateScale()
-
-            // Listen for changes to the models
-            this.listenToScaleModel()
-            this.listenToPointModel()
-
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a ScaleBarView' +
-              '. Error details: ' + error
-            );
+        } catch (e) {
+          console.log("A ScaleBarView failed to initialize.", e);
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {ScaleBarView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // Insert the template into the view
+          this.$el.html(this.template());
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+
+          // Select the elements that will be updatable
+          this.subElements = {};
+          for (const [element, className] of Object.entries(view.classes)) {
+            view.subElements[element] = document.querySelector("." + className);
           }
-        },
 
-        /**
-         * Update the scale bar when the pixel:meters ratio changes
-         * @since 2.27.0
-         */
-        listenToScaleModel: function () {
-          const view = this;
-          this.listenTo(this.scaleModel, 'change', function () {
-            view.updateScale(
-              view.scaleModel.get('pixels'),
-              view.scaleModel.get('meters')
-            );
-          });
-        },
-
-        /**
-         * Stop listening to the scale model
-         * @since 2.27.0
-         */
-        stopListeningToScaleModel: function () {
-          this.stopListening(this.scaleModel, 'change');
-        },
-
-        /**
-         * Update the scale bar view when the lat and long change
-         * @since 2.27.0
-         */
-        listenToPointModel: function () {
-          const view = this;
-          this.listenTo(this.pointModel, 'change:latitude change:longitude', function () {
+          // Start with empty values
+          this.updateCoordinates();
+          this.updateScale();
+
+          // Listen for changes to the models
+          this.listenToScaleModel();
+          this.listenToPointModel();
+
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a ScaleBarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Update the scale bar when the pixel:meters ratio changes
+       * @since 2.27.0
+       */
+      listenToScaleModel: function () {
+        const view = this;
+        this.listenTo(this.scaleModel, "change", function () {
+          view.updateScale(
+            view.scaleModel.get("pixels"),
+            view.scaleModel.get("meters"),
+          );
+        });
+      },
+
+      /**
+       * Stop listening to the scale model
+       * @since 2.27.0
+       */
+      stopListeningToScaleModel: function () {
+        this.stopListening(this.scaleModel, "change");
+      },
+
+      /**
+       * Update the scale bar view when the lat and long change
+       * @since 2.27.0
+       */
+      listenToPointModel: function () {
+        const view = this;
+        this.listenTo(
+          this.pointModel,
+          "change:latitude change:longitude",
+          function () {
             view.updateCoordinates(
-              view.pointModel.get('latitude'),
-              view.pointModel.get('longitude')
-            );
-          });
-        },
-
-        /**
-         * Stop listening to the point model
-         */
-        stopListeningToPointModel: function () {
-          this.stopListening(this.pointModel, 'change:latitude change:longitude');
-        },
-
-        /**
-         * Updates the displayed coordinates on the scale bar view. Numbers are rounded so
-         * that long and lat have 5 digits after the decimal point.
-         * @param {number} latitude The north-south position of the point to show
-         * coordinates for
-         * @param {number} longitude The east-west position of the point to show
-         * coordinates for
-         * @param {number} elevation The distance from sea-level of the point to show
-         * coordinates for
-         */
-        updateCoordinates: function (latitude, longitude, elevation) {
-          try {
-
-            if ((latitude || latitude === 0) && (longitude || longitude === 0)) {
-              // Update the displayed coordinates
-              this.subElements.latitude.textContent = Number.parseFloat(latitude).toFixed(5);
-              this.subElements.longitude.textContent = Number.parseFloat(longitude).toFixed(5);
-              this.subElements.latitudeLabel.style.display = null;
-              this.subElements.longitudeLabel.style.display = null;
-            } else {
-              // Update the displayed coordinates
-              this.subElements.latitude.textContent = '';
-              this.subElements.longitude.textContent = '';
-              this.subElements.latitudeLabel.style.display = 'none';
-              this.subElements.longitudeLabel.style.display = 'none';
-            }
-
-            if ((elevation || elevation === 0)) {
-              // TODO: round/prettify elevation number
-              this.subElements.elevation.textContent = elevation + 'm';
-              this.subElements.elevationLabel.style.display = 'none';
-            } else {
-              this.subElements.elevation.textContent = '';
-              this.subElements.elevationLabel.style.display = 'none';
-            }
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error updating the coordinates in a ScaleBarView' +
-              '. Error details: ' + error
+              view.pointModel.get("latitude"),
+              view.pointModel.get("longitude"),
             );
+          },
+        );
+      },
+
+      /**
+       * Stop listening to the point model
+       */
+      stopListeningToPointModel: function () {
+        this.stopListening(this.pointModel, "change:latitude change:longitude");
+      },
+
+      /**
+       * Updates the displayed coordinates on the scale bar view. Numbers are rounded so
+       * that long and lat have 5 digits after the decimal point.
+       * @param {number} latitude The north-south position of the point to show
+       * coordinates for
+       * @param {number} longitude The east-west position of the point to show
+       * coordinates for
+       * @param {number} elevation The distance from sea-level of the point to show
+       * coordinates for
+       */
+      updateCoordinates: function (latitude, longitude, elevation) {
+        try {
+          if ((latitude || latitude === 0) && (longitude || longitude === 0)) {
+            // Update the displayed coordinates
+            this.subElements.latitude.textContent =
+              Number.parseFloat(latitude).toFixed(5);
+            this.subElements.longitude.textContent =
+              Number.parseFloat(longitude).toFixed(5);
+            this.subElements.latitudeLabel.style.display = null;
+            this.subElements.longitudeLabel.style.display = null;
+          } else {
+            // Update the displayed coordinates
+            this.subElements.latitude.textContent = "";
+            this.subElements.longitude.textContent = "";
+            this.subElements.latitudeLabel.style.display = "none";
+            this.subElements.longitudeLabel.style.display = "none";
           }
-        },
-
-        /**
-         * Change the width of the scale bar and the displayed measurement value based on
-         * a new pixel:meters ratio. This function ensures that the resulting values are
-         * 'pretty' - the pixel and meter measurements passed to this function do not need
-         * to be within any range or rounded, though both values must be > 0.
-         * @param {number} pixels A length in pixels
-         * @param {number} meters A distance, in meters, that is equivalent to the given
-         * distance in pixels
-         */
-        updateScale: function (pixels, meters) {
-          try {
-
-            // Hide the scale bar if a measurement is not available
-            let label = null
-            let barWidth = 0
-
-            if (pixels && meters && pixels > 0 && meters > 0) {
-              const prettyValues = this.prettifyScaleValues(pixels, meters)
-              label = prettyValues.label
-              barWidth = prettyValues.pixels
-            }
-
-            if (barWidth === undefined || barWidth === null || !label) {
-              barWidth = 0
-            }
-
-            this.subElements.distance.textContent = label;
-            this.subElements.bar.style.width = barWidth + 'px';
 
+          if (elevation || elevation === 0) {
+            // TODO: round/prettify elevation number
+            this.subElements.elevation.textContent = elevation + "m";
+            this.subElements.elevationLabel.style.display = "none";
+          } else {
+            this.subElements.elevation.textContent = "";
+            this.subElements.elevationLabel.style.display = "none";
           }
-          catch (error) {
-            console.log(
-              'Failed to update the ScaleBarView. Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "There was an error updating the coordinates in a ScaleBarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Change the width of the scale bar and the displayed measurement value based on
+       * a new pixel:meters ratio. This function ensures that the resulting values are
+       * 'pretty' - the pixel and meter measurements passed to this function do not need
+       * to be within any range or rounded, though both values must be > 0.
+       * @param {number} pixels A length in pixels
+       * @param {number} meters A distance, in meters, that is equivalent to the given
+       * distance in pixels
+       */
+      updateScale: function (pixels, meters) {
+        try {
+          // Hide the scale bar if a measurement is not available
+          let label = null;
+          let barWidth = 0;
+
+          if (pixels && meters && pixels > 0 && meters > 0) {
+            const prettyValues = this.prettifyScaleValues(pixels, meters);
+            label = prettyValues.label;
+            barWidth = prettyValues.pixels;
           }
-        },
-
-        /**
-         * Takes a pixel:meters ratio and returns values ready to use in the scale bar.
-         * @param {number} pixels A length in pixels. Must be > 0.
-         * @param {number} meters A distance, in meters, that is equivalent to the given
-         * distance in pixels. Must be > 0.
-         * @returns {Object} Returns the prettified values.  Returns null for both values
-         * if a matching distance was not found (see {@link ScaleBarView#distances})
-         * @property {number|null} pixels The updated pixel value that is less than the
-         * maxBarWidth and equivalent to the distance given by the label.
-         * @property {string|null} label A string that gives a rounded distance
-         * measurement along with a unit, either meters or kilometers (when > 1000m).
-         */
-        prettifyScaleValues: function (pixels, meters) {
-          try {
-
-            const view = this
-            let prettyValues = {
-              pixels: null,
-              label: null
-            }
-
-            if (pixels && meters && pixels > 0 && meters > 0) {
-
-              const onePixelInMeters = meters / pixels
 
-              // Find the first distance that makes the scale bar less than the maxBarWidth
-              let distance;
-              for (
-                let i = view.distances.length - 1;
-                !(distance !== undefined && distance !== null) && i >= 0;
-                --i
-              ) {
-                if (view.distances[i] / onePixelInMeters < view.maxBarWidth) {
-                  distance = view.distances[i];
-                }
-              }
+          if (barWidth === undefined || barWidth === null || !label) {
+            barWidth = 0;
+          }
 
-              if ((distance !== undefined && distance !== null)) {
-
-                let label;
-                if (distance >= 1000) {
-                  label = (distance / 1000).toString() + ' km';
-                } else if (distance > 1) {
-                  label = distance.toString() + ' m';
-                } else {
-                  label = (distance * 100).toString() + ' cm';
-                }
-
-                prettyValues = {
-                  pixels: (distance / onePixelInMeters),
-                  label: label
-                }
+          this.subElements.distance.textContent = label;
+          this.subElements.bar.style.width = barWidth + "px";
+        } catch (error) {
+          console.log(
+            "Failed to update the ScaleBarView. Error details: " + error,
+          );
+        }
+      },
+
+      /**
+       * Takes a pixel:meters ratio and returns values ready to use in the scale bar.
+       * @param {number} pixels A length in pixels. Must be > 0.
+       * @param {number} meters A distance, in meters, that is equivalent to the given
+       * distance in pixels. Must be > 0.
+       * @returns {Object} Returns the prettified values.  Returns null for both values
+       * if a matching distance was not found (see {@link ScaleBarView#distances})
+       * @property {number|null} pixels The updated pixel value that is less than the
+       * maxBarWidth and equivalent to the distance given by the label.
+       * @property {string|null} label A string that gives a rounded distance
+       * measurement along with a unit, either meters or kilometers (when > 1000m).
+       */
+      prettifyScaleValues: function (pixels, meters) {
+        try {
+          const view = this;
+          let prettyValues = {
+            pixels: null,
+            label: null,
+          };
+
+          if (pixels && meters && pixels > 0 && meters > 0) {
+            const onePixelInMeters = meters / pixels;
+
+            // Find the first distance that makes the scale bar less than the maxBarWidth
+            let distance;
+            for (
+              let i = view.distances.length - 1;
+              !(distance !== undefined && distance !== null) && i >= 0;
+              --i
+            ) {
+              if (view.distances[i] / onePixelInMeters < view.maxBarWidth) {
+                distance = view.distances[i];
               }
             }
 
-            return prettyValues
+            if (distance !== undefined && distance !== null) {
+              let label;
+              if (distance >= 1000) {
+                label = (distance / 1000).toString() + " km";
+              } else if (distance > 1) {
+                label = distance.toString() + " m";
+              } else {
+                label = (distance * 100).toString() + " cm";
+              }
 
-          }
-          catch (error) {
-            console.log(
-              'There was an error prettifying scale values in a ScaleBarView' +
-              '. Error details: ' + error
-            );
-            return {
-              pixels: null,
-              label: null
+              prettyValues = {
+                pixels: distance / onePixelInMeters,
+                label: label,
+              };
             }
           }
-        },
-
-        /**
-         * Function to execute when this view is removed from the DOM
-         * @since 2.27.0
-         */
-        onClose: function () {
-          this.stopListeningToScaleModel()
-          this.stopListeningToPointModel()
-        }
-
-      }
-    );
 
-    return ScaleBarView;
-
-  }
-);
+          return prettyValues;
+        } catch (error) {
+          console.log(
+            "There was an error prettifying scale values in a ScaleBarView" +
+              ". Error details: " +
+              error,
+          );
+          return {
+            pixels: null,
+            label: null,
+          };
+        }
+      },
+
+      /**
+       * Function to execute when this view is removed from the DOM
+       * @since 2.27.0
+       */
+      onClose: function () {
+        this.stopListeningToScaleModel();
+        this.stopListeningToPointModel();
+      },
+    },
+  );
+
+  return ScaleBarView;
+});
 
diff --git a/docs/docs/src_js_views_maps_SearchInputView.js.html b/docs/docs/src_js_views_maps_SearchInputView.js.html index 870c4e6d3..f6885cd5d 100644 --- a/docs/docs/src_js_views_maps_SearchInputView.js.html +++ b/docs/docs/src_js_views_maps_SearchInputView.js.html @@ -46,10 +46,7 @@

Source: src/js/views/maps/SearchInputView.js

"use strict";
 
-define([
-  "backbone",
-  "text!templates/maps/search-input.html",
-], (
+define(["backbone", "text!templates/maps/search-input.html"], (
   Backbone,
   Template,
 ) => {
@@ -76,7 +73,7 @@ 

Source: src/js/views/maps/SearchInputView.js

* @constructs SearchInputView */ const SearchInputView = Backbone.View.extend( - /** @lends SearchInputView.prototype */{ + /** @lends SearchInputView.prototype */ { /** * The type of View this is * @type {string} @@ -84,12 +81,12 @@

Source: src/js/views/maps/SearchInputView.js

type: "SearchInputView", /** - * The HTML classes to use for this view's element - * @type {string} - */ + * The HTML classes to use for this view's element + * @type {string} + */ className: BASE_CLASS, - /** + /** * Values meant to be used by the rendered HTML template. */ templateVars: { @@ -99,17 +96,17 @@

Source: src/js/views/maps/SearchInputView.js

}, /** - * The events this view will listen to and the associated function to call. - * @type {Object} - */ + * The events this view will listen to and the associated function to call. + * @type {Object} + */ events() { return { [`click .${CLASS_NAMES.cancelButton}`]: "onCancel", - [`blur .${CLASS_NAMES.input}`]: 'onBlur', - [`change .${CLASS_NAMES.input}`]: 'onKeyup', - [`focus .${CLASS_NAMES.input}`]: 'onFocus', - [`keydown .${CLASS_NAMES.input}`]: 'onKeydown', - [`keyup .${CLASS_NAMES.input}`]: 'onKeyup', + [`blur .${CLASS_NAMES.input}`]: "onBlur", + [`change .${CLASS_NAMES.input}`]: "onKeyup", + [`focus .${CLASS_NAMES.input}`]: "onFocus", + [`keydown .${CLASS_NAMES.input}`]: "onKeydown", + [`keyup .${CLASS_NAMES.input}`]: "onKeyup", [`click .${CLASS_NAMES.searchButton}`]: "onSearch", }; }, @@ -131,8 +128,10 @@

Source: src/js/views/maps/SearchInputView.js

* @property {String} placeholder The placeholder text for the input box. */ initialize(options) { - if (typeof (options.search) !== "function") { - throw new Error("Initializing SearchInputView without a search function."); + if (typeof options.search !== "function") { + throw new Error( + "Initializing SearchInputView without a search function.", + ); } this.search = options.search; this.keyupCallback = options.keyupCallback || noop; @@ -153,7 +152,7 @@

Source: src/js/views/maps/SearchInputView.js

}, /** - * Event handler for Backbone.View configuration that is called whenever + * Event handler for Backbone.View configuration that is called whenever * the user types a key. */ onKeyup(event) { @@ -190,7 +189,7 @@

Source: src/js/views/maps/SearchInputView.js

}, /** - * Event handler for Backbone.View configuration that is called whenever + * Event handler for Backbone.View configuration that is called whenever * the user types a key. */ onKeydown(event) { @@ -202,7 +201,7 @@

Source: src/js/views/maps/SearchInputView.js

}, /** - * Event handler for Backbone.View configuration that is called whenever + * Event handler for Backbone.View configuration that is called whenever * the user focuses the input. */ onFocus(event) { @@ -210,7 +209,7 @@

Source: src/js/views/maps/SearchInputView.js

}, /** - * Event handler for Backbone.View configuration that is called whenever + * Event handler for Backbone.View configuration that is called whenever * the user blurs the input. */ onBlur(event) { @@ -218,7 +217,7 @@

Source: src/js/views/maps/SearchInputView.js

}, /** - * Event handler for Backbone.View configuration that is called whenever + * Event handler for Backbone.View configuration that is called whenever * the user clicks the search button or hits the Enter key. */ onSearch() { @@ -229,7 +228,7 @@

Source: src/js/views/maps/SearchInputView.js

const matched = this.search(inputValue); if (matched) { this.clearError(); - } else if (typeof (this.noMatchCallback) === "function") { + } else if (typeof this.noMatchCallback === "function") { this.noMatchCallback(); } }, @@ -256,7 +255,7 @@

Source: src/js/views/maps/SearchInputView.js

this.getInputField().removeClass(CLASS_NAMES.errorInput); const errorTextEl = this.getError(); errorTextEl.hide(); - errorTextEl.html(''); + errorTextEl.html(""); }, /** @@ -271,7 +270,7 @@

Source: src/js/views/maps/SearchInputView.js

}, /** - * Focus the input field in this View. + * Focus the input field in this View. */ focus() { this.getInput().trigger("focus"); @@ -335,7 +334,7 @@

Source: src/js/views/maps/SearchInputView.js

* input field is not found. */ getInputValue() { - return this.getInput().val() || ''; + return this.getInput().val() || ""; }, /** @@ -344,10 +343,11 @@

Source: src/js/views/maps/SearchInputView.js

setInputValue(value) { this.getInput().val(value); }, - }); + }, + ); // A function that does nothing. Can be safely called as a default callback. - const noop = () => { }; + const noop = () => {}; return SearchInputView; }); diff --git a/docs/docs/src_js_views_maps_ToolbarView.js.html b/docs/docs/src_js_views_maps_ToolbarView.js.html index 403cff8b9..523989b8b 100644 --- a/docs/docs/src_js_views_maps_ToolbarView.js.html +++ b/docs/docs/src_js_views_maps_ToolbarView.js.html @@ -44,607 +44,599 @@

Source: src/js/views/maps/ToolbarView.js

-
'use strict';
-
-define(
-  [
-    'jquery',
-    'underscore',
-    'backbone',
-    'text!templates/maps/toolbar.html',
-    'models/maps/Map',
-    'common/IconUtilities',
-    // Sub-views - TODO: import these as needed
-    'views/maps/LayersPanelView',
-    'views/maps/DrawToolView',
-    'views/maps/HelpPanelView',
-    'views/maps/viewfinder/ViewfinderView',
-  ],
-  function (
-    $,
-    _,
-    Backbone,
-    Template,
-    Map,
-    IconUtilities,
-    // Sub-views
-    LayersPanelView,
-    DrawTool,
-    HelpPanel,
-    ViewfinderView,
-  ) {
-
-    /**
-    * @class ToolbarView
-    * @classdesc The map toolbar view is a side bar that contains information about a map,
-    * including the available layers, plus UI for changing the settings of a map.
-    * @classcategory Views/Maps
-    * @name ToolbarView
-    * @extends Backbone.View
-    * @screenshot views/maps/ToolbarView.png
-    * @since 2.18.0
-    * @constructs
-    */
-    var ToolbarView = Backbone.View.extend(
-      /** @lends ToolbarView.prototype */{
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: 'ToolbarView',
-
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: 'toolbar',
-
-        /**
-        * The model that this view uses
-        * @type {Map}
-        */
-        model: null,
-
-        /**
-         * The primary HTML template for this view. The template must have two element,
-         * one with the contentContainer class, and one with the linksContainer class.
-         * See {@link ToolbarView#classes}.
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * The classes of the sub-elements that combined to create a toolbar view.
-         *
-         * @name ToolbarView#classes
-         * @type {Object}
-         * @property {string} open The class to add to the view when the toolbar is open
-         * (and the content is visible)
-         * @property {string} contentContainer The element that contains all containers
-         * for the toolbar section content. This element must be part of this view's
-         * template.
-         * @property {string} linksContainer The container for all of the section links
-         * (i.e. tabs)
-         * @property {string} link A section link
-         * @property {string} linkTitle The section link title
-         * @property {string} linkIcon The section link icon
-         * @property {string} linkActive The class to add to a link when its content is
-         * active
-         * @property {string} content A section's content. This element will be the
-         * container for the view associated with this section.
-         * @property {string} contentActive A class added to a content container when it
-         * is the active section
-         */
-        classes: {
-          open: 'toolbar--open',
-          contentContainer: 'toolbar__all-content',
-          linksContainer: 'toolbar__links',
-          link: 'toolbar__link',
-          linkTitle: 'toolbar__link-title',
-          linkIcon: 'toolbar__link-icon',
-          linkActive: 'toolbar__link--active',
-          content: 'toolbar__content',
-          contentActive: 'toolbar__content--active'
+            
"use strict";
+
+define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/maps/toolbar.html",
+  "models/maps/Map",
+  "common/IconUtilities",
+  // Sub-views - TODO: import these as needed
+  "views/maps/LayersPanelView",
+  "views/maps/DrawToolView",
+  "views/maps/HelpPanelView",
+  "views/maps/viewfinder/ViewfinderView",
+], function (
+  $,
+  _,
+  Backbone,
+  Template,
+  Map,
+  IconUtilities,
+  // Sub-views
+  LayersPanelView,
+  DrawTool,
+  HelpPanel,
+  ViewfinderView,
+) {
+  /**
+   * @class ToolbarView
+   * @classdesc The map toolbar view is a side bar that contains information about a map,
+   * including the available layers, plus UI for changing the settings of a map.
+   * @classcategory Views/Maps
+   * @name ToolbarView
+   * @extends Backbone.View
+   * @screenshot views/maps/ToolbarView.png
+   * @since 2.18.0
+   * @constructs
+   */
+  var ToolbarView = Backbone.View.extend(
+    /** @lends ToolbarView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "ToolbarView",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "toolbar",
+
+      /**
+       * The model that this view uses
+       * @type {Map}
+       */
+      model: null,
+
+      /**
+       * The primary HTML template for this view. The template must have two element,
+       * one with the contentContainer class, and one with the linksContainer class.
+       * See {@link ToolbarView#classes}.
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * The classes of the sub-elements that combined to create a toolbar view.
+       *
+       * @name ToolbarView#classes
+       * @type {Object}
+       * @property {string} open The class to add to the view when the toolbar is open
+       * (and the content is visible)
+       * @property {string} contentContainer The element that contains all containers
+       * for the toolbar section content. This element must be part of this view's
+       * template.
+       * @property {string} linksContainer The container for all of the section links
+       * (i.e. tabs)
+       * @property {string} link A section link
+       * @property {string} linkTitle The section link title
+       * @property {string} linkIcon The section link icon
+       * @property {string} linkActive The class to add to a link when its content is
+       * active
+       * @property {string} content A section's content. This element will be the
+       * container for the view associated with this section.
+       * @property {string} contentActive A class added to a content container when it
+       * is the active section
+       */
+      classes: {
+        open: "toolbar--open",
+        contentContainer: "toolbar__all-content",
+        linksContainer: "toolbar__links",
+        link: "toolbar__link",
+        linkTitle: "toolbar__link-title",
+        linkIcon: "toolbar__link-icon",
+        linkActive: "toolbar__link--active",
+        content: "toolbar__content",
+        contentActive: "toolbar__content--active",
+      },
+
+      /**
+       * A string that represents an icon. Can be either the name of the Font Awesome
+       * 3.2 icon OR an SVG string for an icon with all the following properties: 1)
+       * Uses viewBox attribute and not width/height; 2) Sets fill or stroke to
+       * "currentColor" in the svg element, no styles included elsewhere, 3) Has the
+       * required xmlns attribute
+       *
+       * @typedef {string} MapIconString
+       *
+       * @see {@link https://fontawesome.com/v3.2/icons/}
+       *
+       * @example
+       * '<svg viewBox="0 0 400 110" fill="currentColor"><path d="M0 0h300v100H0z"/></svg>'
+       * @example
+       * 'map-marker'
+       */
+
+      /**
+       * Options/settings that are used to create a toolbar section and its associated
+       * link/tab.
+       *
+       * @typedef {Object} SectionOption
+       * @property {string} label The name of this section to show to the user.
+       * @property {MapIconString} icon The icon to show in the link (tab) for this
+       * section
+       * @property {Backbone.View} [view] The view that renders the content of the
+       * toolbar section.
+       * @property {object} [viewOptions] Any additional options to pass to the content
+       * view. By default, the label, icon, and Map model will be passed to the view as
+       * 'label', 'icon', and 'model', respectively. To pass a specific attribute from
+       * the Map model, use a string with the syntax 'model.desiredAttribute'. For
+       * example, 'model.layers' will be converted to view.model.get('layers')
+       * @property {function} [action] A function to call when the link/tab is clicked.
+       * This can be provided instead of a view and viewOptions, in which case no
+       * toolbar section will be created. The function will be passed the view and the
+       * Map model as arguments.
+       * @property {function} [isVisible] A function that determines whether this
+       * section should be visible in the toolbar.
+       */
+
+      /**
+       * The sections displayed in the toolbar will be created based on the options set
+       * in this array.
+       *
+       * @type {SectionOption[]}
+       */
+      sectionOptions: [
+        {
+          label: "Viewfinder",
+          icon: "globe",
+          view: ViewfinderView,
+          action(view, model) {
+            const sectionEl = this;
+            view.defaultActivationAction(sectionEl);
+            sectionEl.sectionView.focusInput();
+          },
+          isVisible(model) {
+            return MetacatUI.mapKey && model.get("showViewfinder");
+          },
         },
-
-        /**
-         * A string that represents an icon. Can be either the name of the Font Awesome
-         * 3.2 icon OR an SVG string for an icon with all the following properties: 1)
-         * Uses viewBox attribute and not width/height; 2) Sets fill or stroke to
-         * "currentColor" in the svg element, no styles included elsewhere, 3) Has the
-         * required xmlns attribute
-         *
-         * @typedef {string} MapIconString
-         *
-         * @see {@link https://fontawesome.com/v3.2/icons/}
-         *
-         * @example
-         * '<svg viewBox="0 0 400 110" fill="currentColor"><path d="M0 0h300v100H0z"/></svg>'
-         * @example
-         * 'map-marker'
-         */
-
-        /**
-         * Options/settings that are used to create a toolbar section and its associated
-         * link/tab.
-         *
-         * @typedef {Object} SectionOption
-         * @property {string} label The name of this section to show to the user.
-         * @property {MapIconString} icon The icon to show in the link (tab) for this
-         * section
-         * @property {Backbone.View} [view] The view that renders the content of the
-         * toolbar section.
-         * @property {object} [viewOptions] Any additional options to pass to the content
-         * view. By default, the label, icon, and Map model will be passed to the view as
-         * 'label', 'icon', and 'model', respectively. To pass a specific attribute from
-         * the Map model, use a string with the syntax 'model.desiredAttribute'. For
-         * example, 'model.layers' will be converted to view.model.get('layers')
-         * @property {function} [action] A function to call when the link/tab is clicked.
-         * This can be provided instead of a view and viewOptions, in which case no
-         * toolbar section will be created. The function will be passed the view and the
-         * Map model as arguments.
-         * @property {function} [isVisible] A function that determines whether this 
-         * section should be visible in the toolbar.
-         */
-
-        /**
-         * The sections displayed in the toolbar will be created based on the options set
-         * in this array.
-         * 
-         * @type {SectionOption[]}
-         */
-        sectionOptions: [
-          {
-            label: 'Viewfinder',
-            icon: 'globe',
-            view: ViewfinderView,
-            action(view, model) {
-              const sectionEl = this;
-              view.defaultActivationAction(sectionEl);
-              sectionEl.sectionView.focusInput();
-            },
-            isVisible(model) {
-              return MetacatUI.mapKey && model.get("showViewfinder");
-            },
+        {
+          label: "Layers",
+          icon: '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="m3.2 7.3 8.6 4.6a.5.5 0 0 0 .4 0l8.6-4.6a.4.4 0 0 0 0-.8L12.1 3a.5.5 0 0 0-.4 0L3.3 6.5a.4.4 0 0 0 0 .8Z"/><path d="M20.7 10.7 19 9.9l-6.7 3.6a.5.5 0 0 1-.4 0L5 9.9l-1.8.8a.5.5 0 0 0 0 .8l8.5 5a.5.5 0 0 0 .5 0l8.5-5a.5.5 0 0 0 0-.8Z"/><path d="m20.7 15.1-1.5-.7-7 3.8a.5.5 0 0 1-.4 0l-7-3.8-1.5.7a.5.5 0 0 0 0 .9l8.5 5a.5.5 0 0 0 .5 0l8.5-5a.5.5 0 0 0 0-.9Z"/></svg>',
+          view: LayersPanelView,
+          isVisible(model) {
+            return model.get("showLayerList");
           },
-          {
-            label: 'Layers',
-            icon: '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="m3.2 7.3 8.6 4.6a.5.5 0 0 0 .4 0l8.6-4.6a.4.4 0 0 0 0-.8L12.1 3a.5.5 0 0 0-.4 0L3.3 6.5a.4.4 0 0 0 0 .8Z"/><path d="M20.7 10.7 19 9.9l-6.7 3.6a.5.5 0 0 1-.4 0L5 9.9l-1.8.8a.5.5 0 0 0 0 .8l8.5 5a.5.5 0 0 0 .5 0l8.5-5a.5.5 0 0 0 0-.8Z"/><path d="m20.7 15.1-1.5-.7-7 3.8a.5.5 0 0 1-.4 0l-7-3.8-1.5.7a.5.5 0 0 0 0 .9l8.5 5a.5.5 0 0 0 .5 0l8.5-5a.5.5 0 0 0 0-.9Z"/></svg>',
-            view: LayersPanelView,
-            isVisible(model) {
-              return model.get('showLayerList');
-            },
+        },
+        {
+          label: "Reset",
+          icon: "rotate-left",
+          action: function (view, model) {
+            model.flyHome();
+            model.resetLayerVisibility();
           },
-          {
-            label: 'Reset',
-            icon: 'rotate-left',
-            action: function (view, model) {
-              model.flyHome();
-              model.resetLayerVisibility();
-            },
-            isVisible(model) {
-              return model.get("showHomeButton");
-            },
+          isVisible(model) {
+            return model.get("showHomeButton");
           },
-          // We can enable to the draw tool once we have a use case for it
-          // {
-          //   label: 'Draw',
-          //   icon: 'pencil',
-          //   view: DrawTool,
-          //   viewOptions: {}
-          // },
-          {
-            label: 'Help',
-            icon: 'question-sign',
-            view: HelpPanel,
-            viewOptions: {
-              showFeedback: 'model.showFeedback',
-              feedbackText: 'model.feedbackText',
-              showNavHelp: 'model.showNavHelp',
-            },
-            isVisible(model) {
-              return model.get("showNavHelp") || model.get("showFeedback");
-            },
-          }
-        ],
-
-        /**
-         * Whether or not the toolbar menu is opened. This will get updated when the user
-         * interacts with the toolbar links.
-         * @type {Boolean}
-         */
-        isOpen: false,
-
-        /**
-        * Executed when a new ToolbarView is created
-        * @param {Object} [options] - A literal object with options to pass to the view
-        */
-        initialize: function (options) {
-
-          try {
-            // Get all the options and apply them to this view
-            if (typeof options == 'object') {
-              for (const [key, value] of Object.entries(options)) {
-                this[key] = value;
-              }
-            }
-            if (!this.model || !(this.model instanceof Map)) {
-              this.model = new Map();
-            }
-
-            if (this.model.get('toolbarOpen') === true) {
-              this.isOpen = true;
-            }
-
-            // Check whether each section should be shown, defaulting to true.
-            this.sections = this.sectionOptions.filter(section => {
-              return typeof section.isVisible === 'function'
-                ? section.isVisible(this.model)
-                : true;
-            });
-          } catch (e) {
-            console.log('Error initializing a ToolbarView', e);
-          }
-
         },
-
-        /**
-        * Renders this view
-        * @return {ToolbarView} Returns the rendered view element
-        */
-        render: function () {
-
-          try {
-
-            // Save a reference to this view
-            var view = this;
-
-            // Insert the template into the view
-            this.$el.html(this.template({}));
-
-            // Ensure the view's main element has the given class name
-            this.el.classList.add(this.className);
-
-            // Select and save a reference to the elements that will contain the
-            // links/tabs and the section content.
-            this.contentContainer = document.querySelector(
-              '.' + view.classes.contentContainer
-            )
-            this.linksContainer = document.querySelector(
-              '.' + view.classes.linksContainer
-            )
-
-            // sectionElements will store the section link, section content element, and
-            // the status of the given section (whether it is active or not)
-            this.sectionElements = [];
-
-            // For each section configured in the view's sections property, create a link
-            // and render the content. Set a listener for when the link is clicked. 
-            this.sections.forEach(function (sectionOption) {
-              // Render the link and content elements
-              var linkEl = view.renderSectionLink(sectionOption)
-              var action = sectionOption.action
-              let contentEl = null;
-              let sectionView;
-              if (sectionOption.view) {
-                const { contentContainer, sectionContent } = view.renderSectionContent(sectionOption)
-                contentEl = contentContainer;
-                sectionView = sectionContent;
-              }
-              // Set the section to false to start
-              var isActive = false
-              // Save a reference to these elements and their status. sectionEl is an
-              // object that has type SectionElement (documented in comments below)
-              var sectionEl = { linkEl, contentEl, isActive, action, sectionView };
-              view.sectionElements.push(sectionEl)
-              // Attach the link and content to the view
-              if (contentEl) {
-                view.contentContainer.appendChild(contentEl);
-              }
-              view.linksContainer.appendChild(linkEl);
-              // Add a listener that shows the section when the link is clicked
-              linkEl.addEventListener('click', function () {
-                view.handleLinkClick(sectionEl)
-              });
-            })
-
-            // Set the toolbar to open, depending on what is initially set in view.isOpen.
-            // Set the first section to active if the toolbar is open.
-            if (this.isOpen) {
-              this.el.classList.add(this.classes.open)
-              view.handleLinkClick(this.sectionElements[0])
-            }
-
-            return this
-
-          }
-          catch (error) {
-            console.log(
-              'There was an error rendering a ToolbarView' +
-              '. Error details: ' + error
-            );
-          }
+        // We can enable to the draw tool once we have a use case for it
+        // {
+        //   label: 'Draw',
+        //   icon: 'pencil',
+        //   view: DrawTool,
+        //   viewOptions: {}
+        // },
+        {
+          label: "Help",
+          icon: "question-sign",
+          view: HelpPanel,
+          viewOptions: {
+            showFeedback: "model.showFeedback",
+            feedbackText: "model.feedbackText",
+            showNavHelp: "model.showNavHelp",
+          },
+          isVisible(model) {
+            return model.get("showNavHelp") || model.get("showFeedback");
+          },
         },
-
-        /**
-         * A reference to all of the elements required to make up a toolbar section: the
-         * section content and the section link (i.e. tab); as well as the status of the
-         * section: active or in active.
-         *
-         * @typedef {Object} SectionElement
-         * @property {HTMLElement} contentEl The element that contains the toolbar
-         * section's content (the content rendered by the associated view)
-         * @property {HTMLElement} linkEl The element that acts as a link to show the
-         * section's content, and open/close the toolbar.
-         * @property {Boolean} isActive True if this is the active section, false
-         * otherwise.
-         * @property {Backbone.View} sectionView The associated Backbone.View instance. 
-         */
-
-        /**
-         * Executed when any one of the tabs/links are clicked. Opens the toolbar if it's
-         * closed, closes it if the active section is clicked, and otherwise activates the
-         * clicked section content.
-         * @param {SectionElement} sectionEl
-         */
-        handleLinkClick: function (sectionEl) {
-          try {
-            var toolbarOpen = this.isOpen;
-            var sectionActive = sectionEl.isActive;
-            if (toolbarOpen && sectionActive) {
-              this.close()
-              return
-            }
-            if (!toolbarOpen && sectionEl.contentEl) {
-              this.open()
-            }
-            if (!sectionActive) {
-              if (sectionEl.contentEl) {
-                this.inactivateAllSections()
-              }
-              this.activateSection(sectionEl)
+      ],
+
+      /**
+       * Whether or not the toolbar menu is opened. This will get updated when the user
+       * interacts with the toolbar links.
+       * @type {Boolean}
+       */
+      isOpen: false,
+
+      /**
+       * Executed when a new ToolbarView is created
+       * @param {Object} [options] - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            for (const [key, value] of Object.entries(options)) {
+              this[key] = value;
             }
           }
-          catch (error) {
-            console.log(
-              'There was an error handling a toolbar link click in a ToolbarView' +
-              '. Error details: ' + error
-            );
+          if (!this.model || !(this.model instanceof Map)) {
+            this.model = new Map();
           }
-        },
 
-        /**
-         * Creates a link/tab for a given toolbar section
-         * @param {SectionOption} sectionOption The label and icon that are set in the
-         * Section Option are used to create the link content
-         * @returns {HTMLElement} Returns the link element
-         */
-        renderSectionLink: function (sectionOption) {
-          try {
-
-            // Create a container, label
-            const link = document.createElement('div')
-            const title = document.createElement('div')
-            // Create the icon
-            const icon = this.createIcon(sectionOption.icon)
-
-            // Add the relevant classes
-            link.classList.add(this.classes.link)
-            title.classList.add(this.classes.linkTitle)
-            // Add the label text
-            title.textContent = sectionOption.label
-
-            link.append(icon, title)
-
-            return link
+          if (this.model.get("toolbarOpen") === true) {
+            this.isOpen = true;
           }
-          catch (error) {
-            console.log(
-              'There was an error rendering a section link in a ToolbarView' +
-              '. Error details: ' + error
-            );
-          }
-        },
-
-        /**
-         * Given the name of a Font Awesome 3.2 icon, or an SVG string, creates an icon
-         * element with the appropriate classes for the tool bar link (tab)
-         * @param {MapIconString} iconString The string to use to create the icon
-         * @returns {HTMLElement} Returns either an <i> element with a Font Awesome icon,
-         * or and SVG with a custom icon
-         */
-        createIcon: function (iconString) {
-          try {
-            // The icon element we will create and return. By default, return an empty span
-            // element.
-            let icon = document.createElement('span');
-
-            // iconString must be string
-            if (typeof iconString === 'string') {
-              // If the icon is an SVG element
-              if (IconUtilities.isSVG(iconString)) {
-                icon = new DOMParser()
-                  .parseFromString(iconString, 'image/svg+xml')
-                  .documentElement;
-                // If the icon is not an SVG, assume it's the name for a Font Awesome icon
-              } else {
-                icon = document.createElement('i')
-                icon.className = 'icon-' + iconString;
-              }
 
+          // Check whether each section should be shown, defaulting to true.
+          this.sections = this.sectionOptions.filter((section) => {
+            return typeof section.isVisible === "function"
+              ? section.isVisible(this.model)
+              : true;
+          });
+        } catch (e) {
+          console.log("Error initializing a ToolbarView", e);
+        }
+      },
+
+      /**
+       * Renders this view
+       * @return {ToolbarView} Returns the rendered view element
+       */
+      render: function () {
+        try {
+          // Save a reference to this view
+          var view = this;
+
+          // Insert the template into the view
+          this.$el.html(this.template({}));
+
+          // Ensure the view's main element has the given class name
+          this.el.classList.add(this.className);
+
+          // Select and save a reference to the elements that will contain the
+          // links/tabs and the section content.
+          this.contentContainer = document.querySelector(
+            "." + view.classes.contentContainer,
+          );
+          this.linksContainer = document.querySelector(
+            "." + view.classes.linksContainer,
+          );
+
+          // sectionElements will store the section link, section content element, and
+          // the status of the given section (whether it is active or not)
+          this.sectionElements = [];
+
+          // For each section configured in the view's sections property, create a link
+          // and render the content. Set a listener for when the link is clicked.
+          this.sections.forEach(function (sectionOption) {
+            // Render the link and content elements
+            var linkEl = view.renderSectionLink(sectionOption);
+            var action = sectionOption.action;
+            let contentEl = null;
+            let sectionView;
+            if (sectionOption.view) {
+              const { contentContainer, sectionContent } =
+                view.renderSectionContent(sectionOption);
+              contentEl = contentContainer;
+              sectionView = sectionContent;
             }
-            icon.classList.add(this.classes.linkIcon);
-            return icon
-          }
-          catch (error) {
-            console.log(
-              'There was an error  in a ToolbarView' +
-              '. Error details: ' + error
-            );
-            return document.createElement('span')
-          }
-        },
-
-        /**
-         * @typedef {Object} SectionContentReturnType
-         * @property {HTMLElement} contentContainer - The content container HTML
-         * element.
-         * @property {Backbone.View} sectionContent - The Backbone.View instance
-         */
-
-        /**
-         * Creates a container for a toolbar section's content, then rendered the
-         * specified view in that container.
-         * @param {SectionOption} sectionOption The view and view options that are set in
-         * the Section Option are used to create the content container
-         * @returns {SectionContentReturnType} The content container with the
-         * rendered view, and the Backbone.View itself.
-         */
-        renderSectionContent: function (sectionOption) {
-          try {
-            const view = this
-            // Create the container for the toolbar section content
-            var contentContainer = document.createElement('div')
-            // Add the class that identifies a toolbar section's content
-            contentContainer.classList.add(this.classes.content)
-            // Render the toolbar section view
-            // Merge the icon and label with the other section options
-            var viewOptions = Object.assign(
-              {
-                label: sectionOption.label,
-                icon: sectionOption.icon,
-                model: this.model
-              },
-              sectionOption.viewOptions
-            )
-            // Convert any values in the form of 'model.someAttribute' to the model
-            // attribute that is specified.
-            for (const [key, value] of Object.entries(viewOptions)) {
-              if (typeof value === 'string' && value.startsWith('model.')) {
-                const attr = value.replace(/^model\./, '')
-                viewOptions[key] = view.model.get(attr)
-              }
+            // Set the section to false to start
+            var isActive = false;
+            // Save a reference to these elements and their status. sectionEl is an
+            // object that has type SectionElement (documented in comments below)
+            var sectionEl = {
+              linkEl,
+              contentEl,
+              isActive,
+              action,
+              sectionView,
+            };
+            view.sectionElements.push(sectionEl);
+            // Attach the link and content to the view
+            if (contentEl) {
+              view.contentContainer.appendChild(contentEl);
             }
-            var sectionContent = new sectionOption.view(viewOptions)
-            contentContainer.appendChild(sectionContent.el)
-            sectionContent.render()
-            return { contentContainer, sectionContent }
-          }
-          catch (error) {
-            console.log('Error rendering ToolbarView section', error);
-          }
-        },
+            view.linksContainer.appendChild(linkEl);
+            // Add a listener that shows the section when the link is clicked
+            linkEl.addEventListener("click", function () {
+              view.handleLinkClick(sectionEl);
+            });
+          });
 
-        /**
-         * Opens the toolbar and displays the content of the active toolbar section
-         */
-        open: function () {
-          try {
-            this.isOpen = true
-            this.el.classList.add(this.classes.open)
-          }
-          catch (error) {
-            console.log(
-              'There was an error opening a ToolbarView' +
-              '. Error details: ' + error
-            );
+          // Set the toolbar to open, depending on what is initially set in view.isOpen.
+          // Set the first section to active if the toolbar is open.
+          if (this.isOpen) {
+            this.el.classList.add(this.classes.open);
+            view.handleLinkClick(this.sectionElements[0]);
           }
-        },
 
-        /**
-         * Closes the toolbar. Also inactivates all sections.
-         */
-        close: function () {
-          try {
-            this.isOpen = false
-            this.el.classList.remove(this.classes.open)
-            // Ensure that no section is active when the toolbar is closed
-            this.inactivateAllSections()
+          return this;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a ToolbarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * A reference to all of the elements required to make up a toolbar section: the
+       * section content and the section link (i.e. tab); as well as the status of the
+       * section: active or in active.
+       *
+       * @typedef {Object} SectionElement
+       * @property {HTMLElement} contentEl The element that contains the toolbar
+       * section's content (the content rendered by the associated view)
+       * @property {HTMLElement} linkEl The element that acts as a link to show the
+       * section's content, and open/close the toolbar.
+       * @property {Boolean} isActive True if this is the active section, false
+       * otherwise.
+       * @property {Backbone.View} sectionView The associated Backbone.View instance.
+       */
+
+      /**
+       * Executed when any one of the tabs/links are clicked. Opens the toolbar if it's
+       * closed, closes it if the active section is clicked, and otherwise activates the
+       * clicked section content.
+       * @param {SectionElement} sectionEl
+       */
+      handleLinkClick: function (sectionEl) {
+        try {
+          var toolbarOpen = this.isOpen;
+          var sectionActive = sectionEl.isActive;
+          if (toolbarOpen && sectionActive) {
+            this.close();
+            return;
           }
-          catch (error) {
-            console.log(
-              'There was an error closing a ToolbarView' +
-              '. Error details: ' + error
-            );
+          if (!toolbarOpen && sectionEl.contentEl) {
+            this.open();
           }
-        },
-
-        /**
-         * Display the content of a given section
-         * @param {SectionElement} sectionEl The section to activate
-         */
-        activateSection: function (sectionEl) {
-          if (!sectionEl) return;
-          try {
-            if (sectionEl.action && typeof sectionEl.action === 'function') {
-              const view = this;
-              const model = this.model;
-              sectionEl.action(view, model)
-            } else {
-              this.defaultActivationAction(sectionEl);
+          if (!sectionActive) {
+            if (sectionEl.contentEl) {
+              this.inactivateAllSections();
             }
+            this.activateSection(sectionEl);
           }
-          catch (error) {
-            console.log('Failed to show a section in a ToolbarView', error);
-          }
-        },
-
-        /**
-         * The default action for a section being activated. 
-         * @param {SectionElement} sectionEl The section to activate
-         */
-        defaultActivationAction(sectionEl) {
-          sectionEl.isActive = true;
-          sectionEl.contentEl.classList.add(this.classes.contentActive)
-          sectionEl.linkEl.classList.add(this.classes.linkActive)
-        },
-
-        /**
-         * Hide the content of a section
-         * @param {SectionElement} sectionEl The section to inactivate
-         */
-        inactivateSection: function (sectionEl) {
-          try {
-            sectionEl.isActive = false;
-            if (sectionEl.contentEl) {
-              sectionEl.contentEl.classList.remove(this.classes.contentActive)
-              sectionEl.linkEl.classList.remove(this.classes.linkActive)
+        } catch (error) {
+          console.log(
+            "There was an error handling a toolbar link click in a ToolbarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Creates a link/tab for a given toolbar section
+       * @param {SectionOption} sectionOption The label and icon that are set in the
+       * Section Option are used to create the link content
+       * @returns {HTMLElement} Returns the link element
+       */
+      renderSectionLink: function (sectionOption) {
+        try {
+          // Create a container, label
+          const link = document.createElement("div");
+          const title = document.createElement("div");
+          // Create the icon
+          const icon = this.createIcon(sectionOption.icon);
+
+          // Add the relevant classes
+          link.classList.add(this.classes.link);
+          title.classList.add(this.classes.linkTitle);
+          // Add the label text
+          title.textContent = sectionOption.label;
+
+          link.append(icon, title);
+
+          return link;
+        } catch (error) {
+          console.log(
+            "There was an error rendering a section link in a ToolbarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Given the name of a Font Awesome 3.2 icon, or an SVG string, creates an icon
+       * element with the appropriate classes for the tool bar link (tab)
+       * @param {MapIconString} iconString The string to use to create the icon
+       * @returns {HTMLElement} Returns either an <i> element with a Font Awesome icon,
+       * or and SVG with a custom icon
+       */
+      createIcon: function (iconString) {
+        try {
+          // The icon element we will create and return. By default, return an empty span
+          // element.
+          let icon = document.createElement("span");
+
+          // iconString must be string
+          if (typeof iconString === "string") {
+            // If the icon is an SVG element
+            if (IconUtilities.isSVG(iconString)) {
+              icon = new DOMParser().parseFromString(
+                iconString,
+                "image/svg+xml",
+              ).documentElement;
+              // If the icon is not an SVG, assume it's the name for a Font Awesome icon
+            } else {
+              icon = document.createElement("i");
+              icon.className = "icon-" + iconString;
             }
           }
-          catch (error) {
-            console.log(
-              'There was an error showing a toolbar section in a ToolbarView' +
-              '. Error details: ' + error
-            );
+          icon.classList.add(this.classes.linkIcon);
+          return icon;
+        } catch (error) {
+          console.log(
+            "There was an error  in a ToolbarView" +
+              ". Error details: " +
+              error,
+          );
+          return document.createElement("span");
+        }
+      },
+
+      /**
+       * @typedef {Object} SectionContentReturnType
+       * @property {HTMLElement} contentContainer - The content container HTML
+       * element.
+       * @property {Backbone.View} sectionContent - The Backbone.View instance
+       */
+
+      /**
+       * Creates a container for a toolbar section's content, then rendered the
+       * specified view in that container.
+       * @param {SectionOption} sectionOption The view and view options that are set in
+       * the Section Option are used to create the content container
+       * @returns {SectionContentReturnType} The content container with the
+       * rendered view, and the Backbone.View itself.
+       */
+      renderSectionContent: function (sectionOption) {
+        try {
+          const view = this;
+          // Create the container for the toolbar section content
+          var contentContainer = document.createElement("div");
+          // Add the class that identifies a toolbar section's content
+          contentContainer.classList.add(this.classes.content);
+          // Render the toolbar section view
+          // Merge the icon and label with the other section options
+          var viewOptions = Object.assign(
+            {
+              label: sectionOption.label,
+              icon: sectionOption.icon,
+              model: this.model,
+            },
+            sectionOption.viewOptions,
+          );
+          // Convert any values in the form of 'model.someAttribute' to the model
+          // attribute that is specified.
+          for (const [key, value] of Object.entries(viewOptions)) {
+            if (typeof value === "string" && value.startsWith("model.")) {
+              const attr = value.replace(/^model\./, "");
+              viewOptions[key] = view.model.get(attr);
+            }
           }
-        },
-
-        /**
-         * Hide all of the sections in a toolbar view
-         */
-        inactivateAllSections: function () {
-          try {
-            var view = this;
-            this.sectionElements.forEach(function (sectionEl) {
-              view.inactivateSection(sectionEl)
-            })
+          var sectionContent = new sectionOption.view(viewOptions);
+          contentContainer.appendChild(sectionContent.el);
+          sectionContent.render();
+          return { contentContainer, sectionContent };
+        } catch (error) {
+          console.log("Error rendering ToolbarView section", error);
+        }
+      },
+
+      /**
+       * Opens the toolbar and displays the content of the active toolbar section
+       */
+      open: function () {
+        try {
+          this.isOpen = true;
+          this.el.classList.add(this.classes.open);
+        } catch (error) {
+          console.log(
+            "There was an error opening a ToolbarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Closes the toolbar. Also inactivates all sections.
+       */
+      close: function () {
+        try {
+          this.isOpen = false;
+          this.el.classList.remove(this.classes.open);
+          // Ensure that no section is active when the toolbar is closed
+          this.inactivateAllSections();
+        } catch (error) {
+          console.log(
+            "There was an error closing a ToolbarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Display the content of a given section
+       * @param {SectionElement} sectionEl The section to activate
+       */
+      activateSection: function (sectionEl) {
+        if (!sectionEl) return;
+        try {
+          if (sectionEl.action && typeof sectionEl.action === "function") {
+            const view = this;
+            const model = this.model;
+            sectionEl.action(view, model);
+          } else {
+            this.defaultActivationAction(sectionEl);
           }
-          catch (error) {
-            console.log(
-              'There was an error hiding toolbar sections in a ToolbarView' +
-              '. Error details: ' + error
-            );
+        } catch (error) {
+          console.log("Failed to show a section in a ToolbarView", error);
+        }
+      },
+
+      /**
+       * The default action for a section being activated.
+       * @param {SectionElement} sectionEl The section to activate
+       */
+      defaultActivationAction(sectionEl) {
+        sectionEl.isActive = true;
+        sectionEl.contentEl.classList.add(this.classes.contentActive);
+        sectionEl.linkEl.classList.add(this.classes.linkActive);
+      },
+
+      /**
+       * Hide the content of a section
+       * @param {SectionElement} sectionEl The section to inactivate
+       */
+      inactivateSection: function (sectionEl) {
+        try {
+          sectionEl.isActive = false;
+          if (sectionEl.contentEl) {
+            sectionEl.contentEl.classList.remove(this.classes.contentActive);
+            sectionEl.linkEl.classList.remove(this.classes.linkActive);
           }
-        },
-
-      }
-    );
-
-    return ToolbarView;
-
-  }
-);
+        } catch (error) {
+          console.log(
+            "There was an error showing a toolbar section in a ToolbarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Hide all of the sections in a toolbar view
+       */
+      inactivateAllSections: function () {
+        try {
+          var view = this;
+          this.sectionElements.forEach(function (sectionEl) {
+            view.inactivateSection(sectionEl);
+          });
+        } catch (error) {
+          console.log(
+            "There was an error hiding toolbar sections in a ToolbarView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+    },
+  );
+
+  return ToolbarView;
+});
 
diff --git a/docs/docs/src_js_views_maps_viewfinder_ExpansionPanelView.js.html b/docs/docs/src_js_views_maps_viewfinder_ExpansionPanelView.js.html index ee9db28dc..05879f0de 100644 --- a/docs/docs/src_js_views_maps_viewfinder_ExpansionPanelView.js.html +++ b/docs/docs/src_js_views_maps_viewfinder_ExpansionPanelView.js.html @@ -44,133 +44,133 @@

Source: src/js/views/maps/viewfinder/ExpansionPanelView.j
-
'use strict';
-
-define(
-  [
-    'underscore',
-    'backbone',
-    'text!templates/maps/viewfinder/expansion-panel.html',
-  ],
-  (_, Backbone, Template) => {
-    // The base classname to use for this View's template elements.
-    const BASE_CLASS = 'expansion-panel';
-    //The HTML classes to use for this view's HTML elements.
-    const CLASS_NAMES = {
-      title: `${BASE_CLASS}__title`,
-      content: `${BASE_CLASS}__content`,
-      toggle: `${BASE_CLASS}__toggle`,
-      icon: `${BASE_CLASS}__icon`,
-      iconToggle: `${BASE_CLASS}__icon-toggle`,
-    };
-
-    /**
-     * @class ExpansionPanelView
-     * @classdesc Allow expand and collapse content in a panel.
-     * @classcategory Views/Maps/Viewfinder
-     * @name ExpansionPanelView
-     * @extends Backbone.View
-     * @screenshot views/maps/viewfinder/ExpansionPanelView_closed.png
-     * @screenshot views/maps/viewfinder/ExpansionPanelView_open.png
-     * @since 2.29.0
-     * @constructs ExpansionPanelView
-     */
-    var ExpansionPanelView = Backbone.View.extend(
-      /** @lends ExpansionPanelView.prototype */{
-        /**
-         * The type of View this is
-         * @type {string}
-         */
-        type: 'ExpansionPanelView',
-
-        /** @inheritdoc */
-        className: BASE_CLASS,
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * @type {Object}
-        */
-        events() {
-          return {
-            [`click .${CLASS_NAMES.toggle}`]: 'toggle',
-          };
-        },
-
-        /** Values meant to be used by the rendered HTML template. */
-        templateVars: {
-          classNames: CLASS_NAMES,
-          title: '',
-          icon: '',
-        },
-
-        /**
-         * @typedef {Object} ExpansionPanelViewOptions
-         * @property {string} title The displayed label for this panel. 
-         * @property {string} icon The icon displayed in the panel's clickable
-         * label. 
-         * @property {Backbone.View} contentViewInstance The Backbone.View that 
-         * will be displayed when the content of the panel is toggled to be
-         * visible.
-         * @property {ExpansionPanelsModel} [panelsModel] Optional model for
-         * coordinating the expanded/collapsed state among many panels. 
-         * @property {boolean} startOpen Whether the panel should be expanded by
-         * default. 
-         */
-        initialize({ title, contentViewInstance, icon, panelsModel, startOpen }) {
-          this.templateVars.title = title;
-          this.templateVars.icon = icon;
-          this.contentViewInstance = contentViewInstance;
-          this.panelsModel = panelsModel;
-          this.startOpen = !!startOpen;
-
-          this.panelsModel?.register(this);
-        },
-
-        /**
-         * Getter function for the content div. 
-         * @return {HTMLDivElement} Returns the content element.
-         */
-        getContent() {
-          return this.$el.find(`.${CLASS_NAMES.content}`);
-        },
-
-        /** Force the panel's content to be hidden. */
-        collapse() {
-          this.$el.removeClass('show-content');
-        },
-
-        /** Force the panel's content to be shown. */
-        open() {
-          this.$el.addClass('show-content');
-          this.panelsModel?.maybeCollapseOthers(this);
-        },
-
-        /** Toggle the visibility of the panel's content. */
-        toggle() {
-          if (this.$el.hasClass('show-content')) {
-            this.collapse();
-          } else {
-            this.open();
-          }
-        },
-
-        /**
-         * Render the view by updating the HTML of the element.
-         * The new HTML is computed from an HTML template that
-         * is passed an object with relevant view state.
-         * */
-        render() {
-          this.el.innerHTML = _.template(Template)(this.templateVars);
-          this.contentViewInstance.render();
-          this.getContent().append(this.contentViewInstance.el);
-          if (this.startOpen) {
-            this.open();
-          }
-        },
-      });
-
-    return ExpansionPanelView;
-  });
+
"use strict";
+
+define([
+  "underscore",
+  "backbone",
+  "text!templates/maps/viewfinder/expansion-panel.html",
+], (_, Backbone, Template) => {
+  // The base classname to use for this View's template elements.
+  const BASE_CLASS = "expansion-panel";
+  //The HTML classes to use for this view's HTML elements.
+  const CLASS_NAMES = {
+    title: `${BASE_CLASS}__title`,
+    content: `${BASE_CLASS}__content`,
+    toggle: `${BASE_CLASS}__toggle`,
+    icon: `${BASE_CLASS}__icon`,
+    iconToggle: `${BASE_CLASS}__icon-toggle`,
+  };
+
+  /**
+   * @class ExpansionPanelView
+   * @classdesc Allow expand and collapse content in a panel.
+   * @classcategory Views/Maps/Viewfinder
+   * @name ExpansionPanelView
+   * @extends Backbone.View
+   * @screenshot views/maps/viewfinder/ExpansionPanelView_closed.png
+   * @screenshot views/maps/viewfinder/ExpansionPanelView_open.png
+   * @since 2.29.0
+   * @constructs ExpansionPanelView
+   */
+  var ExpansionPanelView = Backbone.View.extend(
+    /** @lends ExpansionPanelView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "ExpansionPanelView",
+
+      /** @inheritdoc */
+      className: BASE_CLASS,
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events() {
+        return {
+          [`click .${CLASS_NAMES.toggle}`]: "toggle",
+        };
+      },
+
+      /** Values meant to be used by the rendered HTML template. */
+      templateVars: {
+        classNames: CLASS_NAMES,
+        title: "",
+        icon: "",
+      },
+
+      /**
+       * @typedef {Object} ExpansionPanelViewOptions
+       * @property {string} title The displayed label for this panel.
+       * @property {string} icon The icon displayed in the panel's clickable
+       * label.
+       * @property {Backbone.View} contentViewInstance The Backbone.View that
+       * will be displayed when the content of the panel is toggled to be
+       * visible.
+       * @property {ExpansionPanelsModel} [panelsModel] Optional model for
+       * coordinating the expanded/collapsed state among many panels.
+       * @property {boolean} startOpen Whether the panel should be expanded by
+       * default.
+       */
+      initialize({ title, contentViewInstance, icon, panelsModel, startOpen }) {
+        this.templateVars.title = title;
+        this.templateVars.icon = icon;
+        this.contentViewInstance = contentViewInstance;
+        this.panelsModel = panelsModel;
+        this.startOpen = !!startOpen;
+
+        this.panelsModel?.register(this);
+      },
+
+      /**
+       * Getter function for the content div.
+       * @return {HTMLDivElement} Returns the content element.
+       */
+      getContent() {
+        return this.$el.find(`.${CLASS_NAMES.content}`);
+      },
+
+      /** Force the panel's content to be hidden. */
+      collapse() {
+        this.$el.removeClass("show-content");
+      },
+
+      /** Force the panel's content to be shown. */
+      open() {
+        this.$el.addClass("show-content");
+        this.panelsModel?.maybeCollapseOthers(this);
+      },
+
+      /** Toggle the visibility of the panel's content. */
+      toggle() {
+        if (this.$el.hasClass("show-content")) {
+          this.collapse();
+        } else {
+          this.open();
+        }
+      },
+
+      /**
+       * Render the view by updating the HTML of the element.
+       * The new HTML is computed from an HTML template that
+       * is passed an object with relevant view state.
+       * */
+      render() {
+        this.el.innerHTML = _.template(Template)(this.templateVars);
+        this.contentViewInstance.render();
+        this.getContent().append(this.contentViewInstance.el);
+        if (this.startOpen) {
+          this.open();
+        }
+      },
+    },
+  );
+
+  return ExpansionPanelView;
+});
+
diff --git a/docs/docs/src_js_views_maps_viewfinder_PredictionView.js.html b/docs/docs/src_js_views_maps_viewfinder_PredictionView.js.html index 17cf670f8..25eca3471 100644 --- a/docs/docs/src_js_views_maps_viewfinder_PredictionView.js.html +++ b/docs/docs/src_js_views_maps_viewfinder_PredictionView.js.html @@ -44,31 +44,32 @@

Source: src/js/views/maps/viewfinder/PredictionView.js
-
'use strict';
-define(
-  ['backbone', 'text!templates/maps/viewfinder/viewfinder-prediction.html'],
-  (Backbone, Template) => {
-    // The base classname to use for this View's template elements.
-    const BASE_CLASS = 'viewfinder-prediction';
-
-    /**
-     * @class PredictionView
-     * @classdesc PredictionView shows an autocomplete suggestion
-     * for the user when they are searching for a place on a map.
-     * @classcategory Views/Maps
-     * @name PredictionView
-     * @extends Backbone.View
-     * @screenshot views/maps/viewfinder/PredictionView.png
-     * @since 2.28.0
-     * @constructs PredictionView
-     */
-    const PredictionView = Backbone.View.extend(
-      /** @lends PredictionView.prototype */{
+            
"use strict";
+define([
+  "backbone",
+  "text!templates/maps/viewfinder/viewfinder-prediction.html",
+], (Backbone, Template) => {
+  // The base classname to use for this View's template elements.
+  const BASE_CLASS = "viewfinder-prediction";
+
+  /**
+   * @class PredictionView
+   * @classdesc PredictionView shows an autocomplete suggestion
+   * for the user when they are searching for a place on a map.
+   * @classcategory Views/Maps
+   * @name PredictionView
+   * @extends Backbone.View
+   * @screenshot views/maps/viewfinder/PredictionView.png
+   * @since 2.28.0
+   * @constructs PredictionView
+   */
+  const PredictionView = Backbone.View.extend(
+    /** @lends PredictionView.prototype */ {
       /**
        * The type of View this is
        * @type {string}
        */
-      type: 'PredictionView',
+      type: "PredictionView",
 
       /**
        * The HTML class to use for this view's outermost element.
@@ -80,7 +81,7 @@ 

Source: src/js/views/maps/viewfinder/PredictionView.jsSource: src/js/views/maps/viewfinder/PredictionView.jsSource: src/js/views/maps/viewfinder/PredictionView.jsSource: src/js/views/maps/viewfinder/PredictionView.js { + this.listenTo(this.viewfinderModel, "change:focusIndex", () => { this.render(); }); }, /** - * Event handler function that selects this element, deselecting any other + * Event handler function that selects this element, deselecting any other * sibling list elements. */ select(event) { @@ -149,15 +150,16 @@

Source: src/js/views/maps/viewfinder/PredictionView.js

diff --git a/docs/docs/src_js_views_maps_viewfinder_PredictionsListView.js.html b/docs/docs/src_js_views_maps_viewfinder_PredictionsListView.js.html index 286d6880d..2f32f0ee5 100644 --- a/docs/docs/src_js_views_maps_viewfinder_PredictionsListView.js.html +++ b/docs/docs/src_js_views_maps_viewfinder_PredictionsListView.js.html @@ -44,35 +44,33 @@

Source: src/js/views/maps/viewfinder/PredictionsListView.
-
'use strict';
-define(
-  [
-    'backbone',
-    'views/maps/viewfinder/PredictionView',
-    'models/maps/viewfinder/ViewfinderModel',
-  ],
-  (Backbone, PredictionView, ViewfinderModel) => {
-    // The base classname to use for this View's template elements.
-    const BASE_CLASS = 'viewfinder-predictions';
-
-    /**
-     * @class PredictionsListView
-     * @classdesc PredictionsListView manages a list of autocomplete
-     * predictions that can be selected by the user.
-     * @classcategory Views/Maps
-     * @name PredictionsListView
-     * @extends Backbone.View
-     * @screenshot views/maps/viewfinder/PredictionsListView.png
-     * @since 2.28.0
-     * @constructs PredictionsListView
-     */
-    var PredictionsListView = Backbone.View.extend(
-      /** @lends PredictionsListView.prototype */{
+            
"use strict";
+define([
+  "backbone",
+  "views/maps/viewfinder/PredictionView",
+  "models/maps/viewfinder/ViewfinderModel",
+], (Backbone, PredictionView, ViewfinderModel) => {
+  // The base classname to use for this View's template elements.
+  const BASE_CLASS = "viewfinder-predictions";
+
+  /**
+   * @class PredictionsListView
+   * @classdesc PredictionsListView manages a list of autocomplete
+   * predictions that can be selected by the user.
+   * @classcategory Views/Maps
+   * @name PredictionsListView
+   * @extends Backbone.View
+   * @screenshot views/maps/viewfinder/PredictionsListView.png
+   * @since 2.28.0
+   * @constructs PredictionsListView
+   */
+  var PredictionsListView = Backbone.View.extend(
+    /** @lends PredictionsListView.prototype */ {
       /**
        * The type of View this is
        * @type {string}
        */
-      type: 'PredictionsListView',
+      type: "PredictionsListView",
 
       /**
        * The HTML class to use for this view's outermost element.
@@ -84,7 +82,7 @@ 

Source: src/js/views/maps/viewfinder/PredictionsListView. * The HTML element to use for this view's outermost element. * @type {string} */ - tagName: 'ul', + tagName: "ul", /** * @typedef {Object} ViewfinderViewOptions @@ -99,12 +97,12 @@

Source: src/js/views/maps/viewfinder/PredictionsListView. /** Setup all event listeners on ViewfinderModel. */ setupListeners() { - this.listenTo(this.viewfinderModel, 'change:predictions', () => { + this.listenTo(this.viewfinderModel, "change:predictions", () => { this.render(); }); - this.listenTo(this.viewfinderModel, 'selection-made', (newQuery) => { - if (this.viewfinderModel.get('query') === newQuery) return; + this.listenTo(this.viewfinderModel, "selection-made", (newQuery) => { + if (this.viewfinderModel.get("query") === newQuery) return; this.clear(); }); @@ -122,26 +120,30 @@

Source: src/js/views/maps/viewfinder/PredictionsListView. /** * Render the Prediction sub-views, tracking * them so they can be removed and their event listeners - * cleaned up. + * cleaned up. */ render() { this.clear(); - this.children = this.viewfinderModel.get('predictions').map((prediction, index) => { - const view = new PredictionView({ - index, - predictionModel: prediction, - viewfinderModel: this.viewfinderModel, + this.children = this.viewfinderModel + .get("predictions") + .map((prediction, index) => { + const view = new PredictionView({ + index, + predictionModel: prediction, + viewfinderModel: this.viewfinderModel, + }); + view.render(); + return view; }); - view.render(); - return view; - }); - this.$el.html(this.children.map(view => view.el)); + this.$el.html(this.children.map((view) => view.el)); }, - }); + }, + ); - return PredictionsListView; - });

+ return PredictionsListView; +}); +
diff --git a/docs/docs/src_js_views_maps_viewfinder_SearchView.js.html b/docs/docs/src_js_views_maps_viewfinder_SearchView.js.html index 6ccd6c1ae..2a622756a 100644 --- a/docs/docs/src_js_views_maps_viewfinder_SearchView.js.html +++ b/docs/docs/src_js_views_maps_viewfinder_SearchView.js.html @@ -44,59 +44,57 @@

Source: src/js/views/maps/viewfinder/SearchView.js

-
'use strict';
-
-define(
-  [
-    'underscore',
-    'backbone',
-    'text!templates/maps/viewfinder/viewfinder-search.html',
-    'views/maps/viewfinder/PredictionsListView',
-    'models/maps/viewfinder/ViewfinderModel',
-    "views/maps/SearchInputView",
-  ],
-  (
-    _,
-    Backbone,
-    Template,
-    PredictionsListView,
-    ViewfinderModel,
-    SearchInputView,
-  ) => {
-    // The base classname to use for this View's template elements.
-    const BASE_CLASS = 'viewfinder-search';
-    // The HTML classes to use for this view's HTML elements.
-    const CLASS_NAMES = {
-      predictions: `${BASE_CLASS}__predictions`,
-      searchInput: `${BASE_CLASS}__search-input`,
-    };
-
-    /**
-     * @class SearchView
-     * @classdesc SearchView allows a user to search for
-     * a latitude and longitude in the map view, and find suggestions
-     * for places related to their search terms.
-     * This view requires a Google Maps API key in order to function properly,
-     * and must have the Geocoding API and Places API enabled.
-     * @classcategory Views/Maps
-     * @name SearchView
-     * @extends Backbone.View
-     * @screenshot views/maps/viewfinder/SearchView.png
-     * @since 2.29.0
-     * @constructs SearchView
-     */
-    var SearchView = Backbone.View.extend(
+            
"use strict";
+
+define([
+  "underscore",
+  "backbone",
+  "text!templates/maps/viewfinder/viewfinder-search.html",
+  "views/maps/viewfinder/PredictionsListView",
+  "models/maps/viewfinder/ViewfinderModel",
+  "views/maps/SearchInputView",
+], (
+  _,
+  Backbone,
+  Template,
+  PredictionsListView,
+  ViewfinderModel,
+  SearchInputView,
+) => {
+  // The base classname to use for this View's template elements.
+  const BASE_CLASS = "viewfinder-search";
+  // The HTML classes to use for this view's HTML elements.
+  const CLASS_NAMES = {
+    predictions: `${BASE_CLASS}__predictions`,
+    searchInput: `${BASE_CLASS}__search-input`,
+  };
+
+  /**
+   * @class SearchView
+   * @classdesc SearchView allows a user to search for
+   * a latitude and longitude in the map view, and find suggestions
+   * for places related to their search terms.
+   * This view requires a Google Maps API key in order to function properly,
+   * and must have the Geocoding API and Places API enabled.
+   * @classcategory Views/Maps
+   * @name SearchView
+   * @extends Backbone.View
+   * @screenshot views/maps/viewfinder/SearchView.png
+   * @since 2.29.0
+   * @constructs SearchView
+   */
+  var SearchView = Backbone.View.extend(
     /** @lends SearchView.prototype */ {
       /**
        * The type of View this is
        * @type {string}
        */
-      type: 'SearchView',
+      type: "SearchView",
 
       /** @inheritdoc */
       className: BASE_CLASS,
 
-      /** 
+      /**
        * Values meant to be used by the rendered HTML template.
        */
       templateVars: {
@@ -107,7 +105,7 @@ 

Source: src/js/views/maps/viewfinder/SearchView.js

* @typedef {Object} SearchViewOptions * @property {ViewfinderModel} viewfinderModel The model associated * with this view allowing control of panning to different locations on - * the map, and displaying location related search features. + * the map, and displaying location related search features. */ initialize({ viewfinderModel }) { this.childPredictionViews = []; @@ -117,20 +115,20 @@

Source: src/js/views/maps/viewfinder/SearchView.js

this.autocompleteSearch = _.debounce(() => { this.viewfinderModel.autocompleteSearch( - this.searchInput.getInputValue() + this.searchInput.getInputValue(), ); }, 250 /* milliseconds */); }, /** Setup all event listeners on ViewfinderModel. */ setupListeners() { - this.listenTo(this.viewfinderModel, 'selection-made', (newQuery) => { + this.listenTo(this.viewfinderModel, "selection-made", (newQuery) => { this.setQuery(newQuery); this.searchInput.blur(); }); - this.listenTo(this.viewfinderModel, 'change:error', () => { - this.searchInput.setError(this.viewfinderModel.get('error')); + this.listenTo(this.viewfinderModel, "change:error", () => { + this.searchInput.setError(this.viewfinderModel.get("error")); }); }, @@ -144,7 +142,7 @@

Source: src/js/views/maps/viewfinder/SearchView.js

}, /** - * Getter function for the list of predictions. + * Getter function for the list of predictions. * @return {HTMLUListElement} Returns the predictions unordered list * HTML element. */ @@ -153,7 +151,7 @@

Source: src/js/views/maps/viewfinder/SearchView.js

}, /** - * Getter function for the search query input. + * Getter function for the search query input. * @return {HTMLInputElement} Returns the search input HTML element. */ getSearchInput() { @@ -165,14 +163,14 @@

Source: src/js/views/maps/viewfinder/SearchView.js

* of an input field (default behavior). */ keydown(event) { - if (event.key === 'ArrowUp') { + if (event.key === "ArrowUp") { event.preventDefault(); } // Unset query value since error is cleared and it should show up again // if the user re-enters the same value. - if (this.searchInput.getInputValue() === '') { - this.viewfinderModel.unset('query', { silent: true }); + if (this.searchInput.getInputValue() === "") { + this.viewfinderModel.unset("query", { silent: true }); } }, @@ -182,15 +180,15 @@

Source: src/js/views/maps/viewfinder/SearchView.js

}, /** - * Event handler for Backbone.View configuration that is called whenever + * Event handler for Backbone.View configuration that is called whenever * the user types a key. */ async keyup(event) { - if (event.key === 'Enter') { + if (event.key === "Enter") { this.search(); - } else if (event.key === 'ArrowUp') { + } else if (event.key === "ArrowUp") { this.viewfinderModel.decrementFocusIndex(); - } else if (event.key === 'ArrowDown') { + } else if (event.key === "ArrowDown") { this.viewfinderModel.incrementFocusIndex(); } else { this.autocompleteSearch(this.searchInput.getInputValue()); @@ -209,13 +207,13 @@

Source: src/js/views/maps/viewfinder/SearchView.js

showPredictionsList() { this.getList().show(); this.viewfinderModel.autocompleteSearch( - this.searchInput.getInputValue() + this.searchInput.getInputValue(), ); }, /** * Hide the predictions list unless user is selecting a list item. - * @param {FocusEvent} event Mouse event corresponding to a change in + * @param {FocusEvent} event Mouse event corresponding to a change in * focus. */ hidePredictionsList(event) { @@ -226,25 +224,25 @@

Source: src/js/views/maps/viewfinder/SearchView.js

}, /** - * Render the SearchInputView. + * Render the SearchInputView. */ renderSearchInput() { this.searchInput = new SearchInputView({ placeholder: "Enter coordinates or areas of interest", - search: text => { + search: (text) => { this.viewfinderModel.search(text); return false; }, - keyupCallback: event => { + keyupCallback: (event) => { this.keyup(event); }, - focusCallback: event => { + focusCallback: (event) => { this.showPredictionsList(event); }, - blurCallback: event => { + blurCallback: (event) => { this.hidePredictionsList(event); }, - keydownCallback: event => { + keydownCallback: (event) => { this.keydown(event); }, }); @@ -253,11 +251,11 @@

Source: src/js/views/maps/viewfinder/SearchView.js

}, /** - * Render the Prediction sub-views. + * Render the Prediction sub-views. */ renderPredictionsList() { this.predictionsView = new PredictionsListView({ - viewfinderModel: this.viewfinderModel + viewfinderModel: this.viewfinderModel, }); this.getList().html(this.predictionsView.el); this.predictionsView.render(); @@ -276,10 +274,11 @@

Source: src/js/views/maps/viewfinder/SearchView.js

this.focusInput(); }, - }); + }, + ); - return SearchView; - }); + return SearchView; +});
diff --git a/docs/docs/src_js_views_maps_viewfinder_ViewfinderView.js.html b/docs/docs/src_js_views_maps_viewfinder_ViewfinderView.js.html index f8c085e01..36cefcec7 100644 --- a/docs/docs/src_js_views_maps_viewfinder_ViewfinderView.js.html +++ b/docs/docs/src_js_views_maps_viewfinder_ViewfinderView.js.html @@ -44,80 +44,77 @@

Source: src/js/views/maps/viewfinder/ViewfinderView.js
-
'use strict';
-
-define(
-  [
-    'underscore',
-    'backbone',
-    'text!templates/maps/viewfinder/viewfinder.html',
-    'views/maps/viewfinder/SearchView',
-    'views/maps/viewfinder/ZoomPresetsListView',
-    'views/maps/viewfinder/ExpansionPanelView',
-    'models/maps/viewfinder/ExpansionPanelsModel',
-    'models/maps/viewfinder/ViewfinderModel',
-  ],
-  (
-    _,
-    Backbone,
-    Template,
-    SearchView,
-    ZoomPresetsListView,
-    ExpansionPanelView,
-    ExpansionPanelsModel,
-    ViewfinderModel,
-  ) => {
-    // The base classname to use for this View's template elements.
-    const BASE_CLASS = 'viewfinder';
-    // The HTML classes to use for this view's HTML elements.
-    const CLASS_NAMES = {
-      searchView: `${BASE_CLASS}__search`,
-      zoomPresetsView: `${BASE_CLASS}__zoom-presets`,
-    };
-
-
-    /**
-     * @class ViewfinderView
-     * @classdesc ViewfinderView allows a user to search for
-     * a latitude and longitude in the map view, and find suggestions
-     * for places related to their search terms.
-     * @classcategory Views/Maps
-     * @name ViewfinderView
-     * @extends Backbone.View
-     * @screenshot views/maps/viewfinder/ViewfinderView.png
-     * @since 2.28.0
-     * @constructs ViewfinderView
-     */
-    var ViewfinderView = Backbone.View.extend(
-      /** @lends ViewfinderView.prototype */{
-        /**
-         * The type of View this is
-         * @type {string}
-         */
-        type: 'ViewfinderView',
-
-        /**
-         * The HTML class to use for this view's outermost element.
-         * @type {string}
-         */
-        className: BASE_CLASS,
-
-        /** 
-         * Values meant to be used by the rendered HTML template.
-         */
-        templateVars: {
-          classNames: CLASS_NAMES,
-        },
-
-        /**
-         * @typedef {Object} ViewfinderViewOptions
-         * @property {Map} The Map model associated with this view allowing control
-         * of panning to different locations on the map. 
-         */
-        initialize({ model: mapModel }) {
-          this.viewfinderModel = new ViewfinderModel({ mapModel });
-          this.panelsModel = new ExpansionPanelsModel({ isMulti: true });
-        },
+            
"use strict";
+
+define([
+  "underscore",
+  "backbone",
+  "text!templates/maps/viewfinder/viewfinder.html",
+  "views/maps/viewfinder/SearchView",
+  "views/maps/viewfinder/ZoomPresetsListView",
+  "views/maps/viewfinder/ExpansionPanelView",
+  "models/maps/viewfinder/ExpansionPanelsModel",
+  "models/maps/viewfinder/ViewfinderModel",
+], (
+  _,
+  Backbone,
+  Template,
+  SearchView,
+  ZoomPresetsListView,
+  ExpansionPanelView,
+  ExpansionPanelsModel,
+  ViewfinderModel,
+) => {
+  // The base classname to use for this View's template elements.
+  const BASE_CLASS = "viewfinder";
+  // The HTML classes to use for this view's HTML elements.
+  const CLASS_NAMES = {
+    searchView: `${BASE_CLASS}__search`,
+    zoomPresetsView: `${BASE_CLASS}__zoom-presets`,
+  };
+
+  /**
+   * @class ViewfinderView
+   * @classdesc ViewfinderView allows a user to search for
+   * a latitude and longitude in the map view, and find suggestions
+   * for places related to their search terms.
+   * @classcategory Views/Maps
+   * @name ViewfinderView
+   * @extends Backbone.View
+   * @screenshot views/maps/viewfinder/ViewfinderView.png
+   * @since 2.28.0
+   * @constructs ViewfinderView
+   */
+  var ViewfinderView = Backbone.View.extend(
+    /** @lends ViewfinderView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "ViewfinderView",
+
+      /**
+       * The HTML class to use for this view's outermost element.
+       * @type {string}
+       */
+      className: BASE_CLASS,
+
+      /**
+       * Values meant to be used by the rendered HTML template.
+       */
+      templateVars: {
+        classNames: CLASS_NAMES,
+      },
+
+      /**
+       * @typedef {Object} ViewfinderViewOptions
+       * @property {Map} The Map model associated with this view allowing control
+       * of panning to different locations on the map.
+       */
+      initialize({ model: mapModel }) {
+        this.viewfinderModel = new ViewfinderModel({ mapModel });
+        this.panelsModel = new ExpansionPanelsModel({ isMulti: true });
+      },
 
       /**
        * Get the ZoomPresetsView element.
@@ -128,13 +125,13 @@ 

Source: src/js/views/maps/viewfinder/ViewfinderView.jsSource: src/js/views/maps/viewfinder/ViewfinderView.js { + zoomPresets: this.viewfinderModel.get("zoomPresets"), + selectZoomPreset: (preset) => { this.viewfinderModel.selectZoomPreset(preset); }, }); const expansionPanel = new ExpansionPanelView({ contentViewInstance: zoomPresetsListView, - icon: 'icon-plane', + icon: "icon-plane", panelsModel: this.panelsModel, - title: 'Zoom to...', + title: "Zoom to...", startOpen: true, }); expansionPanel.render(); - this.getZoomPresets().append(expansionPanel.el); - }, - - /** Render child SearchView and append to DOM. */ - renderSearchView() { - this.searchView = new SearchView({ - viewfinderModel: this.viewfinderModel, - }); - this.searchView.render(); - - this.getSearch().append(this.searchView.el); - }, - - /** - * Render the view by updating the HTML of the element. - * The new HTML is computed from an HTML template that - * is passed an object with relevant view state. - * */ - render() { - this.el.innerHTML = _.template(Template)(this.templateVars); - - this.renderSearchView(); - if (this.viewfinderModel.get('zoomPresets').length) { - this.renderZoomPresetsView(); - } - }, - }); - - return ViewfinderView; - });

+ this.getZoomPresets().append(expansionPanel.el); + }, + + /** Render child SearchView and append to DOM. */ + renderSearchView() { + this.searchView = new SearchView({ + viewfinderModel: this.viewfinderModel, + }); + this.searchView.render(); + + this.getSearch().append(this.searchView.el); + }, + + /** + * Render the view by updating the HTML of the element. + * The new HTML is computed from an HTML template that + * is passed an object with relevant view state. + * */ + render() { + this.el.innerHTML = _.template(Template)(this.templateVars); + + this.renderSearchView(); + if (this.viewfinderModel.get("zoomPresets").length) { + this.renderZoomPresetsView(); + } + }, + }, + ); + + return ViewfinderView; +}); +

diff --git a/docs/docs/src_js_views_maps_viewfinder_ZoomPresetView.js.html b/docs/docs/src_js_views_maps_viewfinder_ZoomPresetView.js.html index 3a31c7533..2829379c8 100644 --- a/docs/docs/src_js_views_maps_viewfinder_ZoomPresetView.js.html +++ b/docs/docs/src_js_views_maps_viewfinder_ZoomPresetView.js.html @@ -44,112 +44,112 @@

Source: src/js/views/maps/viewfinder/ZoomPresetView.js
-
'use strict';
-
-define(
-  [
-    'underscore',
-    'backbone',
-    'text!templates/maps/viewfinder/viewfinder-zoom-preset.html',
-  ],
-  (_, Backbone, Template) => {
-    // The base classname to use for this View's template elements.
-    const BASE_CLASS = 'viewfinder-zoom-preset';
-    //The HTML classes to use for this view's HTML elements.
-    const CLASS_NAMES = {
-      active: `${BASE_CLASS}--active`,
-      description: `${BASE_CLASS}__description`,
-      layer: `${BASE_CLASS}__layer`,
-      layerContent: `${BASE_CLASS}__layer-content`,
-      layers: `${BASE_CLASS}__layers`,
-      preset: `${BASE_CLASS}__preset`,
-      title: `${BASE_CLASS}__title`,
-    };
-    // A function that does nothing. Can be safely called as a default callback.
-    const noop = () => { };
-
-    /**
-     * @class ZoomPresetView
-     * @classdesc Shows the title, description, and associated layers of a
-     * configured location within a MapView. Users may click on a preset
-     * to zoom to that location.
-     * @classcategory Views/Maps/Viewfinder
-     * @name ZoomPresetView
-     * @extends Backbone.View
-     * @screenshot views/maps/viewfinder/ZoomPresetView.png
-     * @since 2.29.0
-     * @constructs ZoomPresetView
-     */
-    var ZoomPresetView = Backbone.View.extend(
+            
"use strict";
+
+define([
+  "underscore",
+  "backbone",
+  "text!templates/maps/viewfinder/viewfinder-zoom-preset.html",
+], (_, Backbone, Template) => {
+  // The base classname to use for this View's template elements.
+  const BASE_CLASS = "viewfinder-zoom-preset";
+  //The HTML classes to use for this view's HTML elements.
+  const CLASS_NAMES = {
+    active: `${BASE_CLASS}--active`,
+    description: `${BASE_CLASS}__description`,
+    layer: `${BASE_CLASS}__layer`,
+    layerContent: `${BASE_CLASS}__layer-content`,
+    layers: `${BASE_CLASS}__layers`,
+    preset: `${BASE_CLASS}__preset`,
+    title: `${BASE_CLASS}__title`,
+  };
+  // A function that does nothing. Can be safely called as a default callback.
+  const noop = () => {};
+
+  /**
+   * @class ZoomPresetView
+   * @classdesc Shows the title, description, and associated layers of a
+   * configured location within a MapView. Users may click on a preset
+   * to zoom to that location.
+   * @classcategory Views/Maps/Viewfinder
+   * @name ZoomPresetView
+   * @extends Backbone.View
+   * @screenshot views/maps/viewfinder/ZoomPresetView.png
+   * @since 2.29.0
+   * @constructs ZoomPresetView
+   */
+  var ZoomPresetView = Backbone.View.extend(
     /** @lends ZoomPresetView.prototype */ {
-        /**
-         * The type of View this is
-         * @type {string}
-         */
-        type: 'ZoomPresetView',
-
-        /** @inheritdoc */
-        className: BASE_CLASS,
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * @type {Object}
-        */
-        events() {
-          return {
-            [`click .${CLASS_NAMES.preset}`]: 'select',
-          };
-        },
-
-        resetActiveState() {
-          this.el.classList.remove(CLASS_NAMES.active);
-        },
-
-        /**
-         * Add the active class and call the select callback function set on
-         * this view by the parent ZoomPresetsListView.
-         */
-        select() {
-          this.selectCallback();
-
-          this.el.classList.add(CLASS_NAMES.active);
-        },
-
-        /** Values meant to be used by the rendered HTML template. */
-        templateVars: {
-          classNames: CLASS_NAMES,
-          preset: {},
-        },
-
-        /**
-         * @typedef {Object} ZoomPresetViewOptions
-         * @property {ZoomPresetModel} The metadata associated with this zoom
-         * preset.
-         * @property {Function} selectCallback to be called when this preset is
-         * selected.
-         */
-        initialize({ preset, selectCallback }) {
-          this.selectCallback = typeof selectCallback === 'function'
-            ? selectCallback : noop;
-          this.templateVars.preset = {
-            title: preset.get('title'),
-            description: preset.get('description'),
-            enabledLayerLabels: preset.get('enabledLayerLabels'),
-          };
-        },
-
-        /**
-         * Render the view by updating the HTML of the element.
-         * The new HTML is computed from an HTML template that
-         * is passed an object with relevant view state.
-         * */
-        render() {
-          this.el.innerHTML = _.template(Template)(this.templateVars);
-        },
-      });
-
-    return ZoomPresetView;
-  });
+ /** + * The type of View this is + * @type {string} + */ + type: "ZoomPresetView", + + /** @inheritdoc */ + className: BASE_CLASS, + + /** + * The events this view will listen to and the associated function to call. + * @type {Object} + */ + events() { + return { + [`click .${CLASS_NAMES.preset}`]: "select", + }; + }, + + resetActiveState() { + this.el.classList.remove(CLASS_NAMES.active); + }, + + /** + * Add the active class and call the select callback function set on + * this view by the parent ZoomPresetsListView. + */ + select() { + this.selectCallback(); + + this.el.classList.add(CLASS_NAMES.active); + }, + + /** Values meant to be used by the rendered HTML template. */ + templateVars: { + classNames: CLASS_NAMES, + preset: {}, + }, + + /** + * @typedef {Object} ZoomPresetViewOptions + * @property {ZoomPresetModel} The metadata associated with this zoom + * preset. + * @property {Function} selectCallback to be called when this preset is + * selected. + */ + initialize({ preset, selectCallback }) { + this.selectCallback = + typeof selectCallback === "function" ? selectCallback : noop; + this.templateVars.preset = { + title: preset.get("title"), + description: preset.get("description"), + enabledLayerLabels: preset.get("enabledLayerLabels"), + }; + }, + + /** + * Render the view by updating the HTML of the element. + * The new HTML is computed from an HTML template that + * is passed an object with relevant view state. + * */ + render() { + this.el.innerHTML = _.template(Template)(this.templateVars); + }, + }, + ); + + return ZoomPresetView; +}); +

diff --git a/docs/docs/src_js_views_maps_viewfinder_ZoomPresetsListView.js.html b/docs/docs/src_js_views_maps_viewfinder_ZoomPresetsListView.js.html index aa4937057..4fbece901 100644 --- a/docs/docs/src_js_views_maps_viewfinder_ZoomPresetsListView.js.html +++ b/docs/docs/src_js_views_maps_viewfinder_ZoomPresetsListView.js.html @@ -44,77 +44,77 @@

Source: src/js/views/maps/viewfinder/ZoomPresetsListView.
-
'use strict';
-
-define(
-  [
-    'underscore',
-    'backbone',
-    'views/maps/viewfinder/ZoomPresetView',
-  ],
-  (_, Backbone, ZoomPresetView) => {
-    // The base classname to use for this View's template elements.
-    const BASE_CLASS = 'viewfinder-zoom-presets';
-
-    /**
-     * @class ZoomPresetsListView
-     * @classdesc Allow user to zoom to a preset location with certain data
-     * layers enabled.
-     * @classcategory Views/Maps/Viewfinder
-     * @name ZoomPresetsListView
-     * @extends Backbone.View
-     * @screenshot views/maps/viewfinder/ZoomPresetsListView.png
-     * @since 2.29.0
-     * @constructs ZoomPresetsListView
-     */
-    var ZoomPresetsListView = Backbone.View.extend(
+            
"use strict";
+
+define(["underscore", "backbone", "views/maps/viewfinder/ZoomPresetView"], (
+  _,
+  Backbone,
+  ZoomPresetView,
+) => {
+  // The base classname to use for this View's template elements.
+  const BASE_CLASS = "viewfinder-zoom-presets";
+
+  /**
+   * @class ZoomPresetsListView
+   * @classdesc Allow user to zoom to a preset location with certain data
+   * layers enabled.
+   * @classcategory Views/Maps/Viewfinder
+   * @name ZoomPresetsListView
+   * @extends Backbone.View
+   * @screenshot views/maps/viewfinder/ZoomPresetsListView.png
+   * @since 2.29.0
+   * @constructs ZoomPresetsListView
+   */
+  var ZoomPresetsListView = Backbone.View.extend(
     /** @lends ZoomPresetsListView.prototype */ {
-        /**
-         * The type of View this is
-         * @type {string}
-         */
-        type: 'ZoomPresetsListView',
-
-        /** @inheritdoc */
-        className: BASE_CLASS,
-
-        /**
-         * @typedef {Object} ZoomPresetsListViewOptions
-         * @property {ZoomPreset[]} zoomPresets The zoom presets to render.
-         * @property {Function} selectZoomPreset The callback function for 
-         * selecting a zoom preset.
-         */
-        initialize({ zoomPresets, selectZoomPreset }) {
-          this.children = [];
-          this.zoomPresets = zoomPresets;
-          this.selectZoomPreset = selectZoomPreset;
-        },
-
-        /**
-         * Render the view by updating the HTML of the element.
-         */
-        render() {
-          this.children = this.zoomPresets.map(preset => {
-            const view = new ZoomPresetView({
-              selectCallback: () => {
-                this.selectZoomPreset(preset);
-                this.children.forEach(child => {
-                  child.resetActiveState();
-                });
-              },
-              preset,
-            });
-            view.render();
-
-            this.el.appendChild(view.el);
-
-            return view;
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "ZoomPresetsListView",
+
+      /** @inheritdoc */
+      className: BASE_CLASS,
+
+      /**
+       * @typedef {Object} ZoomPresetsListViewOptions
+       * @property {ZoomPreset[]} zoomPresets The zoom presets to render.
+       * @property {Function} selectZoomPreset The callback function for
+       * selecting a zoom preset.
+       */
+      initialize({ zoomPresets, selectZoomPreset }) {
+        this.children = [];
+        this.zoomPresets = zoomPresets;
+        this.selectZoomPreset = selectZoomPreset;
+      },
+
+      /**
+       * Render the view by updating the HTML of the element.
+       */
+      render() {
+        this.children = this.zoomPresets.map((preset) => {
+          const view = new ZoomPresetView({
+            selectCallback: () => {
+              this.selectZoomPreset(preset);
+              this.children.forEach((child) => {
+                child.resetActiveState();
+              });
+            },
+            preset,
           });
-        },
-      });
+          view.render();
 
-    return ZoomPresetsListView;
-  });
+ this.el.appendChild(view.el); + + return view; + }); + }, + }, + ); + + return ZoomPresetsListView; +}); +
diff --git a/docs/docs/src_js_views_metadata_EML211EditorView.js.html b/docs/docs/src_js_views_metadata_EML211EditorView.js.html index 542e54c9c..6259dd179 100644 --- a/docs/docs/src_js_views_metadata_EML211EditorView.js.html +++ b/docs/docs/src_js_views_metadata_EML211EditorView.js.html @@ -44,1126 +44,1278 @@

Source: src/js/views/metadata/EML211EditorView.js

-
/* global define */
-define(['underscore',
-  'jquery',
-  'backbone',
-  'localforage',
-  'collections/DataPackage',
-  'models/metadata/eml211/EML211',
-  'models/metadata/eml211/EMLOtherEntity',
-  'models/metadata/ScienceMetadata',
-  'views/EditorView',
-  'views/CitationView',
-  'views/DataPackageView',
-  'views/metadata/EML211View',
-  'views/metadata/EMLEntityView',
-  'views/SignInView',
-  'text!templates/editor.html',
-  'collections/ObjectFormats',
-  'text!templates/editorSubmitMessage.html'],
-  function (_, $, Backbone, LocalForage,
-    DataPackage, EML, EMLOtherEntity, ScienceMetadata,
-    EditorView, CitationView, DataPackageView, EMLView, EMLEntityView, SignInView,
-    EditorTemplate, ObjectFormats, EditorSubmitMessageTemplate) {
-
-    /**
-    * @class EML211EditorView
-    * @classdesc A view of a form for creating and editing EML 2.1.1 documents
-    * @classcategory Views/Metadata
-    * @name EML211EditorView
-    * @extends EditorView
-    * @constructs
-    */
-    var EML211EditorView = EditorView.extend(
-      /** @lends EML211EditorView.prototype */{
-
-        type: "EML211Editor",
-
-        /* The initial editor layout */
-        template: _.template(EditorTemplate),
-        editorSubmitMessageTemplate: _.template(EditorSubmitMessageTemplate),
-
-        /**
-        * The text to use in the editor submit button
-        * @type {string}
-        */
-        submitButtonText: MetacatUI.appModel.get("editorSaveButtonText"),
-
-        /**
-        * The events this view will listen to and the associated function to call.
-        * This view will inherit events from the parent class, EditorView.
-        * @type {Object}
-        */
-        events: _.extend(EditorView.prototype.events, {
-          "change": "saveDraft",
-          "click .data-package-item .edit": "showEntity"
-        }),
-
-        /**
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "localforage",
+  "collections/DataPackage",
+  "models/metadata/eml211/EML211",
+  "models/metadata/eml211/EMLOtherEntity",
+  "models/metadata/ScienceMetadata",
+  "views/EditorView",
+  "views/CitationView",
+  "views/DataPackageView",
+  "views/metadata/EML211View",
+  "views/metadata/EMLEntityView",
+  "views/SignInView",
+  "text!templates/editor.html",
+  "collections/ObjectFormats",
+  "text!templates/editorSubmitMessage.html",
+], function (
+  _,
+  $,
+  Backbone,
+  LocalForage,
+  DataPackage,
+  EML,
+  EMLOtherEntity,
+  ScienceMetadata,
+  EditorView,
+  CitationView,
+  DataPackageView,
+  EMLView,
+  EMLEntityView,
+  SignInView,
+  EditorTemplate,
+  ObjectFormats,
+  EditorSubmitMessageTemplate,
+) {
+  /**
+   * @class EML211EditorView
+   * @classdesc A view of a form for creating and editing EML 2.1.1 documents
+   * @classcategory Views/Metadata
+   * @name EML211EditorView
+   * @extends EditorView
+   * @constructs
+   */
+  var EML211EditorView = EditorView.extend(
+    /** @lends EML211EditorView.prototype */ {
+      type: "EML211Editor",
+
+      /* The initial editor layout */
+      template: _.template(EditorTemplate),
+      editorSubmitMessageTemplate: _.template(EditorSubmitMessageTemplate),
+
+      /**
+       * The text to use in the editor submit button
+       * @type {string}
+       */
+      submitButtonText: MetacatUI.appModel.get("editorSaveButtonText"),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * This view will inherit events from the parent class, EditorView.
+       * @type {Object}
+       */
+      events: _.extend(EditorView.prototype.events, {
+        change: "saveDraft",
+        "click .data-package-item .edit": "showEntity",
+      }),
+
+      /**
         The identifier of the root package EML being rendered
         * @type {string}
         */
-        pid: null,
-
-        /**
-        * A list of the subviews of the editor
-        * @type {Backbone.Views[]}
-        */
-        subviews: [],
-
-        /**
-        * The data package view
-        * @type {DataPackageView}
-        */
-        dataPackageView: null,
-
-        /**
-        * Initialize a new EML211EditorView - called post constructor
-        */
-        initialize: function (options) {
-
-          // Ensure the object formats are cached for the editor's use
-          if (typeof MetacatUI.objectFormats === "undefined") {
-            MetacatUI.objectFormats = new ObjectFormats();
-            MetacatUI.objectFormats.fetch();
-
+      pid: null,
+
+      /**
+       * A list of the subviews of the editor
+       * @type {Backbone.Views[]}
+       */
+      subviews: [],
+
+      /**
+       * The data package view
+       * @type {DataPackageView}
+       */
+      dataPackageView: null,
+
+      /**
+       * Initialize a new EML211EditorView - called post constructor
+       */
+      initialize: function (options) {
+        // Ensure the object formats are cached for the editor's use
+        if (typeof MetacatUI.objectFormats === "undefined") {
+          MetacatUI.objectFormats = new ObjectFormats();
+          MetacatUI.objectFormats.fetch();
+        }
+        return this;
+      },
+
+      /**
+       * Create a new EML model for this view
+       */
+      createModel: function () {
+        //If no pid is given, create a new EML model
+        if (!this.pid) var model = new EML({ synced: true });
+        //Otherwise create a generic metadata model until we find out the formatId
+        else var model = new ScienceMetadata({ id: this.pid });
+
+        // Once the ScienceMetadata is populated, populate the associated package
+        this.model = model;
+
+        //Listen for the replace event on this model
+        var view = this;
+        this.listenTo(this.model, "replace", function (newModel) {
+          if (view.model.get("id") == newModel.get("id")) {
+            view.model = newModel;
+            view.setListeners();
+          }
+        });
+
+        this.setListeners();
+      },
+
+      /**
+       * Render the view
+       */
+      render: function () {
+        var view = this;
+
+        //Execute the superclass render() function, which will add some basic Editor functionality
+        EditorView.prototype.render.call(this);
+
+        MetacatUI.appModel.set("headerType", "default");
+
+        //Empty the view element first
+        this.$el.empty();
+
+        //Inert the basic template on the page
+        this.$el.html(
+          this.template({
+            loading: MetacatUI.appView.loadingTemplate({
+              msg: "Starting the editor...",
+            }),
+            submitButtonText: this.submitButtonText,
+          }),
+        );
+
+        //If we don't have a model at this point, create one
+        if (!this.model) this.createModel();
+
+        // Before rendering the editor, we must:
+        // 1. Make sure the user is signed in
+        // 2. Fetch the metadata
+        // 3. Use the metadata to identify and then fetch the resource map
+        // 4. Make sure the user has write permission on the metadata
+        // 5. Make sure the user has write permission on the resource map
+
+        // As soon as we have all of the metadata information (STEP 2 complete)...
+        this.listenToOnce(this.model, "sync", function () {
+          // Skip the remaining steps the metadata doesn't exist.
+          if (this.model.get("notFound") == true) {
+            this.showNotFound();
+            return;
           }
-          return this;
-        },
-
-        /**
-        * Create a new EML model for this view
-        */
-        createModel: function () {
-
-          //If no pid is given, create a new EML model
-          if (!this.pid)
-            var model = new EML({ 'synced': true });
-          //Otherwise create a generic metadata model until we find out the formatId
-          else
-            var model = new ScienceMetadata({ id: this.pid });
-
-          // Once the ScienceMetadata is populated, populate the associated package
-          this.model = model;
-
-          //Listen for the replace event on this model
-          var view = this;
-          this.listenTo(this.model, "replace", function (newModel) {
-            if (view.model.get("id") == newModel.get("id")) {
-              view.model = newModel;
-              view.setListeners();
-            }
-          });
-
-          this.setListeners();
-        },
-
-        /**
-        * Render the view
-        */
-        render: function () {
-
-          var view = this;
-
-          //Execute the superclass render() function, which will add some basic Editor functionality
-          EditorView.prototype.render.call(this);
-
-          MetacatUI.appModel.set('headerType', 'default');
-
-          //Empty the view element first
-          this.$el.empty();
-
-          //Inert the basic template on the page
-          this.$el.html(this.template({
-            loading: MetacatUI.appView.loadingTemplate({ msg: "Starting the editor..." }),
-            submitButtonText: this.submitButtonText
-          }));
-
-          //If we don't have a model at this point, create one
-          if (!this.model) this.createModel();
-
-          // Before rendering the editor, we must:
-          // 1. Make sure the user is signed in
-          // 2. Fetch the metadata
-          // 3. Use the metadata to identify and then fetch the resource map
-          // 4. Make sure the user has write permission on the metadata
-          // 5. Make sure the user has write permission on the resource map
-
-          // As soon as we have all of the metadata information (STEP 2 complete)...
-          this.listenToOnce(this.model, "sync", function () {
-
-            // Skip the remaining steps the metadata doesn't exist.
-            if (this.model.get("notFound") == true) {
-              this.showNotFound();
-              return
-            }
-
-            // STEP 3
-            // Listen for a trigger from the getDataPackage function that indicates
-            // The data package (resource map) has been retrieved.
-            this.listenToOnce(this, "dataPackageFound", function () {
-
-              var resourceMap = MetacatUI.rootDataPackage.packageModel;
 
-              // STEP 5
-              // Once we have the resource map, then check that the user is authorized to edit this package.
-              this.listenToOnce(resourceMap, "change:isAuthorized_write", function (model, authorization) {
+          // STEP 3
+          // Listen for a trigger from the getDataPackage function that indicates
+          // The data package (resource map) has been retrieved.
+          this.listenToOnce(this, "dataPackageFound", function () {
+            var resourceMap = MetacatUI.rootDataPackage.packageModel;
+
+            // STEP 5
+            // Once we have the resource map, then check that the user is authorized to edit this package.
+            this.listenToOnce(
+              resourceMap,
+              "change:isAuthorized_write",
+              function (model, authorization) {
                 // Render if authorized (will show not authorized if not)
                 this.renderEditorComponents();
-              });
-              // No need to check authorization for a new resource map
-              if (resourceMap.isNew()) {
-                resourceMap.set("isAuthorized_write", true);
-              } else {
-                resourceMap.checkAuthority("write");
-                this.updateLoadingText("Loading metadata...");
-              }
-
-            });
-
-            this.getDataPackage();
-
-            // STEP 4
-            // Check the authority of this user to edit the metadata
-            this.listenToOnce(this.model, "change:isAuthorized_write", function (model, authorization) {
-              // Render if authorized (will show not authorized if not)
-              this.renderEditorComponents();
-            });
-            // If the model is new, no need to check for authorization.
-            if (this.model.isNew()) {
-              this.model.set("isAuthorized_write", true);
+              },
+            );
+            // No need to check authorization for a new resource map
+            if (resourceMap.isNew()) {
+              resourceMap.set("isAuthorized_write", true);
             } else {
-              this.model.checkAuthority("write");
-              this.updateLoadingText("Checking authorization...");
+              resourceMap.checkAuthority("write");
+              this.updateLoadingText("Loading metadata...");
             }
           });
 
-          // STEP 1
-          // Check that the user is signed in
-          var afterAccountChecked = function () {
-            if (MetacatUI.appUserModel.get("loggedIn") == false) {
-              // If they are not signed in, then show the sign-in view
-              view.showSignIn();
-            } else {
-              // STEP 2
-              // If signed in, then fetch model
-              view.fetchModel();
-            }
+          this.getDataPackage();
+
+          // STEP 4
+          // Check the authority of this user to edit the metadata
+          this.listenToOnce(
+            this.model,
+            "change:isAuthorized_write",
+            function (model, authorization) {
+              // Render if authorized (will show not authorized if not)
+              this.renderEditorComponents();
+            },
+          );
+          // If the model is new, no need to check for authorization.
+          if (this.model.isNew()) {
+            this.model.set("isAuthorized_write", true);
+          } else {
+            this.model.checkAuthority("write");
+            this.updateLoadingText("Checking authorization...");
           }
-          // If we've already checked the user account
-          if (MetacatUI.appUserModel.get("checked")) {
-            afterAccountChecked();
+        });
+
+        // STEP 1
+        // Check that the user is signed in
+        var afterAccountChecked = function () {
+          if (MetacatUI.appUserModel.get("loggedIn") == false) {
+            // If they are not signed in, then show the sign-in view
+            view.showSignIn();
+          } else {
+            // STEP 2
+            // If signed in, then fetch model
+            view.fetchModel();
           }
-          // If we haven't checked for authentication yet,
-          // wait until the user info is loaded before we request the Metadata
-          else {
-            this.listenToOnce(MetacatUI.appUserModel, "change:checked", function () {
+        };
+        // If we've already checked the user account
+        if (MetacatUI.appUserModel.get("checked")) {
+          afterAccountChecked();
+        }
+        // If we haven't checked for authentication yet,
+        // wait until the user info is loaded before we request the Metadata
+        else {
+          this.listenToOnce(
+            MetacatUI.appUserModel,
+            "change:checked",
+            function () {
               afterAccountChecked();
-            });
-          }
+            },
+          );
+        }
 
-          // When the user mistakenly drops a file into an area in the window
-          // that isn't a proper drop-target, prevent navigating away from the
-          // page. Without this, the user will lose their progress in the
-          // editor.
-          window.addEventListener("dragover", function (e) {
+        // When the user mistakenly drops a file into an area in the window
+        // that isn't a proper drop-target, prevent navigating away from the
+        // page. Without this, the user will lose their progress in the
+        // editor.
+        window.addEventListener(
+          "dragover",
+          function (e) {
             e = e || event;
             e.preventDefault();
-          }, false);
+          },
+          false,
+        );
 
-          window.addEventListener("drop", function (e) {
+        window.addEventListener(
+          "drop",
+          function (e) {
             e = e || event;
             e.preventDefault();
-          }, false);
-
-          return this;
-        },
-
-        /**
-         * Render the editor components (data package view and metadata view),
-         * or, if not authorized, render the not authorized message.
-         */
-        renderEditorComponents: function () {
-
-          if (!MetacatUI.rootDataPackage.packageModel) {
-            return
-          }
-          var resMapPermission = MetacatUI.rootDataPackage.packageModel.get("isAuthorized_write"),
-            metadataPermission = this.model.get("isAuthorized_write");
-
-          if (resMapPermission === true && metadataPermission === true) {
-            var view = this;
-            // Render the Data Package table.
-            // This function will also render metadata.
-            view.renderDataPackage();
-          } else if (resMapPermission === false || metadataPermission === false) {
-            this.notAuthorized();
-          }
+          },
+          false,
+        );
+
+        return this;
+      },
+
+      /**
+       * Render the editor components (data package view and metadata view),
+       * or, if not authorized, render the not authorized message.
+       */
+      renderEditorComponents: function () {
+        if (!MetacatUI.rootDataPackage.packageModel) {
+          return;
+        }
+        var resMapPermission =
+            MetacatUI.rootDataPackage.packageModel.get("isAuthorized_write"),
+          metadataPermission = this.model.get("isAuthorized_write");
 
-        },
+        if (resMapPermission === true && metadataPermission === true) {
+          var view = this;
+          // Render the Data Package table.
+          // This function will also render metadata.
+          view.renderDataPackage();
+        } else if (resMapPermission === false || metadataPermission === false) {
+          this.notAuthorized();
+        }
+      },
+
+      /**
+       * Fetch the metadata model
+       */
+      fetchModel: function () {
+        //If the user hasn't provided an id, then don't check the authority and mark as synced already
+        if (!this.pid) {
+          this.model.trigger("sync");
+        } else {
+          //Fetch the model
+          this.model.fetch();
+        }
+      },
+
+      /**
+       * @inheritdoc
+       */
+      isAccessPolicyEditEnabled: function () {
+        if (!MetacatUI.appModel.get("allowAccessPolicyChanges")) {
+          return false;
+        }
 
-        /**
-        * Fetch the metadata model
-        */
-        fetchModel: function () {
+        if (!MetacatUI.appModel.get("allowAccessPolicyChangesDatasets")) {
+          return false;
+        }
 
-          //If the user hasn't provided an id, then don't check the authority and mark as synced already
-          if (!this.pid) {
-            this.model.trigger("sync");
+        let limitedTo = MetacatUI.appModel.get(
+          "allowAccessPolicyChangesDatasetsForSubjects",
+        );
+        if (Array.isArray(limitedTo) && limitedTo.length) {
+          return (
+            _.intersection(
+              limitedTo,
+              MetacatUI.appUserModel.get("allIdentitiesAndGroups"),
+            ).length > 0
+          );
+        } else {
+          return true;
+        }
+      },
+
+      /**
+       * Update the text that is shown below the spinner while the editor is loading
+       *
+       * @param {string} message - The message to display
+       */
+      updateLoadingText: function (message) {
+        try {
+          if (!message || typeof message != "string") {
+            console.log(
+              "Was not able to update the loading message, left it as-is. A message must be provided to the updateLoadingText function",
+            );
+            return;
           }
-          else {
-            //Fetch the model
-            this.model.fetch();
+          var loadingPara = this.$el.find(".loading > p");
+          if (loadingPara) {
+            loadingPara.text(message);
           }
-        },
-
-        /**
-        * @inheritdoc
-        */
-        isAccessPolicyEditEnabled: function(){
+        } catch (error) {
+          console.log(
+            "Was not able to update the loading message, left it as-is. Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Get the data package (resource map) associated with the EML. Save it to MetacatUI.rootDataPackage.
+       * The metadata model must already be synced, and the user must be authorized to edit the EML before this function
+       * can run.
+       * @param {Model} scimetaModel - The science metadata model for which to find the associated data package
+       */
+      getDataPackage: function (scimetaModel) {
+        if (!scimetaModel) var scimetaModel = this.model;
+
+        // Check if this package is obsoleted
+        if (this.model.get("obsoletedBy")) {
+          this.showLatestVersion();
+          return;
+        }
 
-          if( !MetacatUI.appModel.get("allowAccessPolicyChanges") ){
-            return false;
+        var resourceMapIds = scimetaModel.get("resourceMap");
+
+        // Case 1: No resource map PID found in the metadata
+        if (
+          typeof resourceMapIds === "undefined" ||
+          resourceMapIds === null ||
+          resourceMapIds.length <= 0
+        ) {
+          // 1A: Check if the rootDataPackage contains the metadata document the user is trying to edit.
+          // Ensure the resource map is not new. If it's a previously unsaved map, then getLatestVersion
+          // will result in a 404.
+          if (
+            MetacatUI.rootDataPackage &&
+            MetacatUI.rootDataPackage.pluck &&
+            !MetacatUI.rootDataPackage.packageModel.isNew() &&
+            _.contains(
+              MetacatUI.rootDataPackage.pluck("id"),
+              this.model.get("id"),
+            )
+          ) {
+            // Remove the cached system metadata XML so we retrieve it again
+            MetacatUI.rootDataPackage.packageModel.set("sysMetaXML", null);
+            this.getLatestResourceMap();
           }
 
-          if( !MetacatUI.appModel.get("allowAccessPolicyChangesDatasets") ){
-            return false;
+          // 1B. If the root data package does not contain the metadata the user is trying to edit,
+          // then create a new data package.
+          else {
+            console.log(
+              "Resource map ids could not be found for " +
+                scimetaModel.id +
+                ", creating a new resource map.",
+            );
+
+            // Create a new DataPackage collection for this view
+            this.createDataPackage();
+            this.trigger("dataPackageFound");
+            // Set the listeners
+            this.setListeners();
           }
 
-          let limitedTo = MetacatUI.appModel.get("allowAccessPolicyChangesDatasetsForSubjects");
-          if( Array.isArray(limitedTo) && limitedTo.length ){
+          // Case 2: A resource map PID was found in the metadata
+        } else {
+          // Create a new data package with this id
+          this.createRootDataPackage([this.model], { id: resourceMapIds[0] });
 
-            return _.intersection(limitedTo, MetacatUI.appUserModel.get("allIdentitiesAndGroups")).length > 0;
+          //Handle the add of the metadata model
+          MetacatUI.rootDataPackage.saveReference(this.model);
 
-          }
-          else{
-            return true;
-          }
+          // 2A. If there is more than one resource map, we need to make sure we fetch the most recent one
+          if (resourceMapIds.length > 1) {
+            this.getLatestResourceMap();
 
-        },
-
-        /**
-         * Update the text that is shown below the spinner while the editor is loading
-         *
-         * @param {string} message - The message to display
-         */
-        updateLoadingText: function (message) {
-          try {
-            if (!message || typeof message != "string") {
-              console.log("Was not able to update the loading message, left it as-is. A message must be provided to the updateLoadingText function");
-              return
-            }
-            var loadingPara = this.$el.find(".loading > p");
-            if (loadingPara) {
-              loadingPara.text(message)
-            }
-          } catch (error) {
-            console.log("Was not able to update the loading message, left it as-is. Error details: " + error);
+            // 2B. Just one resource map found
+          } else {
+            this.listenToOnce(MetacatUI.rootDataPackage, "sync", function () {
+              this.trigger("dataPackageFound");
+            });
+            // Fetch the data package
+            MetacatUI.rootDataPackage.fetch();
           }
-        },
-
-        /**
-        * Get the data package (resource map) associated with the EML. Save it to MetacatUI.rootDataPackage.
-        * The metadata model must already be synced, and the user must be authorized to edit the EML before this function
-        * can run.
-        * @param {Model} scimetaModel - The science metadata model for which to find the associated data package
-        */
-        getDataPackage: function (scimetaModel) {
-
-          if (!scimetaModel)
-            var scimetaModel = this.model;
-
-          // Check if this package is obsoleted
-          if (this.model.get("obsoletedBy")) {
-            this.showLatestVersion();
+        }
+      },
+
+      /**
+       * Get the latest version of the resource map model stored in MetacatUI.rootDataPackage.packageModel.
+       * When the newest resource map is synced, the "dataPackageFound" event will be triggered.
+       */
+      getLatestResourceMap: function () {
+        try {
+          if (
+            !MetacatUI.rootDataPackage ||
+            !MetacatUI.rootDataPackage.packageModel
+          ) {
+            console.log(
+              "Could not get the latest verion of the resource map because no resource map is saved.",
+            );
             return;
           }
-
-          var resourceMapIds = scimetaModel.get("resourceMap");
-
-          // Case 1: No resource map PID found in the metadata
-          if (typeof resourceMapIds === "undefined" || resourceMapIds === null || resourceMapIds.length <= 0) {
-
-            // 1A: Check if the rootDataPackage contains the metadata document the user is trying to edit.
-            // Ensure the resource map is not new. If it's a previously unsaved map, then getLatestVersion
-            // will result in a 404.
-            if (
-              MetacatUI.rootDataPackage &&
-              MetacatUI.rootDataPackage.pluck &&
-              !MetacatUI.rootDataPackage.packageModel.isNew() &&
-              _.contains(MetacatUI.rootDataPackage.pluck("id"), this.model.get("id"))
-            ) {
-
-              // Remove the cached system metadata XML so we retrieve it again
-              MetacatUI.rootDataPackage.packageModel.set("sysMetaXML", null);
-              this.getLatestResourceMap();
-
-            }
-
-            // 1B. If the root data package does not contain the metadata the user is trying to edit,
-            // then create a new data package.
-            else {
-
-              console.log("Resource map ids could not be found for " + scimetaModel.id + ", creating a new resource map.");
-
-              // Create a new DataPackage collection for this view
-              this.createDataPackage();
-              this.trigger("dataPackageFound");
-              // Set the listeners
-              this.setListeners();
-            }
-
-            // Case 2: A resource map PID was found in the metadata
-          } else {
-
-            // Create a new data package with this id
-            this.createRootDataPackage([this.model], { id: resourceMapIds[0] });
-
-            //Handle the add of the metadata model
-            MetacatUI.rootDataPackage.saveReference(this.model);
-
-            // 2A. If there is more than one resource map, we need to make sure we fetch the most recent one
-            if (resourceMapIds.length > 1) {
-              this.getLatestResourceMap();
-
-              // 2B. Just one resource map found
-            } else {
-
-              this.listenToOnce(MetacatUI.rootDataPackage, "sync", function () {
-                this.trigger("dataPackageFound");
-              })
-              // Fetch the data package
-              MetacatUI.rootDataPackage.fetch();
-            }
-
-          }
-
-        },
-
-
-        /**
-         * Get the latest version of the resource map model stored in MetacatUI.rootDataPackage.packageModel.
-         * When the newest resource map is synced, the "dataPackageFound" event will be triggered.
-         */
-        getLatestResourceMap: function () {
-
-          try {
-
-            if (!MetacatUI.rootDataPackage || !MetacatUI.rootDataPackage.packageModel) {
-              console.log("Could not get the latest verion of the resource map because no resource map is saved.");
-              return
-            }
-            // Make sure we have the latest version of the resource map before we allow editing
-            this.listenToOnce(MetacatUI.rootDataPackage.packageModel, "latestVersionFound", function (model) {
+          // Make sure we have the latest version of the resource map before we allow editing
+          this.listenToOnce(
+            MetacatUI.rootDataPackage.packageModel,
+            "latestVersionFound",
+            function (model) {
               //Create a new data package for the latest version package
-              this.createRootDataPackage([this.model], { id: model.get("latestVersion") });
+              this.createRootDataPackage([this.model], {
+                id: model.get("latestVersion"),
+              });
               //Handle the add of the metadata model
               MetacatUI.rootDataPackage.saveReference(this.model);
               this.listenToOnce(MetacatUI.rootDataPackage, "sync", function () {
                 this.trigger("dataPackageFound");
-              })
+              });
               // Fetch the data package
               MetacatUI.rootDataPackage.fetch();
-            });
-
-            //Find the latest version of the resource map
-            MetacatUI.rootDataPackage.packageModel.findLatestVersion();
-          } catch (error) {
-            console.log("Error attempting to find the latest version of the resource map. Error details: " + error);
-          }
-        },
-
-        /**
-        * Creates a DataPackage collection for this EML211EditorView and sets it on the MetacatUI
-        * global object (as `rootDataPackage`)
-        */
-        createDataPackage: function () {
-          // Create a new Data packages
-          this.createRootDataPackage([this.model], { packageModelAttrs: { synced: true }})
-
-          try{
-            //Inherit the access policy of the metadata document, if the metadata document is not `new`
-            if(!this.model.isNew()){
-              let metadataAccPolicy = this.model.get("accessPolicy");
-              let accPolicy = MetacatUI.rootDataPackage.packageModel.get("accessPolicy")
-
-              //If there is no access policy, it hasn't been fetched yet, so wait
-              if( !metadataAccPolicy.length ){
-                //If the model is of ScienceMetadata class, we need to wait for the "replace" function,
-                // which happens when the model is fetched and an EML211 model is created to replace it.
-                if( this.model.type == "ScienceMetadata" ){
-                   this.listenTo(this.model, "replace", function(){
-                     this.listenToOnce(this.model, "sysMetaUpdated", function(){
-                       accPolicy.copyAccessPolicy(this.model.get("accessPolicy"))
-                       MetacatUI.rootDataPackage.packageModel.set("rightsHolder", this.model.get("rightsHolder"));
-                     });
-                   });
-                }
-              }
-              else{
-                accPolicy.copyAccessPolicy(this.model.get("accessPolicy"))
+            },
+          );
+
+          //Find the latest version of the resource map
+          MetacatUI.rootDataPackage.packageModel.findLatestVersion();
+        } catch (error) {
+          console.log(
+            "Error attempting to find the latest version of the resource map. Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Creates a DataPackage collection for this EML211EditorView and sets it on the MetacatUI
+       * global object (as `rootDataPackage`)
+       */
+      createDataPackage: function () {
+        // Create a new Data packages
+        this.createRootDataPackage([this.model], {
+          packageModelAttrs: { synced: true },
+        });
+
+        try {
+          //Inherit the access policy of the metadata document, if the metadata document is not `new`
+          if (!this.model.isNew()) {
+            let metadataAccPolicy = this.model.get("accessPolicy");
+            let accPolicy =
+              MetacatUI.rootDataPackage.packageModel.get("accessPolicy");
+
+            //If there is no access policy, it hasn't been fetched yet, so wait
+            if (!metadataAccPolicy.length) {
+              //If the model is of ScienceMetadata class, we need to wait for the "replace" function,
+              // which happens when the model is fetched and an EML211 model is created to replace it.
+              if (this.model.type == "ScienceMetadata") {
+                this.listenTo(this.model, "replace", function () {
+                  this.listenToOnce(this.model, "sysMetaUpdated", function () {
+                    accPolicy.copyAccessPolicy(this.model.get("accessPolicy"));
+                    MetacatUI.rootDataPackage.packageModel.set(
+                      "rightsHolder",
+                      this.model.get("rightsHolder"),
+                    );
+                  });
+                });
               }
+            } else {
+              accPolicy.copyAccessPolicy(this.model.get("accessPolicy"));
             }
           }
-          catch(e){
-            console.error("Could not copy the access policy from the metadata to the resource map: ", e);
-          }
-
-          //Handle the add of the metadata model
-          MetacatUI.rootDataPackage.handleAdd(this.model);
+        } catch (e) {
+          console.error(
+            "Could not copy the access policy from the metadata to the resource map: ",
+            e,
+          );
+        }
 
-          // Associate the science metadata with the resource map
-          if (this.model.get && Array.isArray(this.model.get("resourceMap"))) {
-            this.model.get("resourceMap").push(MetacatUI.rootDataPackage.packageModel.id);
+        //Handle the add of the metadata model
+        MetacatUI.rootDataPackage.handleAdd(this.model);
+
+        // Associate the science metadata with the resource map
+        if (this.model.get && Array.isArray(this.model.get("resourceMap"))) {
+          this.model
+            .get("resourceMap")
+            .push(MetacatUI.rootDataPackage.packageModel.id);
+        } else {
+          this.model.set(
+            "resourceMap",
+            MetacatUI.rootDataPackage.packageModel.id,
+          );
+        }
 
-          } else {
-            this.model.set("resourceMap", MetacatUI.rootDataPackage.packageModel.id);
+        // Set the sysMetaXML for the packageModel
+        MetacatUI.rootDataPackage.packageModel.set(
+          "sysMetaXML",
+          MetacatUI.rootDataPackage.packageModel.serializeSysMeta(),
+        );
+      },
+
+      /**
+       * Creates a {@link DataPackage} collection for this Editor view, and saves it as the Root Data Package of the app.
+       * This centralizes the DataPackage creation so listeners and other functionality is always performed
+       * @param {(DataONEObject[]|ScienceMetadata[]|EML211[])} models - An array of models to add to the collection
+       * @param {object} [attributes] A literal object of attributes to pass to the DataPackage.initialize() function
+       * @since 2.17.1
+       */
+      createRootDataPackage: function (models, attributes) {
+        MetacatUI.rootDataPackage = new DataPackage(models, attributes);
+
+        this.listenTo(
+          MetacatUI.rootDataPackage.packageModel,
+          "change:numLoadingFiles",
+          this.toggleEnableControls,
+        );
+      },
+
+      renderChildren: function (model, options) {},
+
+      /**
+       * Render the Data Package View and insert it into this view
+       */
+      renderDataPackage: function () {
+        var view = this;
+
+        if (MetacatUI.rootDataPackage.packageModel.isNew()) {
+          view.renderMember(this.model);
+        }
 
+        // As the root collection is updated with models, render the UI
+        this.listenTo(MetacatUI.rootDataPackage, "add", function (model) {
+          if (!model.get("synced") && model.get("id"))
+            this.listenTo(model, "sync", view.renderMember);
+          else if (model.get("synced")) view.renderMember(model);
+
+          //Listen for changes on this member
+          model.on("change:fileName", model.addToUploadQueue);
+        });
+
+        //Render the Data Package view
+        this.dataPackageView = new DataPackageView({
+          edit: true,
+          dataPackage: MetacatUI.rootDataPackage,
+          parentEditorView: this,
+        });
+
+        //Render the view
+        var $packageTableContainer = this.$("#data-package-container");
+        $packageTableContainer.html(this.dataPackageView.render().el);
+
+        //Make the view resizable on the bottom
+        var handle = $(document.createElement("div"))
+          .addClass("ui-resizable-handle ui-resizable-s")
+          .attr("title", "Drag to resize")
+          .append(
+            $(document.createElement("i")).addClass("icon icon-caret-down"),
+          );
+        $packageTableContainer.after(handle);
+        $packageTableContainer.resizable({
+          handles: { s: handle },
+          minHeight: 100,
+          maxHeight: 900,
+          resize: function () {
+            view.emlView.resizeTOC();
+          },
+        });
+
+        var tableHeight = ($(window).height() - $("#Navbar").height()) * 0.4;
+        $packageTableContainer.css("height", tableHeight + "px");
+
+        var table = this.dataPackageView.$el;
+        this.listenTo(this.dataPackageView, "addOne", function () {
+          if (
+            table.outerHeight() > $packageTableContainer.outerHeight() &&
+            table.outerHeight() < 220
+          ) {
+            $packageTableContainer.css(
+              "height",
+              table.outerHeight() + handle.outerHeight(),
+            );
+            if (this.emlView) this.emlView.resizeTOC();
           }
+        });
+
+        if (this.emlView) this.emlView.resizeTOC();
+
+        //Save the view as a subview
+        this.subviews.push(this.dataPackageView);
+
+        this.listenTo(
+          MetacatUI.rootDataPackage.packageModel,
+          "change:childPackages",
+          this.renderChildren,
+        );
+      },
+
+      /**
+       * Calls the appropriate render method depending on the model type
+       */
+      renderMember: function (model, collection, options) {
+        // Render metadata or package information, based on the type
+        if (typeof model.attributes === "undefined") {
+          return;
+        } else {
+          switch (model.get("type")) {
+            case "DataPackage":
+              // Do recursive rendering here for sub packages
+              break;
+
+            case "Metadata":
+              // this.renderDataPackageItem(model, collection, options);
+              this.renderMetadata(model, collection, options);
+              break;
+
+            case "Data":
+              //this.renderDataPackageItem(model, collection, options);
+              break;
+
+            default:
+              console.log("model.type is not set correctly");
+          }
+        }
+      },
+
+      /**
+       * Renders the metadata section of the EML211EditorView
+       */
+      renderMetadata: function (model, collection, options) {
+        if (!model && this.model) var model = this.model;
+        if (!model) return;
+
+        var emlView, dataPackageView;
+
+        // render metadata as the collection is updated, but only EML passed from the event
+        if (
+          typeof model.get === "undefined" ||
+          !(
+            model.get("formatId") === "eml://ecoinformatics.org/eml-2.1.1" ||
+            model.get("formatId") === "https://eml.ecoinformatics.org/eml-2.2.0"
+          )
+        ) {
+          console.log("Not EML. TODO: Render generic ScienceMetadata.");
+          return;
+        }
 
-          // Set the sysMetaXML for the packageModel
-          MetacatUI.rootDataPackage.packageModel.set("sysMetaXML",
-            MetacatUI.rootDataPackage.packageModel.serializeSysMeta());
-        },
-
-        /**
-        * Creates a {@link DataPackage} collection for this Editor view, and saves it as the Root Data Package of the app.
-        * This centralizes the DataPackage creation so listeners and other functionality is always performed
-        * @param {(DataONEObject[]|ScienceMetadata[]|EML211[])} models - An array of models to add to the collection
-        * @param {object} [attributes] A literal object of attributes to pass to the DataPackage.initialize() function
-        * @since 2.17.1
-        */
-        createRootDataPackage: function(models, attributes){
-          MetacatUI.rootDataPackage = new DataPackage(models, attributes);
-
-          this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:numLoadingFiles", this.toggleEnableControls);
-        },
-
-        renderChildren: function (model, options) {
-
-
-        },
-
-        /**
-         * Render the Data Package View and insert it into this view
-         */
-        renderDataPackage: function () {
-
-          var view = this;
-
-          if(MetacatUI.rootDataPackage.packageModel.isNew()){
-            view.renderMember(this.model);
-          };
-
-          // As the root collection is updated with models, render the UI
-          this.listenTo(MetacatUI.rootDataPackage, "add", function (model) {
-
-            if (!model.get("synced") && model.get('id'))
-              this.listenTo(model, "sync", view.renderMember);
-            else if (model.get("synced"))
-              view.renderMember(model);
-
-            //Listen for changes on this member
-            model.on("change:fileName", model.addToUploadQueue);
-          });
+        //Create an EML model
+        if (model.type != "EML") {
+          //Create a new EML model from the ScienceMetadata model
+          var EMLmodel = new EML(model.toJSON());
+          //Replace the old ScienceMetadata model in the collection
+          MetacatUI.rootDataPackage.remove(model);
+          MetacatUI.rootDataPackage.add(EMLmodel, { silent: true });
+          MetacatUI.rootDataPackage.handleAdd(EMLmodel);
+          model.trigger("replace", EMLmodel);
+
+          //Fetch the EML and render it
+          this.listenToOnce(EMLmodel, "sync", this.renderMetadata);
+          EMLmodel.fetch();
+
+          return;
+        }
 
-          //Render the Data Package view
+        //Create an EML211 View and render it
+        emlView = new EMLView({
+          model: model,
+          edit: true,
+        });
+        this.subviews.push(emlView);
+        this.emlView = emlView;
+        emlView.render();
+
+        //Show the required fields for this editor
+        this.renderRequiredIcons(this.getRequiredFields());
+        this.listenTo(emlView, "editorInputsAdded", function () {
+          this.trigger("editorInputsAdded");
+        });
+
+        // Create a citation view and render it
+        var citationView = new CitationView({
+          model: model,
+          defaultTitle: "Untitled dataset",
+          createLink: false,
+          createTitleLink: !model.isNew(),
+        });
+
+        this.subviews.push(citationView);
+        $("#citation-container").html(citationView.render().$el);
+
+        //Remove the rendering class from the body element
+        $("body").removeClass("rendering");
+
+        // Focus the folder name field once loaded but only if this is a new
+        // document
+        if (!this.pid) {
+          $("#data-package-table-body td.name").focus();
+        }
+      },
+
+      /**
+       * Renders the data package section of the EML211EditorView
+       */
+      renderDataPackageItem: function (model, collection, options) {
+        var hasPackageSubView = _.find(
+          this.subviews,
+          function (subview) {
+            return subview.id === "data-package-table";
+          },
+          model,
+        );
+
+        // Only create the package table if it hasn't been created
+        if (!hasPackageSubView) {
           this.dataPackageView = new DataPackageView({
-            edit: true,
             dataPackage: MetacatUI.rootDataPackage,
-            parentEditorView: this
-          });
-
-          //Render the view
-          var $packageTableContainer = this.$("#data-package-container");
-          $packageTableContainer.html(this.dataPackageView.render().el);
-
-          //Make the view resizable on the bottom
-          var handle = $(document.createElement("div"))
-            .addClass("ui-resizable-handle ui-resizable-s")
-            .attr("title", "Drag to resize")
-            .append($(document.createElement("i")).addClass("icon icon-caret-down"));
-          $packageTableContainer.after(handle);
-          $packageTableContainer.resizable({
-            handles: { "s": handle },
-            minHeight: 100,
-            maxHeight: 900,
-            resize: function () {
-              view.emlView.resizeTOC();
-            }
-          });
-
-          var tableHeight = ($(window).height() - $("#Navbar").height()) * .40;
-          $packageTableContainer.css("height", tableHeight + "px");
-
-          var table = this.dataPackageView.$el;
-          this.listenTo(this.dataPackageView, "addOne", function () {
-            if (table.outerHeight() > $packageTableContainer.outerHeight() && table.outerHeight() < 220) {
-              $packageTableContainer.css("height", table.outerHeight() + handle.outerHeight());
-              if (this.emlView)
-                this.emlView.resizeTOC();
-            }
+            edit: true,
+            parentEditorView: this,
           });
-
-          if (this.emlView)
-            this.emlView.resizeTOC();
-
-          //Save the view as a subview
           this.subviews.push(this.dataPackageView);
-
-          this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:childPackages", this.renderChildren);
-        },
-
-
-        /**
-         * Calls the appropriate render method depending on the model type
-         */
-        renderMember: function (model, collection, options) {
-
-          // Render metadata or package information, based on the type
-          if (typeof model.attributes === "undefined") {
-            return;
-
-          } else {
-            switch (model.get("type")) {
-              case "DataPackage":
-                // Do recursive rendering here for sub packages
-                break;
-
-              case "Metadata":
-
-                // this.renderDataPackageItem(model, collection, options);
-                this.renderMetadata(model, collection, options);
-                break;
-
-              case "Data":
-                //this.renderDataPackageItem(model, collection, options);
-                break;
-
-              default:
-                console.log("model.type is not set correctly");
-
-            }
-          }
-        },
-
-
-        /**
-         * Renders the metadata section of the EML211EditorView
-         */
-        renderMetadata: function (model, collection, options) {
-
-          if (!model && this.model) var model = this.model;
-          if (!model) return;
-
-          var emlView, dataPackageView;
-
-          // render metadata as the collection is updated, but only EML passed from the event
-          if (typeof model.get === "undefined" ||
-            !(
-              model.get("formatId") === "eml://ecoinformatics.org/eml-2.1.1" ||
-              model.get("formatId") === "https://eml.ecoinformatics.org/eml-2.2.0"
-            )) {
-            console.log("Not EML. TODO: Render generic ScienceMetadata.");
-            return;
-
-          }
-
-          //Create an EML model
-          if (model.type != "EML") {
-              //Create a new EML model from the ScienceMetadata model
-              var EMLmodel = new EML(model.toJSON());
-              //Replace the old ScienceMetadata model in the collection
-              MetacatUI.rootDataPackage.remove(model);
-              MetacatUI.rootDataPackage.add(EMLmodel, { silent: true });
-              MetacatUI.rootDataPackage.handleAdd(EMLmodel);
-              model.trigger("replace", EMLmodel);
-
-              //Fetch the EML and render it
-              this.listenToOnce(EMLmodel, "sync", this.renderMetadata);
-              EMLmodel.fetch();
-
-              return;
-            }
-
-          //Create an EML211 View and render it
-          emlView = new EMLView({
-            model: model,
-            edit: true
-          });
-          this.subviews.push(emlView);
-          this.emlView = emlView;
-          emlView.render();
-
-          //Show the required fields for this editor
-          this.renderRequiredIcons(this.getRequiredFields());
-          this.listenTo(emlView, "editorInputsAdded", function(){
-            this.trigger("editorInputsAdded")
-          });
-
-          // Create a citation view and render it
-          var citationView = new CitationView({
-            model: model,
-            defaultTitle: "Untitled dataset",
-            createLink: false,
-            createTitleLink: !model.isNew()
-          });
-
-          this.subviews.push(citationView);
-          $("#citation-container").html(citationView.render().$el);
-
-          //Remove the rendering class from the body element
-          $("body").removeClass("rendering");
-
-          // Focus the folder name field once loaded but only if this is a new
-          // document
-          if (!this.pid) {
-            $("#data-package-table-body td.name").focus();
-          }
-
-        },
-
-
-        /**
-         * Renders the data package section of the EML211EditorView
-         */
-        renderDataPackageItem: function (model, collection, options) {
-
-          var hasPackageSubView =
-            _.find(this.subviews, function (subview) {
-              return subview.id === "data-package-table";
-            }, model);
-
-          // Only create the package table if it hasn't been created
-          if (!hasPackageSubView) {
-            this.dataPackageView = new DataPackageView({
-              dataPackage: MetacatUI.rootDataPackage,
-              edit: true,
-              parentEditorView: this
-            });
-            this.subviews.push(this.dataPackageView);
-            dataPackageView.render();
-
-          }
-        },
-
-        /**
-         * Set listeners on the view's model for various reasons.
-         * This function centralizes all the listeners so that when/if the view's model is replaced, the listeners would be reset.
-         */
-        setListeners: function () {
-
-          this.listenTo(this.model, "change:uploadStatus", this.showControls);
-
-          // Register a listener for any attribute change
-          this.model.on("change", this.model.handleChange, this.model);
-
-          // Register a listener to save drafts on change
-          this.model.on("change", this.model.saveDraft, this.model);
-
-          // If any attributes have changed (including nested objects), show the controls
-          if (typeof MetacatUI.rootDataPackage.packageModel !== "undefined") {
-            this.stopListening(MetacatUI.rootDataPackage.packageModel, "change:changed");
-            this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:changed", this.toggleControls);
-            this.listenTo(MetacatUI.rootDataPackage.packageModel, "change:changed", function (event) {
+          dataPackageView.render();
+        }
+      },
+
+      /**
+       * Set listeners on the view's model for various reasons.
+       * This function centralizes all the listeners so that when/if the view's model is replaced, the listeners would be reset.
+       */
+      setListeners: function () {
+        this.listenTo(this.model, "change:uploadStatus", this.showControls);
+
+        // Register a listener for any attribute change
+        this.model.on("change", this.model.handleChange, this.model);
+
+        // Register a listener to save drafts on change
+        this.model.on("change", this.model.saveDraft, this.model);
+
+        // If any attributes have changed (including nested objects), show the controls
+        if (typeof MetacatUI.rootDataPackage.packageModel !== "undefined") {
+          this.stopListening(
+            MetacatUI.rootDataPackage.packageModel,
+            "change:changed",
+          );
+          this.listenTo(
+            MetacatUI.rootDataPackage.packageModel,
+            "change:changed",
+            this.toggleControls,
+          );
+          this.listenTo(
+            MetacatUI.rootDataPackage.packageModel,
+            "change:changed",
+            function (event) {
               if (MetacatUI.rootDataPackage.packageModel.get("changed")) {
                 // Put this metadata model in the queue when the package has been changed
                 // Don't put it in the queue if it's in the process of saving already
                 if (this.model.get("uploadStatus") != "p")
                   this.model.set("uploadStatus", "q");
               }
-            });
-
-          }
-
-          if (MetacatUI.rootDataPackage && DataPackage.prototype.isPrototypeOf(MetacatUI.rootDataPackage)) {
-            // If the Data Package failed saving, display an error message
-            this.listenTo(MetacatUI.rootDataPackage, "errorSaving", this.saveError);
-
-            // Listen for when the package has been successfully saved
-            this.listenTo(MetacatUI.rootDataPackage, "successSaving", this.saveSuccess);
+            },
+          );
+        }
 
-            //When the Data Package cancels saving, hide the saving styling
-            this.listenTo(MetacatUI.rootDataPackage, "cancelSave", this.hideSaving);
-            this.listenTo(MetacatUI.rootDataPackage, "cancelSave", this.handleSaveCancel);
-          }
+        if (
+          MetacatUI.rootDataPackage &&
+          DataPackage.prototype.isPrototypeOf(MetacatUI.rootDataPackage)
+        ) {
+          // If the Data Package failed saving, display an error message
+          this.listenTo(
+            MetacatUI.rootDataPackage,
+            "errorSaving",
+            this.saveError,
+          );
+
+          // Listen for when the package has been successfully saved
+          this.listenTo(
+            MetacatUI.rootDataPackage,
+            "successSaving",
+            this.saveSuccess,
+          );
+
+          //When the Data Package cancels saving, hide the saving styling
+          this.listenTo(
+            MetacatUI.rootDataPackage,
+            "cancelSave",
+            this.hideSaving,
+          );
+          this.listenTo(
+            MetacatUI.rootDataPackage,
+            "cancelSave",
+            this.handleSaveCancel,
+          );
+        }
 
-          //When the model is invalid, show the required fields
-          this.listenTo(this.model, "invalid", this.showValidation);
-          this.listenTo(this.model, "valid", this.showValidation);
-
-          // When a data package member fails to load, remove it and warn the user
-          this.listenTo(MetacatUI.eventDispatcher, "fileLoadError", this.handleFileLoadError);
-
-          // When a data package member fails to be read, remove it and warn the user
-          this.listenTo(MetacatUI.eventDispatcher, "fileReadError", this.handleFileReadError);
-
-          //Set a beforeunload event only if there isn't one already
-          if (!this.beforeunloadCallback) {
-            var view = this;
-            //When the Window is about to be closed, show a confirmation message
-            this.beforeunloadCallback = function (e) {
-              if (!view.canClose()) {
-                //Browsers don't support custom confirmation messages anymore,
-                // so preventDefault() needs to be called or the return value has to be set
-                e.preventDefault();
-                e.returnValue = "";
-              }
-              return;
+        //When the model is invalid, show the required fields
+        this.listenTo(this.model, "invalid", this.showValidation);
+        this.listenTo(this.model, "valid", this.showValidation);
+
+        // When a data package member fails to load, remove it and warn the user
+        this.listenTo(
+          MetacatUI.eventDispatcher,
+          "fileLoadError",
+          this.handleFileLoadError,
+        );
+
+        // When a data package member fails to be read, remove it and warn the user
+        this.listenTo(
+          MetacatUI.eventDispatcher,
+          "fileReadError",
+          this.handleFileReadError,
+        );
+
+        //Set a beforeunload event only if there isn't one already
+        if (!this.beforeunloadCallback) {
+          var view = this;
+          //When the Window is about to be closed, show a confirmation message
+          this.beforeunloadCallback = function (e) {
+            if (!view.canClose()) {
+              //Browsers don't support custom confirmation messages anymore,
+              // so preventDefault() needs to be called or the return value has to be set
+              e.preventDefault();
+              e.returnValue = "";
             }
-            window.addEventListener("beforeunload", this.beforeunloadCallback);
-          }
-
-        },
-
-        /**
-         * Saves all edits in the collection
-         * @param {Event} e - The DOM Event that triggerd this function
-         */
-        save: function (e) {
-          var btn = (e && e.target) ? $(e.target) : this.$("#save-editor");
-
-          //If the save button is disabled, then we don't want to save right now
-          if (btn.is(".btn-disabled")) return;
-
-          this.showSaving();
-
-          //Save the package!
-          MetacatUI.rootDataPackage.save();
-        },
-
-        /**
-         * When the data package collection saves successfully, tell the user
-         * @param {DataPackage|DataONEObject} savedObject - The model or collection that was just saved
-         */
-        saveSuccess: function (savedObject) {
-
-          //We only want to perform these actions after the package saves
-          if (savedObject.type != "DataPackage") return;
-
-          //Change the URL to the new id
-          MetacatUI.uiRouter.navigate("submit/" + encodeURIComponent(this.model.get("id")), { trigger: false, replace: true });
-
-          this.toggleControls();
-
-          // Construct the save message
-          var message = this.editorSubmitMessageTemplate({
-            messageText: "Your changes have been submitted.",
-            viewURL: MetacatUI.root + "/view/" + encodeURIComponent(this.model.get("id")),
-            buttonText: "View your dataset"
-          });
-
-          MetacatUI.appView.showAlert(message, "alert-success", this.$el, null, { remove: true });
-
-          //Rerender the CitationView
-          var citationView = _.where(this.subviews, { type: "Citation" });
-          if (citationView.length) {
-            citationView[0].createTitleLink = true;
-            citationView[0].render();
-          }
-
-          // Reset the state to clean
-          MetacatUI.rootDataPackage.packageModel.set("changed", false);
-          this.model.set("hasContentChanges", false);
-
-          this.setListeners();
-        },
-
-        /**
-         * When the data package collection fails to save, tell the user
-         * @param {string} errorMsg - The error message from the failed save() function
-         */
-        saveError: function (errorMsg) {
+            return;
+          };
+          window.addEventListener("beforeunload", this.beforeunloadCallback);
+        }
+      },
+
+      /**
+       * Saves all edits in the collection
+       * @param {Event} e - The DOM Event that triggerd this function
+       */
+      save: function (e) {
+        var btn = e && e.target ? $(e.target) : this.$("#save-editor");
+
+        //If the save button is disabled, then we don't want to save right now
+        if (btn.is(".btn-disabled")) return;
+
+        this.showSaving();
+
+        //Save the package!
+        MetacatUI.rootDataPackage.save();
+      },
+
+      /**
+       * When the data package collection saves successfully, tell the user
+       * @param {DataPackage|DataONEObject} savedObject - The model or collection that was just saved
+       */
+      saveSuccess: function (savedObject) {
+        //We only want to perform these actions after the package saves
+        if (savedObject.type != "DataPackage") return;
+
+        //Change the URL to the new id
+        MetacatUI.uiRouter.navigate(
+          "submit/" + encodeURIComponent(this.model.get("id")),
+          { trigger: false, replace: true },
+        );
+
+        this.toggleControls();
+
+        // Construct the save message
+        var message = this.editorSubmitMessageTemplate({
+          messageText: "Your changes have been submitted.",
+          viewURL:
+            MetacatUI.root +
+            "/view/" +
+            encodeURIComponent(this.model.get("id")),
+          buttonText: "View your dataset",
+        });
+
+        MetacatUI.appView.showAlert(message, "alert-success", this.$el, null, {
+          remove: true,
+        });
+
+        //Rerender the CitationView
+        var citationView = _.where(this.subviews, { type: "Citation" });
+        if (citationView.length) {
+          citationView[0].createTitleLink = true;
+          citationView[0].render();
+        }
 
-          var errorId = "error" + Math.round(Math.random() * 100),
-            messageContainer = $(document.createElement("div")).append(document.createElement("p")),
-            messageParagraph = messageContainer.find("p"),
+        // Reset the state to clean
+        MetacatUI.rootDataPackage.packageModel.set("changed", false);
+        this.model.set("hasContentChanges", false);
+
+        this.setListeners();
+      },
+
+      /**
+       * When the data package collection fails to save, tell the user
+       * @param {string} errorMsg - The error message from the failed save() function
+       */
+      saveError: function (errorMsg) {
+        var errorId = "error" + Math.round(Math.random() * 100),
+          messageContainer = $(document.createElement("div")).append(
+            document.createElement("p"),
+          ),
+          messageParagraph = messageContainer.find("p"),
+          messageClasses = "alert-error";
+
+        //Get all the models that have an error
+        var failedModels = MetacatUI.rootDataPackage.where({
+          uploadStatus: "e",
+        });
+
+        //If every failed model is a DataONEObject data file that failed
+        // because of a slow network, construct a specific error message that
+        // is more informative than the usual message
+        if (
+          failedModels.length &&
+          _.every(failedModels, function (m) {
+            return (
+              m.get("type") == "Data" &&
+              m.get("errorMessage").indexOf("network issue") > -1
+            );
+          })
+        ) {
+          //Create a list of file names for the files that failed to upload
+          var failedFileList = $(document.createElement("ul"));
+
+          _.each(
+            failedModels,
+            function (failedModel) {
+              failedFileList.append(
+                $(document.createElement("li")).text(
+                  failedModel.get("fileName"),
+                ),
+              );
+            },
+            this,
+          );
+
+          //Make the error message
+          messageParagraph.text(
+            "The following files could not be uploaded due to a network issue. Make sure you are connected to a reliable internet connection. ",
+          );
+          messageParagraph.after(failedFileList);
+        }
+        //If one of the failed models is this package's metadata model or the
+        // resource map model and it failed to upload due to a network issue,
+        // show a more specific error message
+        else if (
+          _.find(
+            failedModels,
+            function (m) {
+              var errorMsg = m.get("errorMessage") || "";
+              return m == this.model && errorMsg.indexOf("network issue") > -1;
+            },
+            this,
+          ) ||
+          (MetacatUI.rootDataPackage.packageModel.get("uploadStatus") == "e" &&
+            MetacatUI.rootDataPackage.packageModel
+              .get("errorMessage")
+              .indexOf("network issue") > -1)
+        ) {
+          messageParagraph.text(
+            "Your changes could not be submitted due to a network issue. Make sure you are connected to a reliable internet connection. ",
+          );
+        } else {
+          if (
+            this.model.get("draftSaved") &&
+            MetacatUI.appModel.get("editorSaveErrorMsgWithDraft")
+          ) {
+            messageParagraph.text(
+              MetacatUI.appModel.get("editorSaveErrorMsgWithDraft"),
+            );
+            messageClasses = "alert-warning";
+          } else if (MetacatUI.appModel.get("editorSaveErrorMsg")) {
+            messageParagraph.text(MetacatUI.appModel.get("editorSaveErrorMsg"));
+            messageClasses = "alert-error";
+          } else {
+            messageParagraph.text(
+              "Not all of your changes could be submitted.",
+            );
             messageClasses = "alert-error";
-
-          //Get all the models that have an error
-          var failedModels = MetacatUI.rootDataPackage.where({ uploadStatus: "e" });
-
-          //If every failed model is a DataONEObject data file that failed
-          // because of a slow network, construct a specific error message that
-          // is more informative than the usual message
-          if (failedModels.length &&
-            _.every(failedModels, function (m) {
-              return m.get("type") == "Data" &&
-                m.get("errorMessage").indexOf("network issue") > -1
-            })) {
-
-            //Create a list of file names for the files that failed to upload
-            var failedFileList = $(document.createElement("ul"));
-
-            _.each(failedModels, function (failedModel) {
-
-              failedFileList.append($(document.createElement("li")).text(failedModel.get("fileName")));
-
-            }, this);
-
-            //Make the error message
-            messageParagraph.text("The following files could not be uploaded due to a network issue. Make sure you are connected to a reliable internet connection. ");
-            messageParagraph.after(failedFileList);
-          }
-          //If one of the failed models is this package's metadata model or the
-          // resource map model and it failed to upload due to a network issue,
-          // show a more specific error message
-          else if (_.find(failedModels, function (m) {
-            var errorMsg = m.get("errorMessage") || "";
-            return (m == this.model && errorMsg.indexOf("network issue") > -1)
-          }, this) ||
-            (MetacatUI.rootDataPackage.packageModel.get("uploadStatus") == "e" &&
-              MetacatUI.rootDataPackage.packageModel.get("errorMessage").indexOf("network issue") > -1)) {
-
-            messageParagraph.text("Your changes could not be submitted due to a network issue. Make sure you are connected to a reliable internet connection. ");
-
           }
-          else {
-
-            if (this.model.get("draftSaved") && MetacatUI.appModel.get("editorSaveErrorMsgWithDraft")) {
-              messageParagraph.text(MetacatUI.appModel.get("editorSaveErrorMsgWithDraft"));
-              messageClasses = "alert-warning"
-            }
-            else if (MetacatUI.appModel.get("editorSaveErrorMsg")) {
-              messageParagraph.text(MetacatUI.appModel.get("editorSaveErrorMsg"));
-              messageClasses = "alert-error";
-            }
-            else {
-              messageParagraph.text("Not all of your changes could be submitted.");
-              messageClasses = "alert-error";
-            }
 
-            messageParagraph.after($(document.createElement("p")).append($(document.createElement("a"))
-              .text("See technical details")
-              .attr("data-toggle", "collapse")
-              .attr("data-target", "#" + errorId)
-              .addClass("pointer")),
-              $(document.createElement("div"))
-                .addClass("collapse")
-                .attr("id", errorId)
-                .append($(document.createElement("pre")).text(errorMsg)));
-          }
+          messageParagraph.after(
+            $(document.createElement("p")).append(
+              $(document.createElement("a"))
+                .text("See technical details")
+                .attr("data-toggle", "collapse")
+                .attr("data-target", "#" + errorId)
+                .addClass("pointer"),
+            ),
+            $(document.createElement("div"))
+              .addClass("collapse")
+              .attr("id", errorId)
+              .append($(document.createElement("pre")).text(errorMsg)),
+          );
+        }
 
-          MetacatUI.appView.showAlert(messageContainer, messageClasses, this.$el, null, {
+        MetacatUI.appView.showAlert(
+          messageContainer,
+          messageClasses,
+          this.$el,
+          null,
+          {
             emailBody: "Error message: Data Package save error: " + errorMsg,
-            remove: true
-          });
-
-          //Reset the Saving styling
-          this.hideSaving();
-        },
-
-
-        /**
-         * Find the most recently updated version of the metadata
-         */
-        showLatestVersion: function () {
-          var view = this;
-
-          //When the latest version is found,
-          this.listenToOnce(this.model, "change:latestVersion", function () {
-            //Make sure it has a newer version, and if so,
-            if (view.model.get("latestVersion") != view.model.get("id")) {
-              //Get the obsoleted id
-              var oldID = view.model.get("id");
-
-              //Reset the current model
-              view.pid = view.model.get("latestVersion");
-              view.model = null;
-
-              //Update the URL
-              MetacatUI.uiRouter.navigate("submit/" + encodeURIComponent(view.pid), { trigger: false, replace: true });
-
-              //Render the new model
-              view.render();
-
-              //Show a warning that the user was trying to edit old content
-              MetacatUI.appView.showAlert("You've been forwarded to the newest version of your dataset for editing.",
-                "alert-warning", this.$el, 12000, { remove: true });
-            }
-            else {
-              view.getDataPackage();
-            }
-
-          });
-
-          //Find the latest version of this metadata object
-          this.model.findLatestVersion();
-        },
-
-        /**
-         * Show the entity editor
-         * @param {Event} e - The DOM Event that triggerd this function
-         */
-        showEntity: function (e) {
-          if (!e || !e.target)
+            remove: true,
+          },
+        );
+
+        //Reset the Saving styling
+        this.hideSaving();
+      },
+
+      /**
+       * Find the most recently updated version of the metadata
+       */
+      showLatestVersion: function () {
+        var view = this;
+
+        //When the latest version is found,
+        this.listenToOnce(this.model, "change:latestVersion", function () {
+          //Make sure it has a newer version, and if so,
+          if (view.model.get("latestVersion") != view.model.get("id")) {
+            //Get the obsoleted id
+            var oldID = view.model.get("id");
+
+            //Reset the current model
+            view.pid = view.model.get("latestVersion");
+            view.model = null;
+
+            //Update the URL
+            MetacatUI.uiRouter.navigate(
+              "submit/" + encodeURIComponent(view.pid),
+              { trigger: false, replace: true },
+            );
+
+            //Render the new model
+            view.render();
+
+            //Show a warning that the user was trying to edit old content
+            MetacatUI.appView.showAlert(
+              "You've been forwarded to the newest version of your dataset for editing.",
+              "alert-warning",
+              this.$el,
+              12000,
+              { remove: true },
+            );
+          } else {
+            view.getDataPackage();
+          }
+        });
+
+        //Find the latest version of this metadata object
+        this.model.findLatestVersion();
+      },
+
+      /**
+       * Show the entity editor
+       * @param {Event} e - The DOM Event that triggerd this function
+       */
+      showEntity: function (e) {
+        if (!e || !e.target) return;
+
+        //For EML metadata docs
+        if (this.model.type == "EML") {
+          //Get the Entity View
+          var row = $(e.target).parents(".data-package-item"),
+            entityView = row.data("entityView"),
+            dataONEObject = row.data("model");
+
+          if (
+            dataONEObject.get("uploadStatus") == "p" ||
+            dataONEObject.get("uploadStatus") == "l" ||
+            dataONEObject.get("uploadStatus") == "e"
+          )
             return;
 
-          //For EML metadata docs
-          if (this.model.type == "EML") {
-            //Get the Entity View
-            var row = $(e.target).parents(".data-package-item"),
-              entityView = row.data("entityView"),
-              dataONEObject = row.data("model");
-
-            if (dataONEObject.get("uploadStatus") == "p" || dataONEObject.get("uploadStatus") == "l" || dataONEObject.get("uploadStatus") == "e")
-              return;
-
-            //If there isn't a view yet, create one
-            if (!entityView) {
-
-              //Get the entity model for this data package item
-              var entityModel = this.model.getEntity(row.data("model"));
-
-              //Create a new EMLOtherEntity if it doesn't exist
-              if (!entityModel) {
-                entityModel = new EMLOtherEntity({
-                  entityName: dataONEObject.get("fileName"),
-                  entityType: dataONEObject.get("formatId") || dataONEObject.get("mediaType"),
-                  parentModel: this.model,
-                  xmlID: dataONEObject.getXMLSafeID()
-                });
-
-                if (!dataONEObject.get("fileName")) {
-                  //Listen to changes to required fields on the otherEntity models
-                  this.listenTo(entityModel, "change:entityName", function () {
-                    if (!entityModel.isValid()) return;
+          //If there isn't a view yet, create one
+          if (!entityView) {
+            //Get the entity model for this data package item
+            var entityModel = this.model.getEntity(row.data("model"));
+
+            //Create a new EMLOtherEntity if it doesn't exist
+            if (!entityModel) {
+              entityModel = new EMLOtherEntity({
+                entityName: dataONEObject.get("fileName"),
+                entityType:
+                  dataONEObject.get("formatId") ||
+                  dataONEObject.get("mediaType"),
+                parentModel: this.model,
+                xmlID: dataONEObject.getXMLSafeID(),
+              });
 
-                    //Get the position this entity will be in
-                    var position = $(".data-package-item.data").index(row);
+              if (!dataONEObject.get("fileName")) {
+                //Listen to changes to required fields on the otherEntity models
+                this.listenTo(entityModel, "change:entityName", function () {
+                  if (!entityModel.isValid()) return;
 
-                    this.model.addEntity(entityModel, position);
-                  });
-                }
-                else {
                   //Get the position this entity will be in
                   var position = $(".data-package-item.data").index(row);
 
                   this.model.addEntity(entityModel, position);
-                }
-              }
-              else {
-                entityView = new EMLEntityView({
-                  model: entityModel,
-                  DataONEObject: dataONEObject,
-                  edit: true
                 });
-              }
-
-              //Attach the view to the edit button so we can access it again
-              row.data("entityView", entityView);
-
-              //Render the view
-              entityView.render();
-            }
-
-            //Show the modal window editor for this entity
-            if (entityView)
-              entityView.show();
-          }
-
-        },
-
-        /**
-         * Shows a message if the user is not authorized to edit this package
-         */
-        notAuthorized: function () {
+              } else {
+                //Get the position this entity will be in
+                var position = $(".data-package-item.data").index(row);
 
-          // Don't show the not authorized message if the user is authorized to edit the EML and the resource map
-          if (MetacatUI.rootDataPackage && MetacatUI.rootDataPackage.packageModel) {
-            if (
-              MetacatUI.rootDataPackage.packageModel.get("isAuthorized_changePermission") &&
-              this.model.get("isAuthorized")
-            ) {
-              return
-            }
-          } else {
-            if (this.model.get("isAuthorized")) {
-              return
+                this.model.addEntity(entityModel, position);
+              }
+            } else {
+              entityView = new EMLEntityView({
+                model: entityModel,
+                DataONEObject: dataONEObject,
+                edit: true,
+              });
             }
-          }
 
-          this.$("#editor-body").empty();
-          MetacatUI.appView.showAlert("You are not authorized to edit this data set.",
-            "alert-error", "#editor-body");
-
-          //Stop listening to any further events
-          this.stopListening();
-          this.model.off();
-        },
-
-
-        /**
-         * Toggle the editor footer controls (Save bar)
-         */
-        toggleControls: function () {
-          if (MetacatUI.rootDataPackage &&
-            MetacatUI.rootDataPackage.packageModel &&
-            MetacatUI.rootDataPackage.packageModel.get("changed")) {
-            this.showControls();
-
-          } else {
-            this.hideControls();
+            //Attach the view to the edit button so we can access it again
+            row.data("entityView", entityView);
 
+            //Render the view
+            entityView.render();
           }
-        },
-
-        /**
-        * Toggles whether the Save controls for the Editor are enabled or disabled based on various attributes of the DataPackage and its models.
-        * @since 2.17.1
-        */
-        toggleEnableControls: function(){
 
-          if( MetacatUI.rootDataPackage.packageModel.get("isLoadingFiles") ){
-            let noun = MetacatUI.rootDataPackage.packageModel.get("numLoadingFiles") > 1? " files" : " file";
-            this.disableControls("Waiting for " + MetacatUI.rootDataPackage.packageModel.get("numLoadingFiles") + noun + " to upload...");
+          //Show the modal window editor for this entity
+          if (entityView) entityView.show();
+        }
+      },
+
+      /**
+       * Shows a message if the user is not authorized to edit this package
+       */
+      notAuthorized: function () {
+        // Don't show the not authorized message if the user is authorized to edit the EML and the resource map
+        if (
+          MetacatUI.rootDataPackage &&
+          MetacatUI.rootDataPackage.packageModel
+        ) {
+          if (
+            MetacatUI.rootDataPackage.packageModel.get(
+              "isAuthorized_changePermission",
+            ) &&
+            this.model.get("isAuthorized")
+          ) {
+            return;
           }
-          else{
-            this.enableControls();
+        } else {
+          if (this.model.get("isAuthorized")) {
+            return;
           }
+        }
 
-        },
-
-        /**
-         * Show any errors that occured when trying to save changes
-         */
-        showValidation: function () {
-
-          //First clear all the error messaging
-          this.$(".notification.error").empty();
-          this.$(".side-nav-item .icon").hide();
-          this.$("#metadata-container .error").removeClass("error");
-          $(".alert-container:not(:has(.temporary-message))").remove();
-
-
-          var errors = this.model.validationError;
-
-          _.each(errors, function (errorMsg, category) {
-
+        this.$("#editor-body").empty();
+        MetacatUI.appView.showAlert(
+          "You are not authorized to edit this data set.",
+          "alert-error",
+          "#editor-body",
+        );
+
+        //Stop listening to any further events
+        this.stopListening();
+        this.model.off();
+      },
+
+      /**
+       * Toggle the editor footer controls (Save bar)
+       */
+      toggleControls: function () {
+        if (
+          MetacatUI.rootDataPackage &&
+          MetacatUI.rootDataPackage.packageModel &&
+          MetacatUI.rootDataPackage.packageModel.get("changed")
+        ) {
+          this.showControls();
+        } else {
+          this.hideControls();
+        }
+      },
+
+      /**
+       * Toggles whether the Save controls for the Editor are enabled or disabled based on various attributes of the DataPackage and its models.
+       * @since 2.17.1
+       */
+      toggleEnableControls: function () {
+        if (MetacatUI.rootDataPackage.packageModel.get("isLoadingFiles")) {
+          let noun =
+            MetacatUI.rootDataPackage.packageModel.get("numLoadingFiles") > 1
+              ? " files"
+              : " file";
+          this.disableControls(
+            "Waiting for " +
+              MetacatUI.rootDataPackage.packageModel.get("numLoadingFiles") +
+              noun +
+              " to upload...",
+          );
+        } else {
+          this.enableControls();
+        }
+      },
+
+      /**
+       * Show any errors that occured when trying to save changes
+       */
+      showValidation: function () {
+        //First clear all the error messaging
+        this.$(".notification.error").empty();
+        this.$(".side-nav-item .icon").hide();
+        this.$("#metadata-container .error").removeClass("error");
+        $(".alert-container:not(:has(.temporary-message))").remove();
+
+        var errors = this.model.validationError;
+
+        _.each(
+          errors,
+          function (errorMsg, category) {
             var categoryEls = this.$("[data-category='" + category + "']"),
-                dataItemRow = categoryEls.parents(".data-package-item");
+              dataItemRow = categoryEls.parents(".data-package-item");
 
             //If this field is in a DataItemView, then delegate to that view
             if (dataItemRow.length && dataItemRow.data("view")) {
               dataItemRow.data("view").showValidation(category, errorMsg);
               return;
-            }
-            else {
+            } else {
               var elsWithViews = _.filter(categoryEls, function (el) {
-                return ($(el).data("view") &&
+                return (
+                  $(el).data("view") &&
                   $(el).data("view").showValidation &&
-                  !$(el).data("view").isNew);
+                  !$(el).data("view").isNew
+                );
               });
 
               if (elsWithViews.length) {
                 _.each(elsWithViews, function (el) {
                   $(el).data("view").showValidation();
                 });
-              }
-              else if(categoryEls.length) {
+              } else if (categoryEls.length) {
                 //Show the error message
-                categoryEls.filter(".notification").addClass("error").text(errorMsg);
+                categoryEls
+                  .filter(".notification")
+                  .addClass("error")
+                  .text(errorMsg);
 
                 //Add the error message to inputs
                 categoryEls.filter("textarea, input").addClass("error");
@@ -1171,198 +1323,220 @@ 

Source: src/js/views/metadata/EML211EditorView.js

} //Get the link in the table of contents navigation - var navigationLink = this.$(".side-nav-item[data-category='" + category + "']"); + var navigationLink = this.$( + ".side-nav-item[data-category='" + category + "']", + ); if (!navigationLink.length) { var section = categoryEls.parents("[data-section]"); - navigationLink = this.$(".side-nav-item." + $(section).attr("data-section")); + navigationLink = this.$( + ".side-nav-item." + $(section).attr("data-section"), + ); } //Show the error icon in the table of contents - navigationLink.addClass("error") + navigationLink + .addClass("error") .find(".icon") .addClass("error") .show(); this.model.off("change:" + category, this.model.checkValidity); this.model.once("change:" + category, this.model.checkValidity); + }, + this, + ); + + if (errors) { + //Create a list of errors to display in the error message shown to the user + let errorList = "<ul>" + this.getErrorListItem(errors) + "</ul>"; + + MetacatUI.appView.showAlert( + "Fix the errors flagged below before submitting: " + errorList, + "alert-error", + this.$el, + null, + { + remove: true, + }, + ); + } + }, + + /** + * @inheritdoc + */ + hasUnsavedChanges: function () { + //If the form hasn't been edited, we can close this view without confirmation + if ( + typeof MetacatUI.rootDataPackage.getQueue != "function" || + MetacatUI.rootDataPackage.getQueue().length + ) + return true; + else return false; + }, + + /** + * @inheritdoc + */ + onClose: function () { + //Execute the parent class onClose() function + //EditorView.prototype.onClose.call(this); + + //Remove the listener on the Window + if (this.beforeunloadCallback) { + window.removeEventListener("beforeunload", this.beforeunloadCallback); + delete this.beforeunloadCallback; + } - }, this); - - if (errors) { - - //Create a list of errors to display in the error message shown to the user - let errorList = "<ul>" + - this.getErrorListItem(errors) + - "</ul>"; - - - MetacatUI.appView.showAlert("Fix the errors flagged below before submitting: " + errorList, - "alert-error", - this.$el, - null, - { - remove: true - }); - } - - }, - - /** - * @inheritdoc - */ - hasUnsavedChanges: function () { - //If the form hasn't been edited, we can close this view without confirmation - if (typeof MetacatUI.rootDataPackage.getQueue != "function" || MetacatUI.rootDataPackage.getQueue().length) - return true; - else - return false; - }, - - /** - * @inheritdoc - */ - onClose: function () { - - //Execute the parent class onClose() function - //EditorView.prototype.onClose.call(this); - - //Remove the listener on the Window - if (this.beforeunloadCallback) { - window.removeEventListener("beforeunload", this.beforeunloadCallback); - delete this.beforeunloadCallback; - } - - //Stop listening to the "add" event so that new package members aren't rendered. - //Check first if the DataPackage has been intialized. An easy check is to see is - // the 'models' attribute is undefined. If the DataPackage collection has been intialized, - // then it would be an empty array. - if (typeof MetacatUI.rootDataPackage.models !== "undefined") { - this.stopListening(MetacatUI.rootDataPackage, "add"); - } - - //Remove all the other events - this.off(); // remove callbacks, prevent zombies - this.model.off(); - - $(".Editor").removeClass("Editor"); - this.$el.empty(); - - this.model = null; + //Stop listening to the "add" event so that new package members aren't rendered. + //Check first if the DataPackage has been intialized. An easy check is to see is + // the 'models' attribute is undefined. If the DataPackage collection has been intialized, + // then it would be an empty array. + if (typeof MetacatUI.rootDataPackage.models !== "undefined") { + this.stopListening(MetacatUI.rootDataPackage, "add"); + } - // Close each subview - _.each(this.subviews, function (subview) { - if (subview.onClose) - subview.onClose(); + //Remove all the other events + this.off(); // remove callbacks, prevent zombies + this.model.off(); + + $(".Editor").removeClass("Editor"); + this.$el.empty(); + + this.model = null; + + // Close each subview + _.each(this.subviews, function (subview) { + if (subview.onClose) subview.onClose(); + }); + + this.subviews = []; + + this.undelegateEvents(); + }, + + /** + * Handle "fileLoadError" events by alerting the user + * and removing the row from the data package table. + * @param {DataONEObject} item The model item passed by the fileLoadError event + */ + handleFileLoadError: function (item) { + var message; + var fileName; + /* Remove the data package table row */ + this.dataPackageView.removeOne(item); + /* Then inform the user */ + if ( + item && + item.get && + (item.get("fileName") !== "undefined" || + item.get("fileName") !== null) + ) { + fileName = item.get("fileName"); + message = + "The file " + + fileName + + " is already included in this dataset. The duplicate file has not been added."; + } else { + message = + "The chosen file is already included in this dataset. " + + "The duplicate file has not been added."; + } + MetacatUI.appView.showAlert(message, "alert-info", this.el, 10000, { + remove: true, + }); + }, + + /** + * Handle "fileReadError" events by alerting the user + * and removing the row from the data package table. + * @param {DataONEObject} item The model item passed by the fileReadError event + */ + handleFileReadError: function (item) { + var message; + var fileName; + /* Remove the data package table row */ + this.dataPackageView.removeOne(item); + /* Then inform the user */ + if ( + item && + item.get && + (item.get("fileName") !== "undefined" || + item.get("fileName") !== null) + ) { + fileName = item.get("fileName"); + message = + "The file " + + fileName + + " could not be read. You may not have permission to read the file," + + " or the file was too large for your browser to upload. " + + "The file has not been added."; + } else { + message = + "The chosen file " + + " could not be read. You may not have permission to read the file," + + " or the file was too large for your browser to upload. " + + "The file has not been added."; + } + MetacatUI.appView.showAlert(message, "alert-info", this.el, 10000, { + remove: true, + }); + }, + + /** + * Save a draft of the parent EML model + */ + saveDraft: function () { + var view = this; + + try { + var title = this.model.get("title") || "No title"; + // Create a clone of the model that we will use for serialization. + // Don't serialize the model that is currently being edited, + // since serialize may make changes to the model that should not + // happen until the user is ready to save + // (e.g. - create a contact if there is not one) + var draftModel = this.model.clone(); + + LocalForage.setItem(this.model.get("id"), { + id: this.model.get("id"), + datetime: new Date().toISOString(), + title: Array.isArray(title) ? title[0] : title, + draft: draftModel.serialize(), + }).then(function () { + view.clearOldDrafts(); }); - - this.subviews = []; - - this.undelegateEvents(); - - }, - - /** - * Handle "fileLoadError" events by alerting the user - * and removing the row from the data package table. - * @param {DataONEObject} item The model item passed by the fileLoadError event - */ - handleFileLoadError: function (item) { - var message; - var fileName; - /* Remove the data package table row */ - this.dataPackageView.removeOne(item); - /* Then inform the user */ - if (item && item.get && - (item.get("fileName") !== "undefined" || item.get("fileName") !== null)) { - fileName = item.get("fileName"); - message = "The file " + fileName + - " is already included in this dataset. The duplicate file has not been added."; - } else { - message = "The chosen file is already included in this dataset. " + - "The duplicate file has not been added."; - } - MetacatUI.appView.showAlert(message, "alert-info", this.el, 10000, { remove: true }); - }, - - /** - * Handle "fileReadError" events by alerting the user - * and removing the row from the data package table. - * @param {DataONEObject} item The model item passed by the fileReadError event - */ - handleFileReadError: function (item) { - var message; - var fileName; - /* Remove the data package table row */ - this.dataPackageView.removeOne(item); - /* Then inform the user */ - if (item && item.get && - (item.get("fileName") !== "undefined" || item.get("fileName") !== null)) { - fileName = item.get("fileName"); - message = "The file " + fileName + - " could not be read. You may not have permission to read the file," + - " or the file was too large for your browser to upload. " + - "The file has not been added."; - } else { - message = "The chosen file " + - " could not be read. You may not have permission to read the file," + - " or the file was too large for your browser to upload. " + - "The file has not been added."; - } - MetacatUI.appView.showAlert(message, "alert-info", this.el, 10000, { remove: true }); - }, - - /** - * Save a draft of the parent EML model - */ - saveDraft: function () { - var view = this; - - try { - var title = this.model.get("title") || "No title"; - // Create a clone of the model that we will use for serialization. - // Don't serialize the model that is currently being edited, - // since serialize may make changes to the model that should not - // happen until the user is ready to save - // (e.g. - create a contact if there is not one) - var draftModel = this.model.clone(); - - LocalForage.setItem(this.model.get("id"), { - id: this.model.get("id"), - datetime: (new Date()).toISOString(), - title: Array.isArray(title) ? title[0] : title, - draft: draftModel.serialize() - }).then(function () { - view.clearOldDrafts(); + } catch (ex) { + console.log("Error saving draft:", ex); + } + }, + + /** + * Clear older drafts by iterating over the sorted list of drafts + * stored by LocalForage and removing any beyond a hardcoded limit. + */ + clearOldDrafts: function () { + var drafts = []; + + try { + LocalForage.iterate(function (value, key, iterationNumber) { + // Extract each draft + drafts.push({ + key: key, + value: value, }); - } catch (ex) { - console.log("Error saving draft:", ex); - } - }, - - /** - * Clear older drafts by iterating over the sorted list of drafts - * stored by LocalForage and removing any beyond a hardcoded limit. - */ - clearOldDrafts: function () { - var drafts = []; - - try { - LocalForage.iterate(function (value, key, iterationNumber) { - // Extract each draft - drafts.push({ - key: key, - value: value - }); - }).then(function () { + }) + .then(function () { // Sort by datetime drafts = _.sortBy(drafts, function (draft) { return draft.value.datetime.toString(); }).reverse(); - }).then(function () { + }) + .then(function () { _.each(drafts, function (draft, i) { - var age = (new Date()) - new Date(draft.value.datetime); - var isOld = (age / 2678400000) > 1; // ~31days + var age = new Date() - new Date(draft.value.datetime); + var isOld = age / 2678400000 > 1; // ~31days // Delete this draft is not in the most recent 100 or // if older than 31 days @@ -1377,64 +1551,68 @@

Source: src/js/views/metadata/EML211EditorView.js

}); }); }); - } - catch (ex) { - console.log("Failed to clear old drafts: ", ex); - } - }, - - /** - * Show the AccessPolicy view in a modal dialog - * - * This method calls the superclass method, feeding it the identifier - * associated with the row in the package table that was clicked. The - * reason for this is so the AccessPolicyView can be used for single - * objects (like in the Portal editor) or an entire Collection of - * objects, like in the EML editor: The superclass impelements the - * generic behavior and the subclass tweaks it. - * - * @param {EventHandler} e: The click event - */ - showAccessPolicyModal: function(e) { - var id = null; - - try { - id = $(e.target).parents("tr").data("id"); - } catch (e) { - console.log("Error determining the identifier to show an AccessPolicyView for:", e); - } + } catch (ex) { + console.log("Failed to clear old drafts: ", ex); + } + }, + + /** + * Show the AccessPolicy view in a modal dialog + * + * This method calls the superclass method, feeding it the identifier + * associated with the row in the package table that was clicked. The + * reason for this is so the AccessPolicyView can be used for single + * objects (like in the Portal editor) or an entire Collection of + * objects, like in the EML editor: The superclass impelements the + * generic behavior and the subclass tweaks it. + * + * @param {EventHandler} e: The click event + */ + showAccessPolicyModal: function (e) { + var id = null; + + try { + id = $(e.target).parents("tr").data("id"); + } catch (e) { + console.log( + "Error determining the identifier to show an AccessPolicyView for:", + e, + ); + } - var model = MetacatUI.rootDataPackage.find(function(model) { - return model.get("id") === id; + var model = MetacatUI.rootDataPackage.find(function (model) { + return model.get("id") === id; + }); + + EditorView.prototype.showAccessPolicyModal.call(this, e, model); + }, + + /** + * Gets the EML required fields, as configured in the {@link AppConfig#emlEditorRequiredFields}, and adds + * possible other special fields that may be configured elsewhere. (e.g. the {@link AppConfig#customEMLMethods}) + * @extends EditorView.getRequiredFields + */ + getRequiredFields: function () { + let requiredFields = _.clone( + MetacatUI.appModel.get("emlEditorRequiredFields"), + ); + + //Add required fields for Custom Methods, which are configured in a different property of the AppConfig + let customMethodOptions = MetacatUI.appModel.get("customEMLMethods"); + if (customMethodOptions) { + customMethodOptions.forEach((options) => { + if (options.required && !requiredFields[options.id]) { + requiredFields[options.id] = true; + } }); - - EditorView.prototype.showAccessPolicyModal.call(this, e, model); - }, - - /** - * Gets the EML required fields, as configured in the {@link AppConfig#emlEditorRequiredFields}, and adds - * possible other special fields that may be configured elsewhere. (e.g. the {@link AppConfig#customEMLMethods}) - * @extends EditorView.getRequiredFields - */ - getRequiredFields: function(){ - let requiredFields = _.clone(MetacatUI.appModel.get("emlEditorRequiredFields")); - - //Add required fields for Custom Methods, which are configured in a different property of the AppConfig - let customMethodOptions = MetacatUI.appModel.get("customEMLMethods"); - if(customMethodOptions){ - customMethodOptions.forEach(options => { - if( options.required && !requiredFields[options.id] ){ - requiredFields[options.id] = true; - } - }) - } - - return requiredFields; - } - }); - return EML211EditorView; - }); + + return requiredFields; + }, + }, + ); + return EML211EditorView; +});
diff --git a/docs/docs/src_js_views_metadata_EML211MissingValueCodeView.js.html b/docs/docs/src_js_views_metadata_EML211MissingValueCodeView.js.html index 541d501f5..964dddc75 100644 --- a/docs/docs/src_js_views_metadata_EML211MissingValueCodeView.js.html +++ b/docs/docs/src_js_views_metadata_EML211MissingValueCodeView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/metadata/EML211MissingValueCodeView.
-
/* global define */
-define([
+            
define([
   "jquery",
   "backbone",
   "models/metadata/eml211/EMLMissingValueCode",
@@ -142,7 +141,7 @@ 

Source: src/js/views/metadata/EML211MissingValueCodeView. try { if (!this.model) { console.warn( - "An EMLMissingValueCodeView model is required to render this view." + "An EMLMissingValueCodeView model is required to render this view.", ); return this; } @@ -233,7 +232,7 @@

Source: src/js/views/metadata/EML211MissingValueCodeView. // The model must be part of a collection to remove it from anything if (!this.model.collection) { console.warn( - "The model must be part of a collection to render a remove button." + "The model must be part of a collection to render a remove button.", ); return; } @@ -306,7 +305,7 @@

Source: src/js/views/metadata/EML211MissingValueCodeView. // Remove the view from the DOM this.remove(); }, - } + }, ); return EMLMissingValueCodeView; diff --git a/docs/docs/src_js_views_metadata_EML211MissingValueCodesView.js.html b/docs/docs/src_js_views_metadata_EML211MissingValueCodesView.js.html index e55e3ff5c..13012eca0 100644 --- a/docs/docs/src_js_views_metadata_EML211MissingValueCodesView.js.html +++ b/docs/docs/src_js_views_metadata_EML211MissingValueCodesView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/metadata/EML211MissingValueCodesView
-
/* global define */
-define([
+            
define([
   "backbone",
   "models/metadata/eml211/EMLMissingValueCode",
   "collections/metadata/eml/EMLMissingValueCodes",
@@ -54,7 +53,7 @@ 

Source: src/js/views/metadata/EML211MissingValueCodesView Backbone, EMLMissingValueCode, EMLMissingValueCodes, - EML211MissingValueCodeView + EML211MissingValueCodeView, ) { /** * @class EMLMissingValueCodesView @@ -141,7 +140,7 @@

Source: src/js/views/metadata/EML211MissingValueCodesView if (!this.collection) { console.warn( `The EMLMissingValueCodesView requires a MissingValueCodes collection` + - ` to render.` + ` to render.`, ); return; } @@ -240,7 +239,7 @@

Source: src/js/views/metadata/EML211MissingValueCodesView * @returns {EML211MissingValueCodeView} The row view that was created */ addRow: function (model) { - if (!model instanceof EMLMissingValueCode) return; + if ((!model) instanceof EMLMissingValueCode) return; // New rows will not have a remove button until the user starts typing const isNew = this.modelIsNew(model); @@ -270,14 +269,14 @@

Source: src/js/views/metadata/EML211MissingValueCodesView * @returns {EML211MissingValueCodeView} The row view that was removed */ removeRow: function (model) { - if (!model instanceof EMLMissingValueCode) return; + if ((!model) instanceof EMLMissingValueCode) return; const rowView = this.el.querySelector(`[data-model-id="${model.cid}"]`); if (rowView) { rowView.remove(); return rowView; } }, - } + }, ); return EMLMissingValueCodesView; diff --git a/docs/docs/src_js_views_metadata_EML211View.js.html b/docs/docs/src_js_views_metadata_EML211View.js.html index 686605c74..2f7b8b441 100644 --- a/docs/docs/src_js_views_metadata_EML211View.js.html +++ b/docs/docs/src_js_views_metadata_EML211View.js.html @@ -44,522 +44,605 @@

Source: src/js/views/metadata/EML211View.js

-
define(['underscore', 'jquery', 'backbone',
-  'views/metadata/ScienceMetadataView',
-  'views/metadata/EMLGeoCoverageView',
-  'views/metadata/EMLPartyView',
-  'views/metadata/EMLMethodsView',
-  'views/metadata/EMLTempCoverageView',
-  'models/metadata/eml211/EML211',
-  'models/metadata/eml211/EMLGeoCoverage',
-  'models/metadata/eml211/EMLKeywordSet',
-  'models/metadata/eml211/EMLParty',
-  'models/metadata/eml211/EMLProject',
-  'models/metadata/eml211/EMLText',
-  'models/metadata/eml211/EMLTaxonCoverage',
-  'models/metadata/eml211/EMLTemporalCoverage',
-  'models/metadata/eml211/EMLMethods',
-  'text!templates/metadata/eml.html',
-  'text!templates/metadata/eml-people.html',
-  'text!templates/metadata/EMLPartyCopyMenu.html',
-  'text!templates/metadata/metadataOverview.html',
-  'text!templates/metadata/dates.html',
-  'text!templates/metadata/locationsSection.html',
-  'text!templates/metadata/taxonomicCoverage.html',
-  'text!templates/metadata/taxonomicClassificationTable.html',
-  'text!templates/metadata/taxonomicClassificationRow.html',
-  'text!templates/metadata/data-sensitivity.html'],
-  function (_, $, Backbone,
-    ScienceMetadataView, EMLGeoCoverageView, EMLPartyView, EMLMethodsView, EMLTempCoverageView,
-    EML, EMLGeoCoverage, EMLKeywordSet, EMLParty, EMLProject, EMLText, EMLTaxonCoverage,
-    EMLTemporalCoverage, EMLMethods, Template, PeopleTemplate, EMLPartyCopyMenuTemplate, OverviewTemplate,
-    DatesTemplate, LocationsTemplate, TaxonomicCoverageTemplate, TaxonomicClassificationTable, TaxonomicClassificationRow,
-    DataSensitivityTemplate) {
-
-    /**
-    * @class EMLView
-    * @classdesc An EMLView renders an editable view of an EML 2.1.1 document
-    * @classcategory Views/Metadata
-    * @extends ScienceMetadataView
-    */
-    var EMLView = ScienceMetadataView.extend(
-      /** @lends EMLView */{
-
-        type: "EML211",
-
-        el: '#metadata-container',
-
-        events: {
-          "change .text": "updateText",
-
-          "change .basic-text": "updateBasicText",
-          "keyup  .basic-text.new": "addBasicText",
-          "mouseover .basic-text-row .remove": "previewTextRemove",
-          "mouseout .basic-text-row .remove": "previewTextRemove",
-
-          "change .pubDate input": "updatePubDate",
-          "focusout .pubDate input": "showPubDateValidation",
-
-          "keyup .eml-geocoverage.new": "updateLocations",
-
-          "change .taxonomic-coverage": "updateTaxonCoverage",
-          "keyup .taxonomic-coverage .new input": "addNewTaxon",
-          "keyup .taxonomic-coverage .new select": "addNewTaxon",
-          "focusout .taxonomic-coverage tr": "showTaxonValidation",
-          "click .taxonomic-coverage-row .remove": "removeTaxonRank",
-          "mouseover .taxonomic-coverage .remove": "previewTaxonRemove",
-          "mouseout .taxonomic-coverage .remove": "previewTaxonRemove",
-
-          "change .keywords": "updateKeywords",
-          "keyup .keyword-row.new input": "addNewKeyword",
-          "mouseover .keyword-row .remove": "previewKeywordRemove",
-          "mouseout .keyword-row .remove": "previewKeywordRemove",
-
-          "change .usage": "updateRadioButtons",
-
-          "change .funding": "updateFunding",
-          "keyup .funding.new": "addFunding",
-          "mouseover .funding-row .remove": "previewFundingRemove",
-          "mouseout .funding-row .remove": "previewFundingRemove",
-          "keyup .funding.error": "handleFundingTyping",
-
-          "click .side-nav-item": "switchSection",
-
-          "keyup .eml-party.new": "handlePersonTyping",
-          "change #new-party-menu": "chooseNewPersonType",
-          "click .eml-party .copy": "showCopyPersonMenu",
-          "click #copy-party-save": "copyPerson",
-          "click .eml-party .remove": "removePerson",
-          "click .eml-party .move-up": "movePersonUp",
-          "click .eml-party .move-down": "movePersonDown",
-
-          "click input.annotation" : "addAnnotation",
-
-          "click  .remove": "handleRemove"
-        },
-
-        /* A list of the subviews */
-        subviews: [],
-
-        /* The active section in the view - can only be the section name (e.g. overview, people)
-         * The active section is highlighted in the table of contents and is scrolled to when the page loads
-         */
-        activeSection: "overview",
-
-        /* The visible section in the view - can either be the section name (e.g. overview, people) or "all"
-         * The visible section is the ONLY section that is displayed. If set to all, all sections are displayed.
-         */
-        visibleSection: "overview",
-
-        /* Templates */
-        template: _.template(Template),
-        overviewTemplate: _.template(OverviewTemplate),
-        dataSensitivityTemplate: _.template(DataSensitivityTemplate),
-        datesTemplate: _.template(DatesTemplate),
-        locationsTemplate: _.template(LocationsTemplate),
-        taxonomicCoverageTemplate: _.template(TaxonomicCoverageTemplate),
-        taxonomicClassificationTableTemplate: _.template(TaxonomicClassificationTable),
-        taxonomicClassificationRowTemplate: _.template(TaxonomicClassificationRow),
-        copyPersonMenuTemplate: _.template(EMLPartyCopyMenuTemplate),
-        peopleTemplate: _.template(PeopleTemplate),
-
-        /**
-        * jQuery selector for the element that contains the Data Sensitivity section.
-        * @type {string}
-        */
-        dataSensitivityContainerSelector: "#data-sensitivity-container",
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "views/metadata/ScienceMetadataView",
+  "views/metadata/EMLGeoCoverageView",
+  "views/metadata/EMLPartyView",
+  "views/metadata/EMLMethodsView",
+  "views/metadata/EMLTempCoverageView",
+  "models/metadata/eml211/EML211",
+  "models/metadata/eml211/EMLGeoCoverage",
+  "models/metadata/eml211/EMLKeywordSet",
+  "models/metadata/eml211/EMLParty",
+  "models/metadata/eml211/EMLProject",
+  "models/metadata/eml211/EMLText",
+  "models/metadata/eml211/EMLTaxonCoverage",
+  "models/metadata/eml211/EMLTemporalCoverage",
+  "models/metadata/eml211/EMLMethods",
+  "text!templates/metadata/eml.html",
+  "text!templates/metadata/eml-people.html",
+  "text!templates/metadata/EMLPartyCopyMenu.html",
+  "text!templates/metadata/metadataOverview.html",
+  "text!templates/metadata/dates.html",
+  "text!templates/metadata/locationsSection.html",
+  "text!templates/metadata/taxonomicCoverage.html",
+  "text!templates/metadata/taxonomicClassificationTable.html",
+  "text!templates/metadata/taxonomicClassificationRow.html",
+  "text!templates/metadata/data-sensitivity.html",
+], function (
+  _,
+  $,
+  Backbone,
+  ScienceMetadataView,
+  EMLGeoCoverageView,
+  EMLPartyView,
+  EMLMethodsView,
+  EMLTempCoverageView,
+  EML,
+  EMLGeoCoverage,
+  EMLKeywordSet,
+  EMLParty,
+  EMLProject,
+  EMLText,
+  EMLTaxonCoverage,
+  EMLTemporalCoverage,
+  EMLMethods,
+  Template,
+  PeopleTemplate,
+  EMLPartyCopyMenuTemplate,
+  OverviewTemplate,
+  DatesTemplate,
+  LocationsTemplate,
+  TaxonomicCoverageTemplate,
+  TaxonomicClassificationTable,
+  TaxonomicClassificationRow,
+  DataSensitivityTemplate,
+) {
+  /**
+   * @class EMLView
+   * @classdesc An EMLView renders an editable view of an EML 2.1.1 document
+   * @classcategory Views/Metadata
+   * @extends ScienceMetadataView
+   */
+  var EMLView = ScienceMetadataView.extend(
+    /** @lends EMLView */ {
+      type: "EML211",
+
+      el: "#metadata-container",
+
+      events: {
+        "change .text": "updateText",
+
+        "change .basic-text": "updateBasicText",
+        "keyup  .basic-text.new": "addBasicText",
+        "mouseover .basic-text-row .remove": "previewTextRemove",
+        "mouseout .basic-text-row .remove": "previewTextRemove",
+
+        "change .pubDate input": "updatePubDate",
+        "focusout .pubDate input": "showPubDateValidation",
+
+        "keyup .eml-geocoverage.new": "updateLocations",
+
+        "change .taxonomic-coverage": "updateTaxonCoverage",
+        "keyup .taxonomic-coverage .new input": "addNewTaxon",
+        "keyup .taxonomic-coverage .new select": "addNewTaxon",
+        "focusout .taxonomic-coverage tr": "showTaxonValidation",
+        "click .taxonomic-coverage-row .remove": "removeTaxonRank",
+        "mouseover .taxonomic-coverage .remove": "previewTaxonRemove",
+        "mouseout .taxonomic-coverage .remove": "previewTaxonRemove",
+
+        "change .keywords": "updateKeywords",
+        "keyup .keyword-row.new input": "addNewKeyword",
+        "mouseover .keyword-row .remove": "previewKeywordRemove",
+        "mouseout .keyword-row .remove": "previewKeywordRemove",
+
+        "change .usage": "updateRadioButtons",
+
+        "change .funding": "updateFunding",
+        "keyup .funding.new": "addFunding",
+        "mouseover .funding-row .remove": "previewFundingRemove",
+        "mouseout .funding-row .remove": "previewFundingRemove",
+        "keyup .funding.error": "handleFundingTyping",
+
+        "click .side-nav-item": "switchSection",
+
+        "keyup .eml-party.new": "handlePersonTyping",
+        "change #new-party-menu": "chooseNewPersonType",
+        "click .eml-party .copy": "showCopyPersonMenu",
+        "click #copy-party-save": "copyPerson",
+        "click .eml-party .remove": "removePerson",
+        "click .eml-party .move-up": "movePersonUp",
+        "click .eml-party .move-down": "movePersonDown",
+
+        "click input.annotation": "addAnnotation",
+
+        "click  .remove": "handleRemove",
+      },
 
-        /**
-         * An array of literal objects to describe each type of EML Party. This property has been moved to 
-         * {@link EMLParty#partyTypes} as of 2.21.0 and will soon be deprecated.
-         * @type {object[]}
-         * @deprecated
-         * @since 2.15.0
-         */
-        partyTypes: EMLParty.prototype.partyTypes,
+      /* A list of the subviews */
+      subviews: [],
+
+      /* The active section in the view - can only be the section name (e.g. overview, people)
+       * The active section is highlighted in the table of contents and is scrolled to when the page loads
+       */
+      activeSection: "overview",
 
-        initialize: function (options) {
+      /* The visible section in the view - can either be the section name (e.g. overview, people) or "all"
+       * The visible section is the ONLY section that is displayed. If set to all, all sections are displayed.
+       */
+      visibleSection: "overview",
+
+      /* Templates */
+      template: _.template(Template),
+      overviewTemplate: _.template(OverviewTemplate),
+      dataSensitivityTemplate: _.template(DataSensitivityTemplate),
+      datesTemplate: _.template(DatesTemplate),
+      locationsTemplate: _.template(LocationsTemplate),
+      taxonomicCoverageTemplate: _.template(TaxonomicCoverageTemplate),
+      taxonomicClassificationTableTemplate: _.template(
+        TaxonomicClassificationTable,
+      ),
+      taxonomicClassificationRowTemplate: _.template(
+        TaxonomicClassificationRow,
+      ),
+      copyPersonMenuTemplate: _.template(EMLPartyCopyMenuTemplate),
+      peopleTemplate: _.template(PeopleTemplate),
 
-          //Set up all the options
-          if (typeof options == "undefined") var options = {};
+      /**
+       * jQuery selector for the element that contains the Data Sensitivity section.
+       * @type {string}
+       */
+      dataSensitivityContainerSelector: "#data-sensitivity-container",
 
-          //The EML Model and ID
-          this.model = options.model || new EML();
-          if (!this.model.get("id") && options.id) this.model.set("id", options.id);
+      /**
+       * An array of literal objects to describe each type of EML Party. This property has been moved to
+       * {@link EMLParty#partyTypes} as of 2.21.0 and will soon be deprecated.
+       * @type {object[]}
+       * @deprecated
+       * @since 2.15.0
+       */
+      partyTypes: EMLParty.prototype.partyTypes,
 
-          //Get the current mode
-          this.edit = options.edit || false;
+      initialize: function (options) {
+        //Set up all the options
+        if (typeof options == "undefined") var options = {};
 
-          return this;
-        },
+        //The EML Model and ID
+        this.model = options.model || new EML();
+        if (!this.model.get("id") && options.id)
+          this.model.set("id", options.id);
 
-        /* Render the view */
-        render: function () {
-          MetacatUI.appModel.set('headerType', 'default');
+        //Get the current mode
+        this.edit = options.edit || false;
 
-          //Render the basic structure of the page and table of contents
-          this.$el.html(this.template({
-            activeSection: this.activeSection,
-            visibleSection: this.visibleSection
-          }));
-          this.$container = this.$(".metadata-container");
+        return this;
+      },
 
-          //Render all the EML sections when the model is synced
-          this.renderAllSections();
-          if (!this.model.get("synced"))
-            this.listenToOnce(this.model, "sync", this.renderAllSections);
+      /* Render the view */
+      render: function () {
+        MetacatUI.appModel.set("headerType", "default");
 
-          //Listen to updates on the data package collections
-          _.each(this.model.get("collections"), function (dataPackage) {
+        //Render the basic structure of the page and table of contents
+        this.$el.html(
+          this.template({
+            activeSection: this.activeSection,
+            visibleSection: this.visibleSection,
+          }),
+        );
+        this.$container = this.$(".metadata-container");
+
+        //Render all the EML sections when the model is synced
+        this.renderAllSections();
+        if (!this.model.get("synced"))
+          this.listenToOnce(this.model, "sync", this.renderAllSections);
+
+        //Listen to updates on the data package collections
+        _.each(
+          this.model.get("collections"),
+          function (dataPackage) {
             if (dataPackage.type != "DataPackage") return;
 
             // When the data package has been saved, render the EML again.
             // This is needed because the EML model validate & serialize functions may
             // automatically make changes, such as adding a contact and creator
             // if none is supplied by the user.
-            this.listenTo(dataPackage.packageModel, "successSaving", this.renderAllSections);
-          }, this);
-
-          return this;
-        },
-
-        renderAllSections: function () {
-          this.renderOverview();
-          this.renderPeople();
-          this.renderDates();
-          this.renderLocations();
-          this.renderTaxa();
-          this.renderMethods();
-          this.renderProject();
-          this.renderSharing();
+            this.listenTo(
+              dataPackage.packageModel,
+              "successSaving",
+              this.renderAllSections,
+            );
+          },
+          this,
+        );
 
-          //Scroll to the active section
-          if (this.activeSection != "overview") {
-            MetacatUI.appView.scrollTo(this.$(".section." + this.activeSection));
-          }
+        return this;
+      },
 
-          //When scrolling through the metadata, highlight the side navigation
-          var view = this;
-          $(document).scroll(function () {
-            view.highlightTOC.call(view);
-          });
+      renderAllSections: function () {
+        this.renderOverview();
+        this.renderPeople();
+        this.renderDates();
+        this.renderLocations();
+        this.renderTaxa();
+        this.renderMethods();
+        this.renderProject();
+        this.renderSharing();
+
+        //Scroll to the active section
+        if (this.activeSection != "overview") {
+          MetacatUI.appView.scrollTo(this.$(".section." + this.activeSection));
+        }
 
-        },
+        //When scrolling through the metadata, highlight the side navigation
+        var view = this;
+        $(document).scroll(function () {
+          view.highlightTOC.call(view);
+        });
+      },
 
-        /*
-         * Renders the Overview section of the page
-         */
-        renderOverview: function () {
-          //Get the overall view mode
-          var edit = this.edit;
+      /*
+       * Renders the Overview section of the page
+       */
+      renderOverview: function () {
+        //Get the overall view mode
+        var edit = this.edit;
 
-          var view = this;
+        var view = this;
 
-          //Append the empty layout
-          var overviewEl = this.$container.find(".overview");
-          $(overviewEl).html(this.overviewTemplate());
+        //Append the empty layout
+        var overviewEl = this.$container.find(".overview");
+        $(overviewEl).html(this.overviewTemplate());
 
-          //Title
-          this.renderTitle();
-          this.listenTo(this.model, "change:title", this.renderTitle);
+        //Title
+        this.renderTitle();
+        this.listenTo(this.model, "change:title", this.renderTitle);
 
-          //Data Sensitivity
-          this.renderDataSensitivity();
+        //Data Sensitivity
+        this.renderDataSensitivity();
 
-          //Abstract
-          _.each(this.model.get("abstract"), function (abs) {
+        //Abstract
+        _.each(
+          this.model.get("abstract"),
+          function (abs) {
             var abstractEl = this.createEMLText(abs, edit, "abstract");
 
             //Add the abstract element to the view
             $(overviewEl).find(".abstract").append(abstractEl);
-          }, this);
+          },
+          this,
+        );
 
-          if (!this.model.get("abstract").length) {
-            var abstractEl = this.createEMLText(null, edit, "abstract");
-
-            //Add the abstract element to the view
-            $(overviewEl).find(".abstract").append(abstractEl);
-          }
+        if (!this.model.get("abstract").length) {
+          var abstractEl = this.createEMLText(null, edit, "abstract");
 
-          //Keywords
-          //Iterate over each keyword and add a text input for the keyword value and a dropdown menu for the thesaurus
-          _.each(this.model.get("keywordSets"), function (keywordSetModel) {
-            _.each(keywordSetModel.get("keywords"), function (keyword) {
-              this.addKeyword(keyword, keywordSetModel.get("thesaurus"));
-            }, this);
-          }, this);
-
-          //Add a new keyword row
-          this.addKeyword();
-
-          //Alternate Ids
-          var altIdsEls = this.createBasicTextFields("alternateIdentifier", "Add a new alternate identifier");
-          $(overviewEl).find(".altids").append(altIdsEls);
-
-          //Usage
-          //Find the model value that matches a radio button and check it
-          // Note the replace() call removing newlines and replacing them with a single space
-          // character. This is a temporary hack to fix https://github.com/NCEAS/metacatui/issues/128
-          if (this.model.get("intellectualRights"))
-            this.$(".checkbox .usage[value='" + this.model.get("intellectualRights").replace(/\r?\n|\r/g, ' ') + "']").prop("checked", true);
+          //Add the abstract element to the view
+          $(overviewEl).find(".abstract").append(abstractEl);
+        }
 
-          //Funding
-          this.renderFunding();
+        //Keywords
+        //Iterate over each keyword and add a text input for the keyword value and a dropdown menu for the thesaurus
+        _.each(
+          this.model.get("keywordSets"),
+          function (keywordSetModel) {
+            _.each(
+              keywordSetModel.get("keywords"),
+              function (keyword) {
+                this.addKeyword(keyword, keywordSetModel.get("thesaurus"));
+              },
+              this,
+            );
+          },
+          this,
+        );
+
+        //Add a new keyword row
+        this.addKeyword();
+
+        //Alternate Ids
+        var altIdsEls = this.createBasicTextFields(
+          "alternateIdentifier",
+          "Add a new alternate identifier",
+        );
+        $(overviewEl).find(".altids").append(altIdsEls);
+
+        //Usage
+        //Find the model value that matches a radio button and check it
+        // Note the replace() call removing newlines and replacing them with a single space
+        // character. This is a temporary hack to fix https://github.com/NCEAS/metacatui/issues/128
+        if (this.model.get("intellectualRights"))
+          this.$(
+            ".checkbox .usage[value='" +
+              this.model.get("intellectualRights").replace(/\r?\n|\r/g, " ") +
+              "']",
+          ).prop("checked", true);
+
+        //Funding
+        this.renderFunding();
+
+        // pubDate
+        // BDM: This isn't a createBasicText call because that helper
+        // assumes multiple values for the category
+        // TODO: Consider a re-factor of createBasicText
+        var pubDateInput = $(overviewEl)
+          .find("input.pubDate")
+          .val(this.model.get("pubDate"));
+
+        //Initialize all the tooltips
+        this.$(".tooltip-this").tooltip();
+      },
 
-          // pubDate
-          // BDM: This isn't a createBasicText call because that helper
-          // assumes multiple values for the category
-          // TODO: Consider a re-factor of createBasicText
-          var pubDateInput = $(overviewEl).find("input.pubDate").val(this.model.get("pubDate"));
+      renderTitle: function () {
+        var titleEl = this.createBasicTextFields(
+          "title",
+          "Example: Greater Yellowstone Rivers from 1:126,700 U.S. Forest Service Visitor Maps (1961-1983)",
+          false,
+        );
+        this.$container
+          .find(".overview")
+          .find(".title-container")
+          .html(titleEl);
+      },
 
-          //Initialize all the tooltips
-          this.$(".tooltip-this").tooltip();
+      /**
+       * Renders the Data Sensitivity section of the Editor using the data-sensitivity.html template.
+       * @fires EML211View#editorInputsAdded
+       */
+      renderDataSensitivity: function () {
+        try {
+          //If Data Sensitivity questions are disabled in the AppConfig, exit before rendering
+          if (!MetacatUI.appModel.get("enableDataSensitivityInEditor")) {
+            return;
+          }
 
-        },
+          var container = this.$(this.dataSensitivityContainerSelector),
+            view = this;
 
-        renderTitle: function () {
-          var titleEl = this.createBasicTextFields("title", "Example: Greater Yellowstone Rivers from 1:126,700 U.S. Forest Service Visitor Maps (1961-1983)", false);
-          this.$container.find(".overview").find(".title-container").html(titleEl);
-        },
+          if (!container.length) {
+            container = $(`<div id="data-sensitivity-container"></div>`);
+            this.$(".section.overview").append(container);
+          }
 
-        /**
-        * Renders the Data Sensitivity section of the Editor using the data-sensitivity.html template.
-        * @fires EML211View#editorInputsAdded
-        */
-        renderDataSensitivity: function(){
-          try{
+          require([
+            "text!../img/icons/datatags/check-tag.svg",
+            "text!../img/icons/datatags/alert-tag.svg",
+          ], function (checkTagIcon, alertTagIcon) {
+            container.html(
+              view.dataSensitivityTemplate({
+                checkTagIcon: checkTagIcon,
+                alertTagIcon: alertTagIcon,
+              }),
+            );
 
-            //If Data Sensitivity questions are disabled in the AppConfig, exit before rendering
-            if( !MetacatUI.appModel.get("enableDataSensitivityInEditor") ){
-              return;
-            }
+            //Initialize all the tooltips
+            view.$(".tooltip-this").tooltip();
 
-            var container = this.$(this.dataSensitivityContainerSelector),
-                view = this;
+            //Check the radio button that is already selected, per the EML
+            let annotations = view.model.getDataSensitivity();
 
-            if(!container.length){
-              container = $(`<div id="data-sensitivity-container"></div>`);
-              this.$(".section.overview").append(container);
+            if (
+              annotations &&
+              annotations.length &&
+              typeof annotations[0].get == "function"
+            ) {
+              let annotationValue = annotations[0].get("valueURI");
+              container
+                .find("[value='" + annotationValue + "']")
+                .prop("checked", true);
             }
 
-            require(['text!../img/icons/datatags/check-tag.svg', 'text!../img/icons/datatags/alert-tag.svg'], function(checkTagIcon, alertTagIcon){
-              container.html(view.dataSensitivityTemplate({
-                checkTagIcon: checkTagIcon,
-                alertTagIcon: alertTagIcon
-              }));
+            //Trigger the editorInputsAdded event which will let other parts of the app,
+            // such as the EditorView, know that new inputs are on the page
+            view.trigger("editorInputsAdded");
+          });
+        } catch (e) {
+          console.error("Could not render the Data Sensitivity section: ", e);
+        }
+      },
 
-              //Initialize all the tooltips
-              view.$(".tooltip-this").tooltip();
+      /*
+       * Renders the People section of the page
+       */
+      renderPeople: function () {
+        var view = this,
+          model = view.model;
 
-              //Check the radio button that is already selected, per the EML
-              let annotations = view.model.getDataSensitivity();
+        this.peopleSection = this.$(".section[data-section='people']");
 
-              if(annotations && annotations.length && typeof annotations[0].get == "function"){
-                let annotationValue = annotations[0].get("valueURI");
-                container.find("[value='" + annotationValue + "']").prop("checked", true);
-              }
+        // Empty the people section in case we are re-rendering people
+        // Insert the people template
+        this.peopleSection.html(this.peopleTemplate());
 
+        // Create a dropdown menu for adding new person types
+        this.renderPeopleDropdown();
 
-              //Trigger the editorInputsAdded event which will let other parts of the app,
-              // such as the EditorView, know that new inputs are on the page
-              view.trigger("editorInputsAdded");
+        EMLParty.prototype.partyTypes.forEach(function (partyType) {
+          // Make sure that there are no container elements saved
+          // in the partyType array, since we may need to re-create the
+          // containers the hold the rendered EMLParty information.
+          partyType.containerEl = null;
 
+          // Any party type that is listed as a role in EMLParty "roleOptions" is saved
+          // in the EML model as an associated party. The isAssociatedParty property
+          // is used for other parts of the EML211View.
+          if (
+            new EMLParty().get("roleOptions").includes(partyType.dataCategory)
+          ) {
+            partyType.isAssociatedParty = true;
+          } else {
+            partyType.isAssociatedParty = false;
+          }
+
+          // Get the array of party members for the given partyType from the EML model
+          var parties = this.model.getPartiesByType(partyType.dataCategory);
+
+          // If no parties exist for the given party type, but one is required,
+          // (e.g. for contact and creator), then create one from the user's information.
+          if (!parties?.length && partyType.createFromUser) {
+            var newParty = new EMLParty({
+              type: partyType.isAssociatedParty
+                ? "associatedParty"
+                : partyType.dataCategory,
+              roles: partyType.isAssociatedParty
+                ? [partyType.dataCategory]
+                : [],
+              parentModel: model,
             });
-          }
-          catch(e){
-            console.error("Could not render the Data Sensitivity section: ", e);
+            newParty.createFromUser();
+            model.addParty(newParty);
+            parties = [newParty];
           }
 
-        },
+          // Render each party of this type
+          if (parties.length) {
+            parties.forEach(function (party) {
+              this.renderPerson(party, partyType.dataCategory);
+            }, this);
+          }
+          //If there are no parties of this type but they are required, then render a new empty person for this type
+          else if (
+            MetacatUI.appModel.get("emlEditorRequiredFields")[
+              partyType.dataCategory
+            ]
+          ) {
+            this.renderPerson(null, partyType.dataCategory);
+          }
+        }, this);
+
+        // Render a new blank party form at the very bottom of the people section.
+        // This allows the user to start entering details for a person before they've
+        // selected the party type.
+        this.renderPerson(null, "new");
+
+        // Initialize the tooltips
+        this.$("input.tooltip-this").tooltip({
+          placement: "top",
+          title: function () {
+            return $(this).attr("data-title") || $(this).attr("placeholder");
+          },
+          delay: 1000,
+        });
+      },
 
-        /*
-         * Renders the People section of the page
-         */
-        renderPeople: function () {
+      /**
+       * Creates and renders the dropdown at the bottom of the people section
+       * that allows the user to create a new party type category. The dropdown
+       * menu is saved to the view as view.partyMenu.
+       * @since 2.15.0
+       */
+      renderPeopleDropdown: function () {
+        try {
+          var helpText =
+              "Optionally add other contributors, collaborators, and maintainers of this dataset.",
+            placeholderText = "Choose new person or organization role ...";
 
-          var view = this,
-            model = view.model;
+          this.partyMenu = $(document.createElement("select"))
+            .attr("id", "new-party-menu")
+            .addClass("header-dropdown");
 
-          this.peopleSection = this.$(".section[data-section='people']");
+          //Add the first option to the menu, which works as a label
+          this.partyMenu.append(
+            $(document.createElement("option")).text(placeholderText),
+          );
 
-          // Empty the people section in case we are re-rendering people
-          // Insert the people template
-          this.peopleSection.html(this.peopleTemplate());
+          //Add some help text for the menu
+          this.partyMenu.attr("title", helpText);
 
-          // Create a dropdown menu for adding new person types
-          this.renderPeopleDropdown();
+          //Add a container element for the new party
+          this.newPartyContainer = $(document.createElement("div"))
+            .attr("data-attribute", "new")
+            .addClass("row-striped");
 
+          //For each party type, add it to the menu as an option
           EMLParty.prototype.partyTypes.forEach(function (partyType) {
-
-            // Make sure that there are no container elements saved
-            // in the partyType array, since we may need to re-create the
-            // containers the hold the rendered EMLParty information.
-            partyType.containerEl = null;
-
-            // Any party type that is listed as a role in EMLParty "roleOptions" is saved
-            // in the EML model as an associated party. The isAssociatedParty property
-            // is used for other parts of the EML211View.
-            if (new EMLParty().get("roleOptions").includes(partyType.dataCategory)) {
-              partyType.isAssociatedParty = true;
-            } else {
-              partyType.isAssociatedParty = false;
-            }
-
-            // Get the array of party members for the given partyType from the EML model
-            var parties = this.model.getPartiesByType(partyType.dataCategory);
-
-            // If no parties exist for the given party type, but one is required,
-            // (e.g. for contact and creator), then create one from the user's information.
-            if (!parties?.length && partyType.createFromUser){
-              
-              var newParty = new EMLParty({
-                type: partyType.isAssociatedParty ? "associatedParty" : partyType.dataCategory,
-                roles: partyType.isAssociatedParty ? [partyType.dataCategory] : [],
-                parentModel: model
-              });
-              newParty.createFromUser();
-              model.addParty(newParty);
-              parties = [newParty]
-
-            }
-
-            // Render each party of this type
-            if (parties.length) {
-              parties.forEach(function (party) {
-                this.renderPerson(party, partyType.dataCategory);
-              }, this);
-            }
-            //If there are no parties of this type but they are required, then render a new empty person for this type
-            else if(MetacatUI.appModel.get("emlEditorRequiredFields")[partyType.dataCategory]){
-              this.renderPerson(null, partyType.dataCategory);
-            }
-
+            $(this.partyMenu).append(
+              $(document.createElement("option"))
+                .val(partyType.dataCategory)
+                .text(partyType.label),
+            );
           }, this);
 
-          // Render a new blank party form at the very bottom of the people section.
-          // This allows the user to start entering details for a person before they've
-          // selected the party type.
-          this.renderPerson(null, "new");
+          // Add the menu and new party element to the page
+          this.peopleSection.append(this.partyMenu, this.newPartyContainer);
+        } catch (error) {
+          console.log(
+            "Error creating the menu for adding new party categories, error message: " +
+              error,
+          );
+        }
+      },
 
-          // Initialize the tooltips
-          this.$("input.tooltip-this").tooltip({
-            placement: "top",
-            title: function () {
-              return $(this).attr("data-title") || $(this).attr("placeholder")
-            },
-            delay: 1000
+      /**
+       * Render the information provided for a given EML party in the party section.
+       *
+       * @param  {EMLParty} emlParty - the EMLParty model to render. If set to null, a new EML party will be created for the given party type.
+       * @param  {string} partyType - The party type for which to render a new EML party. E.g. "creator", "coPrincipalInvestigator", etc.
+       */
+      renderPerson: function (emlParty, partyType) {
+        // Whether or not this is a new emlParty model
+        var isNew = false;
+
+        //If no model is given, create a new model
+        if (!emlParty) {
+          var emlParty = new EMLParty({
+            parentModel: this.model,
           });
 
-        },
-
-        /**
-         * Creates and renders the dropdown at the bottom of the people section
-         * that allows the user to create a new party type category. The dropdown
-         * menu is saved to the view as view.partyMenu.
-         * @since 2.15.0
-         */
-        renderPeopleDropdown: function () {
-
-          try {
-
-            var helpText = "Optionally add other contributors, collaborators, and maintainers of this dataset.",
-              placeholderText = "Choose new person or organization role ...";
-
-            this.partyMenu = $(document.createElement("select"))
-              .attr("id", "new-party-menu")
-              .addClass("header-dropdown");
+          //Mark this model as new
+          isNew = true;
 
-            //Add the first option to the menu, which works as a label
-            this.partyMenu.append($(document.createElement("option")).text(placeholderText));
-
-            //Add some help text for the menu
-            this.partyMenu.attr("title", helpText);
-
-            //Add a container element for the new party
-            this.newPartyContainer = $(document.createElement("div"))
-              .attr("data-attribute", "new")
-              .addClass("row-striped");
-
-            //For each party type, add it to the menu as an option
-            EMLParty.prototype.partyTypes.forEach(function (partyType) {
-              $(this.partyMenu).append($(document.createElement("option"))
-                .val(partyType.dataCategory)
-                .text(partyType.label))
-            }, this);
-
-            // Add the menu and new party element to the page
-            this.peopleSection.append(this.partyMenu, this.newPartyContainer);
-
-          } catch (error) {
-            console.log("Error creating the menu for adding new party categories, error message: " + error);
-          }
-
-        },
-
-        /**
-         * Render the information provided for a given EML party in the party section.
-         *
-         * @param  {EMLParty} emlParty - the EMLParty model to render. If set to null, a new EML party will be created for the given party type.
-         * @param  {string} partyType - The party type for which to render a new EML party. E.g. "creator", "coPrincipalInvestigator", etc.
-         */
-        renderPerson: function (emlParty, partyType) {
-
-          // Whether or not this is a new emlParty model
-          var isNew = false;
-
-          //If no model is given, create a new model
-          if (!emlParty) {
-
-            var emlParty = new EMLParty({
-              parentModel: this.model
-            });
-
-            //Mark this model as new
-            isNew = true;
-
-            // Find the party type or role based on the type given.
-            // Update the model.
-            if (partyType) {
-              var partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { dataCategory: partyType });
-              if (partyTypeProperties) {
-                if (partyTypeProperties.isAssociatedParty) {
-                  var newRoles = _.clone(emlParty.get("roles"));
-                  newRoles.push(partyType);
-                  emlParty.set("roles", newRoles);
-                } else {
-                  emlParty.set("type", partyType);
-                }
+          // Find the party type or role based on the type given.
+          // Update the model.
+          if (partyType) {
+            var partyTypeProperties = _.findWhere(
+              EMLParty.prototype.partyTypes,
+              { dataCategory: partyType },
+            );
+            if (partyTypeProperties) {
+              if (partyTypeProperties.isAssociatedParty) {
+                var newRoles = _.clone(emlParty.get("roles"));
+                newRoles.push(partyType);
+                emlParty.set("roles", newRoles);
+              } else {
+                emlParty.set("type", partyType);
               }
             }
-
           }
-          else {
-
-            //Get the party type, if it was not sent as a parameter
-            if (!partyType || !partyType.length) {
-              var partyType = emlParty.get("type");
-              if (partyType == "associatedParty" || !partyType || !partyType.length) {
-                partyType = emlParty.get("roles");
-              }
+        } else {
+          //Get the party type, if it was not sent as a parameter
+          if (!partyType || !partyType.length) {
+            var partyType = emlParty.get("type");
+            if (
+              partyType == "associatedParty" ||
+              !partyType ||
+              !partyType.length
+            ) {
+              partyType = emlParty.get("roles");
             }
-
-          }
-
-          // partyType is a string when if it's a 'type' and an array if it's 'roles'
-          // If it's a string, convert to an array for the subsequent _.each() function
-          if (typeof partyType == "string") {
-            partyType = [partyType]
           }
+        }
 
-          _.each(partyType, function (partyType) {
+        // partyType is a string when if it's a 'type' and an array if it's 'roles'
+        // If it's a string, convert to an array for the subsequent _.each() function
+        if (typeof partyType == "string") {
+          partyType = [partyType];
+        }
 
+        _.each(
+          partyType,
+          function (partyType) {
             // The container for this specific party type
             var container = null;
 
             if (partyType === "new") {
               container = this.newPartyContainer;
             } else {
-              var partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { dataCategory: partyType });
+              var partyTypeProperties = _.findWhere(
+                EMLParty.prototype.partyTypes,
+                { dataCategory: partyType },
+              );
               if (partyTypeProperties) {
                 container = partyTypeProperties.containerEl;
               }
@@ -592,7 +675,7 @@ 

Source: src/js/views/metadata/EML211View.js

var partyView = new EMLPartyView({ model: emlParty, edit: this.edit, - isNew: isNew + isNew: isNew, }); if (isNew) { @@ -600,273 +683,307 @@

Source: src/js/views/metadata/EML211View.js

} else { if (container.find(".new").length) container.find(".new").before(partyView.render().el); - else - container.append(partyView.render().el); + else container.append(partyView.render().el); } + }, + this, + ); + }, - }, this); - - - }, - - /* - * This function reacts to the user typing a new person in the person section (an EMLPartyView) - */ - handlePersonTyping: function (e) { - - var container = $(e.target).parents(".eml-party"), - emlParty = container.length ? container.data("model") : null, - partyType = container.length && emlParty ? emlParty.get("roles")[0] || emlParty.get("type") : null; - partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { dataCategory: partyType }), - numPartyForms = this.$("[data-attribute='" + partyType + "'] .eml-party").length, - numNewPartyForms = this.$("[data-attribute='" + partyType + "'] .eml-party.new").length; - - // If there is already a form to enter a new party for this party type, don't add another one - if (numNewPartyForms > 1) return; - - // If there is a limit to how many party types can be added for this type, - // don't add more forms than is allowed - if (partyTypeProperties && partyTypeProperties.limit) { - return - } - - // Render a form to enter information for a new person - this.renderPerson(null, partyType); - - }, - - /* - * This function is called when someone chooses a new person type from the dropdown list - */ - chooseNewPersonType: function (e) { + /* + * This function reacts to the user typing a new person in the person section (an EMLPartyView) + */ + handlePersonTyping: function (e) { + var container = $(e.target).parents(".eml-party"), + emlParty = container.length ? container.data("model") : null, + partyType = + container.length && emlParty + ? emlParty.get("roles")[0] || emlParty.get("type") + : null; + (partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { + dataCategory: partyType, + })), + (numPartyForms = this.$( + "[data-attribute='" + partyType + "'] .eml-party", + ).length), + (numNewPartyForms = this.$( + "[data-attribute='" + partyType + "'] .eml-party.new", + ).length); + + // If there is already a form to enter a new party for this party type, don't add another one + if (numNewPartyForms > 1) return; + + // If there is a limit to how many party types can be added for this type, + // don't add more forms than is allowed + if (partyTypeProperties && partyTypeProperties.limit) { + return; + } - var partyType = $(e.target).val(); + // Render a form to enter information for a new person + this.renderPerson(null, partyType); + }, - if (!partyType) return; + /* + * This function is called when someone chooses a new person type from the dropdown list + */ + chooseNewPersonType: function (e) { + var partyType = $(e.target).val(); - //Get the form and model - var partyForm = this.newPartyContainer, - partyModel = partyForm.find(".eml-party").data("model").clone(), - partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { dataCategory: partyType }); + if (!partyType) return; + //Get the form and model + var partyForm = this.newPartyContainer, + partyModel = partyForm.find(".eml-party").data("model").clone(), + partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { + dataCategory: partyType, + }); - // Remove this type from the dropdown menu - this.partyMenu.find("[value='" + partyType + "']").remove(); + // Remove this type from the dropdown menu + this.partyMenu.find("[value='" + partyType + "']").remove(); - if (!partyModel.isEmpty()) { - //Update the model - if (partyTypeProperties.isAssociatedParty) { - var newRoles = _.clone(partyModel.get("roles")); - newRoles.push(partyType); - partyModel.set("roles", newRoles); - } else { - partyModel.set("type", partyType); - } - if (partyModel.isValid()) { - partyModel.mergeIntoParent(); - // Add the person of that type (a section will be added if required) - this.renderPerson(partyModel, partyType); - // Clear and re-render the new person form - partyForm.empty(); - this.renderPerson(null, "new"); - } - else { - partyForm.find(".eml-party").data("view").showValidation(); - } + if (!partyModel.isEmpty()) { + //Update the model + if (partyTypeProperties.isAssociatedParty) { + var newRoles = _.clone(partyModel.get("roles")); + newRoles.push(partyType); + partyModel.set("roles", newRoles); } else { - this.addNewPersonType(partyType); + partyModel.set("type", partyType); + } + if (partyModel.isValid()) { + partyModel.mergeIntoParent(); + // Add the person of that type (a section will be added if required) + this.renderPerson(partyModel, partyType); + // Clear and re-render the new person form + partyForm.empty(); + this.renderPerson(null, "new"); + } else { + partyForm.find(".eml-party").data("view").showValidation(); } + } else { + this.addNewPersonType(partyType); + } + }, - }, + /* + * addNewPersonType - Adds a header and container to the People section for the given party type/role, + * @return {JQuery} Returns the HTML element that contains each rendered EML Party for the given party type. + */ + addNewPersonType: function (partyType) { + if (!partyType) return; - /* - * addNewPersonType - Adds a header and container to the People section for the given party type/role, - * @return {JQuery} Returns the HTML element that contains each rendered EML Party for the given party type. - */ - addNewPersonType: function (partyType) { + var partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { + dataCategory: partyType, + }); - if (!partyType) return; + if (!partyTypeProperties) { + return; + } - var partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { dataCategory: partyType }); + // If there is already a view for this person type, don't re-add it. + if (partyTypeProperties.containerEl) { + return; + } - if (!partyTypeProperties) { - return - } + // Container element to hold all parties of this type + var outerContainer = $(document.createElement("div")).addClass( + "party-type-container", + ); + + // Add a new header for the party type, + // plus an icon and spot for validation messages + var header = $(document.createElement("h4")) + .text(partyTypeProperties.label) + .append( + "<i class='required-icon hidden' data-category='" + + partyType + + "'></i>", + ); + + outerContainer.append(header); + + // If there is a description, add that to the container as well + if (partyTypeProperties.description) { + outerContainer.append( + '<p class="subtle">' + partyTypeProperties.description + "</p>", + ); + } - // If there is already a view for this person type, don't re-add it. - if (partyTypeProperties.containerEl) { - return - } + //Remove this type from the dropdown menu + this.partyMenu.find("[value='" + partyType + "']").remove(); + + //Add the new party container + partyTypeProperties.containerEl = $(document.createElement("div")) + .attr("data-attribute", partyType) + .attr("data-category", partyType) + .addClass("row-striped"); + let notification = document.createElement("p"); + notification.className = "notification"; + notification.setAttribute("data-category", partyType); + partyTypeProperties.containerEl.append(notification); + outerContainer.append(partyTypeProperties.containerEl); + + // Add in the new party type container just before the dropdown + this.partyMenu.before(outerContainer); + + // Add a blank form to the new person type section, unless the max number + // for this party type has already been reached (e.g. when a new person type + // is added after copying from another type) + if ( + typeof partyTypeProperties.limit !== "number" || + this.model.getPartiesByType(partyType).length < + partyTypeProperties.limit + ) { + this.renderPerson(null, partyType); + } - // Container element to hold all parties of this type - var outerContainer = $(document.createElement("div")).addClass("party-type-container"); + return partyTypeProperties.containerEl; + }, - // Add a new header for the party type, - // plus an icon and spot for validation messages - var header = $(document.createElement("h4")) - .text(partyTypeProperties.label) - .append("<i class='required-icon hidden' data-category='" + partyType + "'></i>"); + /* + * showCopyPersonMenu: Displays a modal window to the user with a list of roles that they can + * copy this person to + */ + showCopyPersonMenu: function (e) { + //Get the EMLParty to copy + var partyToCopy = $(e.target).parents(".eml-party").data("model"), + menu = this.$("#copy-person-menu"); + + //Check if the modal window menu has been created already + if (!menu.length) { + //Create the modal window menu from the template + menu = $(this.copyPersonMenuTemplate()); + + //Add to the DOM + this.$el.append(menu); + + //Initialize the modal + menu.modal(); + } else { + //Reset all the checkboxes + menu.find("input:checked").prop("checked", false); + menu + .find(".disabled") + .prop("disabled", false) + .removeClass("disabled") + .parent(".checkbox") + .attr("title", ""); + } - outerContainer.append(header); + //Disable the roles this person is already in + var currentRoles = partyToCopy.get("roles"); + if (!currentRoles || !currentRoles.length) { + currentRoles = partyToCopy.get("type"); + } + // "type" is a string and "roles" is an array. + // so that we can use _.each() on both, convert "type" to an array + if (typeof currentRoles === "string") { + currentRoles = [currentRoles]; + } - // If there is a description, add that to the container as well - if (partyTypeProperties.description) { - outerContainer.append('<p class="subtle">' + partyTypeProperties.description + '</p>'); - } + _.each( + currentRoles, + function (currentRole) { + var partyTypeProperties = _.findWhere( + EMLParty.prototype.partyTypes, + { dataCategory: currentRole }, + ), + label = partyTypeProperties ? partyTypeProperties.label : ""; - //Remove this type from the dropdown menu - this.partyMenu.find("[value='" + partyType + "']").remove(); + menu + .find("input[value='" + currentRole + "']") + .prop("disabled", "disabled") + .addClass("disabled") + .parent(".checkbox") + .attr( + "title", + "This person is already in the " + label + " list.", + ); + }, + this, + ); + + // If the maximum number of parties has already been for this party type, + // then don't allow adding more. + + var partiesWithLimits = _.filter( + EMLParty.prototype.partyTypes, + function (partyType) { + return typeof partyType.limit === "number"; + }, + ); + + partiesWithLimits.forEach(function (partyType) { + // See how many parties already exist for this type + var existingParties = this.model.getPartiesByType( + partyType.dataCategory, + ); - //Add the new party container - partyTypeProperties.containerEl = $(document.createElement("div")) - .attr("data-attribute", partyType) - .attr("data-category", partyType) - .addClass("row-striped"); - let notification=document.createElement("p"); - notification.className="notification"; - notification.setAttribute("data-category", partyType); - partyTypeProperties.containerEl.append(notification); - outerContainer.append(partyTypeProperties.containerEl); - - // Add in the new party type container just before the dropdown - this.partyMenu.before(outerContainer); - - // Add a blank form to the new person type section, unless the max number - // for this party type has already been reached (e.g. when a new person type - // is added after copying from another type) if ( - typeof partyTypeProperties.limit !== "number" || - (this.model.getPartiesByType(partyType).length < partyTypeProperties.limit) + existingParties && + existingParties.length && + existingParties.length >= partyType.limit ) { - this.renderPerson(null, partyType); - } - - return partyTypeProperties.containerEl - }, - - /* - * showCopyPersonMenu: Displays a modal window to the user with a list of roles that they can - * copy this person to - */ - showCopyPersonMenu: function (e) { - - //Get the EMLParty to copy - var partyToCopy = $(e.target).parents(".eml-party").data("model"), - menu = this.$("#copy-person-menu"); - - //Check if the modal window menu has been created already - if (!menu.length) { - - //Create the modal window menu from the template - menu = $(this.copyPersonMenuTemplate()); - - //Add to the DOM - this.$el.append(menu); - - //Initialize the modal - menu.modal(); - } - else { - //Reset all the checkboxes - menu.find("input:checked").prop("checked", false); - menu.find(".disabled") - .prop("disabled", false) - .removeClass("disabled") - .parent(".checkbox") - .attr("title", ""); - } - - //Disable the roles this person is already in - var currentRoles = partyToCopy.get("roles"); - if (!currentRoles || !currentRoles.length) { - currentRoles = partyToCopy.get("type"); - } - // "type" is a string and "roles" is an array. - // so that we can use _.each() on both, convert "type" to an array - if (typeof currentRoles === "string") { - currentRoles = [currentRoles]; - } - - _.each(currentRoles, function (currentRole) { - - var partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { dataCategory: currentRole }), - label = partyTypeProperties ? partyTypeProperties.label : ""; - - menu.find("input[value='" + currentRole + "']") + var names = _.map(existingParties, function (partyModel) { + var name = partyModel.getName(); + if (name) { + return name; + } else { + return "Someone"; + } + }); + var sep = names.length === 2 ? " and " : ", ", + beVerbNames = names.length > 1 ? "are" : "is", + beVerbLimit = partyType.limit > 1 ? "are" : "is", + title = + names.join(sep) + + " " + + beVerbNames + + " already listed as " + + partyType.dataCategory + + ". (Only " + + partyType.limit + + " " + + beVerbLimit + + " is allowed.)"; + + menu + .find("input[value='" + partyType.dataCategory + "']") .prop("disabled", "disabled") .addClass("disabled") .parent(".checkbox") - .attr("title", "This person is already in the " + label + " list."); - - }, this); - - // If the maximum number of parties has already been for this party type, - // then don't allow adding more. - - var partiesWithLimits = _.filter(EMLParty.prototype.partyTypes, function (partyType) { - return typeof partyType.limit === "number" - }); - - partiesWithLimits.forEach(function (partyType) { - - // See how many parties already exist for this type - var existingParties = this.model.getPartiesByType(partyType.dataCategory); - - if ( - existingParties && - existingParties.length && - existingParties.length >= partyType.limit - ) { - var names = _.map(existingParties, function (partyModel) { - var name = partyModel.getName(); - if (name) { - return name - } else { - return "Someone" - } - }); - var sep = names.length === 2 ? " and " : ", ", - beVerbNames = names.length > 1 ? "are" : "is", - beVerbLimit = partyType.limit > 1 ? "are" : "is", - title = names.join(sep) + " " + beVerbNames + " already listed as " + - partyType.dataCategory + ". (Only " + partyType.limit + " " + - beVerbLimit + " is allowed.)"; - - menu.find("input[value='" + partyType.dataCategory + "']") - .prop("disabled", "disabled") - .addClass("disabled") - .parent(".checkbox") - .attr("title", title); - - } - }, this); - - //Attach the EMLParty to the menu DOMs - menu.data({ - EMLParty: partyToCopy - }); - - //Show the modal window menu now - menu.modal("show"); - }, + .attr("title", title); + } + }, this); - /* - * copyPerson: Gets the selected checkboxes from the copy person menu and copies the EMLParty - * to those new roles - */ - copyPerson: function () { + //Attach the EMLParty to the menu DOMs + menu.data({ + EMLParty: partyToCopy, + }); - //Get all the checked boxes - var checkedBoxes = this.$("#copy-person-menu input:checked"), - //Get the EMLParty to copy - partyToCopy = this.$("#copy-person-menu").data("EMLParty"); + //Show the modal window menu now + menu.modal("show"); + }, - //For each selected role, - _.each(checkedBoxes, function (checkedBox) { + /* + * copyPerson: Gets the selected checkboxes from the copy person menu and copies the EMLParty + * to those new roles + */ + copyPerson: function () { + //Get all the checked boxes + var checkedBoxes = this.$("#copy-person-menu input:checked"), + //Get the EMLParty to copy + partyToCopy = this.$("#copy-person-menu").data("EMLParty"); + //For each selected role, + _.each( + checkedBoxes, + function (checkedBox) { //Get the roles var role = $(checkedBox).val(), - partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { dataCategory: role }); + partyTypeProperties = _.findWhere(EMLParty.prototype.partyTypes, { + dataCategory: role, + }); //Create a new EMLParty model var newPerson = new EMLParty(); @@ -890,157 +1007,169 @@

Source: src/js/views/metadata/EML211View.js

// Add a view for the copied person this.renderPerson(newPerson); + }, + this, + ); - }, this); - - //If there was at least one copy created, then trigger the change event - if (checkedBoxes.length) { - this.model.trickleUpChange(); - } - - }, - - removePerson: function (e) { - e.preventDefault(); + //If there was at least one copy created, then trigger the change event + if (checkedBoxes.length) { + this.model.trickleUpChange(); + } + }, - //Get the party view el, view, and model - var partyEl = $(e.target).parents(".eml-party"), - partyView = partyEl.data("view"), - partyToRemove = partyEl.data("model"); + removePerson: function (e) { + e.preventDefault(); - //If there is no model found, we have nothing to do, so exit - if (!partyToRemove) return false; + //Get the party view el, view, and model + var partyEl = $(e.target).parents(".eml-party"), + partyView = partyEl.data("view"), + partyToRemove = partyEl.data("model"); - //Call removeParty on the EML211 model to remove this EMLParty - this.model.removeParty(partyToRemove); + //If there is no model found, we have nothing to do, so exit + if (!partyToRemove) return false; - //Let the EMLPartyView remove itself - partyView.remove(); + //Call removeParty on the EML211 model to remove this EMLParty + this.model.removeParty(partyToRemove); - }, + //Let the EMLPartyView remove itself + partyView.remove(); + }, - /** - * Attempt to move the current person (Party) one index backward (up). - * - * @param {EventHandler} e: The click event handler - */ - movePersonUp: function (e) { - e.preventDefault(); + /** + * Attempt to move the current person (Party) one index backward (up). + * + * @param {EventHandler} e: The click event handler + */ + movePersonUp: function (e) { + e.preventDefault(); - // Get the party view el, view, and model - var partyEl = $(e.target).parents(".eml-party"), - model = partyEl.data("model"), - next = $(partyEl).prev().not(".new"); + // Get the party view el, view, and model + var partyEl = $(e.target).parents(".eml-party"), + model = partyEl.data("model"), + next = $(partyEl).prev().not(".new"); - if (next.length === 0) { - return; - } + if (next.length === 0) { + return; + } - // Remove current view, create and insert a new one for the model - $(partyEl).remove(); + // Remove current view, create and insert a new one for the model + $(partyEl).remove(); - var newView = new EMLPartyView({ - model: model, - edit: this.edit - }); + var newView = new EMLPartyView({ + model: model, + edit: this.edit, + }); - $(next).before(newView.render().el); + $(next).before(newView.render().el); - // Move the party down within the model too - this.model.movePartyUp(model); - this.model.trickleUpChange(); - }, - - /** - * Attempt to move the current person (Party) one index forward (down). - * - * @param {EventHandler} e: The click event handler - */ - movePersonDown: function (e) { - e.preventDefault(); - - // Get the party view el, view, and model - var partyEl = $(e.target).parents(".eml-party"), - model = partyEl.data("model"), - next = $(partyEl).next().not(".new"); - - if (next.length === 0) { - return; - } + // Move the party down within the model too + this.model.movePartyUp(model); + this.model.trickleUpChange(); + }, - // Remove current view, create and insert a new one for the model - $(partyEl).remove(); + /** + * Attempt to move the current person (Party) one index forward (down). + * + * @param {EventHandler} e: The click event handler + */ + movePersonDown: function (e) { + e.preventDefault(); - var newView = new EMLPartyView({ - model: model, - edit: this.edit - }); + // Get the party view el, view, and model + var partyEl = $(e.target).parents(".eml-party"), + model = partyEl.data("model"), + next = $(partyEl).next().not(".new"); - $(next).after(newView.render().el); + if (next.length === 0) { + return; + } - // Move the party down within the model too - this.model.movePartyDown(model); - this.model.trickleUpChange(); - }, + // Remove current view, create and insert a new one for the model + $(partyEl).remove(); - /* - * Renders the Dates section of the page - */ - renderDates: function () { + var newView = new EMLPartyView({ + model: model, + edit: this.edit, + }); - //Add a header - this.$(".section.dates").html($(document.createElement("h2")).text("Dates")); + $(next).after(newView.render().el); - _.each(this.model.get('temporalCoverage'), function (model) { + // Move the party down within the model too + this.model.movePartyDown(model); + this.model.trickleUpChange(); + }, + /* + * Renders the Dates section of the page + */ + renderDates: function () { + //Add a header + this.$(".section.dates").html( + $(document.createElement("h2")).text("Dates"), + ); + + _.each( + this.model.get("temporalCoverage"), + function (model) { var tempCovView = new EMLTempCoverageView({ model: model, isNew: false, - edit: this.edit + edit: this.edit, }); tempCovView.render(); this.$(".section.dates").append(tempCovView.el); + }, + this, + ); - }, this); - - if (!this.model.get('temporalCoverage').length) { - var tempCovView = new EMLTempCoverageView({ - isNew: true, - edit: this.edit, - model: new EMLTemporalCoverage({ parentModel: this.model }) - }); + if (!this.model.get("temporalCoverage").length) { + var tempCovView = new EMLTempCoverageView({ + isNew: true, + edit: this.edit, + model: new EMLTemporalCoverage({ parentModel: this.model }), + }); - tempCovView.render(); + tempCovView.render(); - this.$(".section.dates").append(tempCovView.el); - } - - }, + this.$(".section.dates").append(tempCovView.el); + } + }, - /* - * Renders the Locations section of the page - */ - renderLocations: function () { - var locationsSection = this.$(".section.locations"); + /* + * Renders the Locations section of the page + */ + renderLocations: function () { + var locationsSection = this.$(".section.locations"); - //Add the Locations header - locationsSection.html(this.locationsTemplate()); - var locationsTable = locationsSection.find(".locations-table"); + //Add the Locations header + locationsSection.html(this.locationsTemplate()); + var locationsTable = locationsSection.find(".locations-table"); - //Render an EMLGeoCoverage view for each EMLGeoCoverage model - _.each(this.model.get("geoCoverage"), function (geo, i) { + //Render an EMLGeoCoverage view for each EMLGeoCoverage model + _.each( + this.model.get("geoCoverage"), + function (geo, i) { //Create an EMLGeoCoverageView var geoView = new EMLGeoCoverageView({ model: geo, - edit: this.edit + edit: this.edit, }); //Render the view geoView.render(); - geoView.$el.find(".remove-container").append(this.createRemoveButton(null, "geoCoverage", ".eml-geocoverage", ".locations-table")); + geoView.$el + .find(".remove-container") + .append( + this.createRemoveButton( + null, + "geoCoverage", + ".eml-geocoverage", + ".locations-table", + ), + ); //Add the locations section to the page locationsTable.append(geoView.el); @@ -1050,390 +1179,511 @@

Source: src/js/views/metadata/EML211View.js

//Save it in our subviews array this.subviews.push(geoView); - }, this); - - //Now add one empty row to enter a new geo coverage - if (this.edit) { - var newGeoModel = new EMLGeoCoverage({ parentModel: this.model, isNew: true }), - newGeoView = new EMLGeoCoverageView({ - edit: true, - model: newGeoModel, - isNew: true - }); - locationsTable.append(newGeoView.render().el); - newGeoView.$el.find(".remove-container").append(this.createRemoveButton(null, "geoCoverage", ".eml-geocoverage", ".locations-table")); - - //Listen to validation events - this.listenTo(newGeoModel, "valid", this.updateLocationsError); - } - }, + }, + this, + ); - /* - * Renders the Taxa section of the page - */ - renderTaxa: function () { - const view = this; - - const taxaSectionEl = this.$(".section.taxa"); - if (!taxaSectionEl) return; + //Now add one empty row to enter a new geo coverage + if (this.edit) { + var newGeoModel = new EMLGeoCoverage({ + parentModel: this.model, + isNew: true, + }), + newGeoView = new EMLGeoCoverageView({ + edit: true, + model: newGeoModel, + isNew: true, + }); + locationsTable.append(newGeoView.render().el); + newGeoView.$el + .find(".remove-container") + .append( + this.createRemoveButton( + null, + "geoCoverage", + ".eml-geocoverage", + ".locations-table", + ), + ); - taxaSectionEl.html($(document.createElement("h2")).text("Taxa")); + //Listen to validation events + this.listenTo(newGeoModel, "valid", this.updateLocationsError); + } + }, - var taxonomy = this.model.get('taxonCoverage'); + /* + * Renders the Taxa section of the page + */ + renderTaxa: function () { + const view = this; - // Render a set of tables for each taxonomicCoverage - if (typeof taxonomy !== "undefined" && (Array.isArray(taxonomy) && taxonomy.length)) { - for (var i = 0; i < taxonomy.length; i++) { - taxaSectionEl.append(this.createTaxonomicCoverage(taxonomy[i])); - } - } else { - // Create a new one - var taxonCov = new EMLTaxonCoverage({ - parentModel: this.model - }); + const taxaSectionEl = this.$(".section.taxa"); + if (!taxaSectionEl) return; - this.model.set('taxonCoverage', [taxonCov], { silent: true }); + taxaSectionEl.html($(document.createElement("h2")).text("Taxa")); - taxaSectionEl.append(this.createTaxonomicCoverage(taxonCov)); - } + var taxonomy = this.model.get("taxonCoverage"); - // updating the indexes of taxa-tables before rendering the information on page(view). - var taxaNums = this.$(".editor-header-index"); - for (var i = 0; i < taxaNums.length; i++) { - $(taxaNums[i]).text(i + 1); + // Render a set of tables for each taxonomicCoverage + if ( + typeof taxonomy !== "undefined" && + Array.isArray(taxonomy) && + taxonomy.length + ) { + for (var i = 0; i < taxonomy.length; i++) { + taxaSectionEl.append(this.createTaxonomicCoverage(taxonomy[i])); } + } else { + // Create a new one + var taxonCov = new EMLTaxonCoverage({ + parentModel: this.model, + }); - // Insert the quick-add taxon options, if any are configured for this - // theme. See {@link AppModel#quickAddTaxa} - view.renderTaxaQuickAdd(); - - // If duplicates are removed while saving, make sure to re-render the taxa - view.model.get("taxonCoverage").forEach(function (taxonCov) { - view.model.stopListening(taxonCov); - view.model.listenTo( - taxonCov, - "duplicateClassificationsRemoved", - function () { - view.renderTaxa(); - } - ) - }, view); - }, - - /* - * Renders the Methods section of the page - */ - renderMethods: function () { - var methodsModel = this.model.get("methods"); - - if (!methodsModel) { - methodsModel = new EMLMethods({ edit: this.edit, parentModel: this.model }); - } + this.model.set("taxonCoverage", [taxonCov], { silent: true }); - this.$(".section.methods").html(new EMLMethodsView({ - model: methodsModel, - edit: this.edit, - parentEMLView: this - }).render().el); - }, + taxaSectionEl.append(this.createTaxonomicCoverage(taxonCov)); + } - /* - * Renders the Projcet section of the page - */ - renderProject: function () { + // updating the indexes of taxa-tables before rendering the information on page(view). + var taxaNums = this.$(".editor-header-index"); + for (var i = 0; i < taxaNums.length; i++) { + $(taxaNums[i]).text(i + 1); + } - }, + // Insert the quick-add taxon options, if any are configured for this + // theme. See {@link AppModel#quickAddTaxa} + view.renderTaxaQuickAdd(); + + // If duplicates are removed while saving, make sure to re-render the taxa + view.model.get("taxonCoverage").forEach(function (taxonCov) { + view.model.stopListening(taxonCov); + view.model.listenTo( + taxonCov, + "duplicateClassificationsRemoved", + function () { + view.renderTaxa(); + }, + ); + }, view); + }, - /* - * Renders the Sharing section of the page - */ - renderSharing: function () { + /* + * Renders the Methods section of the page + */ + renderMethods: function () { + var methodsModel = this.model.get("methods"); - }, + if (!methodsModel) { + methodsModel = new EMLMethods({ + edit: this.edit, + parentModel: this.model, + }); + } - /* - * Renders the funding field of the EML - */ - renderFunding: function () { - //Funding - var funding = this.model.get("project") ? this.model.get("project").get("funding") : []; + this.$(".section.methods").html( + new EMLMethodsView({ + model: methodsModel, + edit: this.edit, + parentEMLView: this, + }).render().el, + ); + }, - //Clear the funding section - $(".section.overview .funding").empty(); + /* + * Renders the Projcet section of the page + */ + renderProject: function () {}, - //Create the funding input elements - _.each(funding, function (fundingItem, i) { + /* + * Renders the Sharing section of the page + */ + renderSharing: function () {}, + /* + * Renders the funding field of the EML + */ + renderFunding: function () { + //Funding + var funding = this.model.get("project") + ? this.model.get("project").get("funding") + : []; + + //Clear the funding section + $(".section.overview .funding").empty(); + + //Create the funding input elements + _.each( + funding, + function (fundingItem, i) { this.addFunding(fundingItem); + }, + this, + ); - }, this); + //Add a blank funding input + this.addFunding(); + }, - //Add a blank funding input - this.addFunding(); - }, - - /* - * Adds a single funding input row. Can either be called directly or used as an event callback - */ - addFunding: function (argument) { - if (this.edit) { - - if (typeof argument == "string") - var value = argument; - else if (!argument) - var value = ""; - //Don't add another new funding input if there already is one - else if (!value && (typeof argument == "object") && !$(argument.target).is(".new")) - return; - else if ((typeof argument == "object") && argument.target) { - var event = argument; - - // Don't add a new funding row if the current one is empty - if ($(event.target).val().trim() === "") return; - } + /* + * Adds a single funding input row. Can either be called directly or used as an event callback + */ + addFunding: function (argument) { + if (this.edit) { + if (typeof argument == "string") var value = argument; + else if (!argument) var value = ""; + //Don't add another new funding input if there already is one + else if ( + !value && + typeof argument == "object" && + !$(argument.target).is(".new") + ) + return; + else if (typeof argument == "object" && argument.target) { + var event = argument; + + // Don't add a new funding row if the current one is empty + if ($(event.target).val().trim() === "") return; + } - var fundingInput = $(document.createElement("input")) + var fundingInput = $(document.createElement("input")) .attr("type", "text") .attr("data-category", "funding") .addClass("span12 funding hover-autocomplete-target") - .attr("placeholder", "Search for NSF awards by keyword or enter custom funding information") + .attr( + "placeholder", + "Search for NSF awards by keyword or enter custom funding information", + ) .val(value), - hiddenFundingInput = fundingInput.clone().attr("type", "hidden").val(value).attr("id", "").addClass("hidden"), - loadingSpinner = $(document.createElement("i")).addClass("icon icon-spinner input-icon icon-spin subtle hidden"); - - //Append all the elements to a container - var containerEl = $(document.createElement("div")) - .addClass("ui-autocomplete-container funding-row") - .append(fundingInput, - loadingSpinner, - hiddenFundingInput); - - if (!value) { - $(fundingInput).addClass("new"); - - if (event) { - $(event.target).parents("div.funding-row").append(this.createRemoveButton('project', 'funding', '.funding-row', 'div.funding-container')); - $(event.target).removeClass("new"); - } - } else { // Add a remove button if this is a non-new funding element - $(containerEl).append(this.createRemoveButton('project', 'funding', '.funding-row', 'div.funding-container')); - } - - var view = this; - - //Setup the autocomplete widget for the funding input - fundingInput.autocomplete({ - source: function (request, response) { - var beforeRequest = function () { - loadingSpinner.show(); - } - - var afterRequest = function () { - loadingSpinner.hide(); - } - - return MetacatUI.appLookupModel.getGrantAutocomplete(request, response, beforeRequest, afterRequest) - }, - select: function (e, ui) { - e.preventDefault(); - - var value = "NSF Award " + ui.item.value + " (" + ui.item.label + ")"; - hiddenFundingInput.val(value); - fundingInput.val(value); - - $(".funding .ui-helper-hidden-accessible").hide(); - - view.updateFunding(e); - - }, - position: { - my: "left top", - at: "left bottom", - of: fundingInput, - collision: "fit" - }, - appendTo: containerEl, - minLength: 3 - }); - - this.$(".funding-container").append(containerEl); - } - }, - - previewFundingRemove: function (e) { - $(e.target).parents(".funding-row").toggleClass("remove-preview"); - }, - - handleFundingTyping: function (e) { - var fundingInput = $(e.target); - - //If the funding value is at least one character - if (fundingInput.val().length > 0) { - //Get rid of the error styling in this row - fundingInput.parent(".funding-row").children().removeClass("error"); - - //If this was the only funding input with an error, we can safely remove the error message - if (!this.$("input.funding.error").length) - this.$("[data-category='funding'] .notification").removeClass("error").text(""); - } - }, - - addKeyword: function (keyword, thesaurus) { - if (typeof keyword != "string" || !keyword) { - var keyword = ""; + hiddenFundingInput = fundingInput + .clone() + .attr("type", "hidden") + .val(value) + .attr("id", "") + .addClass("hidden"), + loadingSpinner = $(document.createElement("i")).addClass( + "icon icon-spinner input-icon icon-spin subtle hidden", + ); - //Only show one new keyword row at a time - if ((this.$(".keyword.new").length == 1) && !this.$(".keyword.new").val()) - return; - else if (this.$(".keyword.new").length > 1) - return; + //Append all the elements to a container + var containerEl = $(document.createElement("div")) + .addClass("ui-autocomplete-container funding-row") + .append(fundingInput, loadingSpinner, hiddenFundingInput); + + if (!value) { + $(fundingInput).addClass("new"); + + if (event) { + $(event.target) + .parents("div.funding-row") + .append( + this.createRemoveButton( + "project", + "funding", + ".funding-row", + "div.funding-container", + ), + ); + $(event.target).removeClass("new"); + } + } else { + // Add a remove button if this is a non-new funding element + $(containerEl).append( + this.createRemoveButton( + "project", + "funding", + ".funding-row", + "div.funding-container", + ), + ); } - //Create the keyword row HTML - var row = $(document.createElement("div")).addClass("row-fluid keyword-row"), - keywordInput = $(document.createElement("input")).attr("type", "text").addClass("keyword span10").attr("placeholder", "Add one new keyword"), - thesInput = $(document.createElement("select")).addClass("thesaurus span2"), - thesOptionExists = false, - removeButton; + var view = this; - // Piece together the inputs - row.append(keywordInput, thesInput); + //Setup the autocomplete widget for the funding input + fundingInput.autocomplete({ + source: function (request, response) { + var beforeRequest = function () { + loadingSpinner.show(); + }; + + var afterRequest = function () { + loadingSpinner.hide(); + }; + + return MetacatUI.appLookupModel.getGrantAutocomplete( + request, + response, + beforeRequest, + afterRequest, + ); + }, + select: function (e, ui) { + e.preventDefault(); - //Create the thesaurus options dropdown menu - _.each(MetacatUI.appModel.get("emlKeywordThesauri"), function (option) { + var value = + "NSF Award " + ui.item.value + " (" + ui.item.label + ")"; + hiddenFundingInput.val(value); + fundingInput.val(value); - var optionEl = $(document.createElement("option")).val(option.thesaurus).text(option.label); - thesInput.append(optionEl); + $(".funding .ui-helper-hidden-accessible").hide(); - if (option.thesaurus == thesaurus) { - optionEl.prop("selected", true); - thesOptionExists = true; - } + view.updateFunding(e); + }, + position: { + my: "left top", + at: "left bottom", + of: fundingInput, + collision: "fit", + }, + appendTo: containerEl, + minLength: 3, }); - //Add a "None" option, which is always in the dropdown - thesInput.prepend($(document.createElement("option")).val("None").text("None")); - - if (thesaurus == "None" || !thesaurus) { - thesInput.val("None"); - } - //If this keyword is from a custom thesaurus that is NOT configured in this App, AND - // there is an option with the same label, then remove the option so it doesn't look like a duplicate. - else if (!thesOptionExists && _.findWhere(MetacatUI.appModel.get("emlKeywordThesauri"), { label: thesaurus })) { - var duplicateOptions = thesInput.find("option:contains(" + thesaurus + ")"); - duplicateOptions.each(function (i, option) { - if ($(option).text() == thesaurus && !$(option).prop("selected")) { - $(option).remove(); - } - }); - } - //If this keyword is from a custom thesaurus that is NOT configured in this App, then show it as a custom option - else if (!thesOptionExists) { - thesInput.append($(document.createElement("option")).val(thesaurus).text(thesaurus).prop("selected", true)); - } - - if (!keyword) - row.addClass("new"); - else { - - //Set the keyword value on the text input - keywordInput.val(keyword); - - // Add a remove button unless this is the .new keyword - row.append(this.createRemoveButton(null, 'keywordSets', 'div.keyword-row', 'div.keywords')); - } + this.$(".funding-container").append(containerEl); + } + }, - this.$(".keywords").append(row); - }, + previewFundingRemove: function (e) { + $(e.target).parents(".funding-row").toggleClass("remove-preview"); + }, - addNewKeyword: function (e) { - if ($(e.target).val().trim() === "") return; + handleFundingTyping: function (e) { + var fundingInput = $(e.target); - $(e.target).parents(".keyword-row").first().removeClass("new"); + //If the funding value is at least one character + if (fundingInput.val().length > 0) { + //Get rid of the error styling in this row + fundingInput.parent(".funding-row").children().removeClass("error"); - // Add in a remove button - $(e.target).parents(".keyword-row").append(this.createRemoveButton(null, 'keywordSets', 'div.keyword-row', 'div.keywords')); + //If this was the only funding input with an error, we can safely remove the error message + if (!this.$("input.funding.error").length) + this.$("[data-category='funding'] .notification") + .removeClass("error") + .text(""); + } + }, - var row = $(document.createElement("div")).addClass("row-fluid keyword-row new").data({ model: new EMLKeywordSet() }), - keywordInput = $(document.createElement("input")).attr("type", "text").addClass("keyword span10"), - thesInput = $(document.createElement("select")).addClass("thesaurus span2"); + addKeyword: function (keyword, thesaurus) { + if (typeof keyword != "string" || !keyword) { + var keyword = ""; - row.append(keywordInput, thesInput); + //Only show one new keyword row at a time + if ( + this.$(".keyword.new").length == 1 && + !this.$(".keyword.new").val() + ) + return; + else if (this.$(".keyword.new").length > 1) return; + } - //Create the thesaurus options dropdown menu - _.each(MetacatUI.appModel.get("emlKeywordThesauri"), function (option) { - thesInput.append($(document.createElement("option")).val(option.thesaurus).text(option.label)); + //Create the keyword row HTML + var row = $(document.createElement("div")).addClass( + "row-fluid keyword-row", + ), + keywordInput = $(document.createElement("input")) + .attr("type", "text") + .addClass("keyword span10") + .attr("placeholder", "Add one new keyword"), + thesInput = $(document.createElement("select")).addClass( + "thesaurus span2", + ), + thesOptionExists = false, + removeButton; + + // Piece together the inputs + row.append(keywordInput, thesInput); + + //Create the thesaurus options dropdown menu + _.each(MetacatUI.appModel.get("emlKeywordThesauri"), function (option) { + var optionEl = $(document.createElement("option")) + .val(option.thesaurus) + .text(option.label); + thesInput.append(optionEl); + + if (option.thesaurus == thesaurus) { + optionEl.prop("selected", true); + thesOptionExists = true; + } + }); + + //Add a "None" option, which is always in the dropdown + thesInput.prepend( + $(document.createElement("option")).val("None").text("None"), + ); + + if (thesaurus == "None" || !thesaurus) { + thesInput.val("None"); + } + //If this keyword is from a custom thesaurus that is NOT configured in this App, AND + // there is an option with the same label, then remove the option so it doesn't look like a duplicate. + else if ( + !thesOptionExists && + _.findWhere(MetacatUI.appModel.get("emlKeywordThesauri"), { + label: thesaurus, + }) + ) { + var duplicateOptions = thesInput.find( + "option:contains(" + thesaurus + ")", + ); + duplicateOptions.each(function (i, option) { + if ($(option).text() == thesaurus && !$(option).prop("selected")) { + $(option).remove(); + } }); + } + //If this keyword is from a custom thesaurus that is NOT configured in this App, then show it as a custom option + else if (!thesOptionExists) { + thesInput.append( + $(document.createElement("option")) + .val(thesaurus) + .text(thesaurus) + .prop("selected", true), + ); + } - //Add a "None" option, which is always in the dropdown - thesInput.prepend($(document.createElement("option")).val("None").text("None").prop("selected", true)); - - this.$(".keywords").append(row); - }, - - previewKeywordRemove: function (e) { - var row = $(e.target).parents(".keyword-row").toggleClass("remove-preview"); - }, + if (!keyword) row.addClass("new"); + else { + //Set the keyword value on the text input + keywordInput.val(keyword); + + // Add a remove button unless this is the .new keyword + row.append( + this.createRemoveButton( + null, + "keywordSets", + "div.keyword-row", + "div.keywords", + ), + ); + } - /* - * Update the funding info when the form is changed - */ - updateFunding: function (e) { - if (!e) return; + this.$(".keywords").append(row); + }, - var row = $(e.target).parent(".funding-row").first(), - rowNum = this.$(".funding-row").index(row), - input = $(row).find("input"), - isNew = $(row).is(".new"); + addNewKeyword: function (e) { + if ($(e.target).val().trim() === "") return; + + $(e.target).parents(".keyword-row").first().removeClass("new"); + + // Add in a remove button + $(e.target) + .parents(".keyword-row") + .append( + this.createRemoveButton( + null, + "keywordSets", + "div.keyword-row", + "div.keywords", + ), + ); + + var row = $(document.createElement("div")) + .addClass("row-fluid keyword-row new") + .data({ model: new EMLKeywordSet() }), + keywordInput = $(document.createElement("input")) + .attr("type", "text") + .addClass("keyword span10"), + thesInput = $(document.createElement("select")).addClass( + "thesaurus span2", + ); + + row.append(keywordInput, thesInput); + + //Create the thesaurus options dropdown menu + _.each(MetacatUI.appModel.get("emlKeywordThesauri"), function (option) { + thesInput.append( + $(document.createElement("option")) + .val(option.thesaurus) + .text(option.label), + ); + }); + + //Add a "None" option, which is always in the dropdown + thesInput.prepend( + $(document.createElement("option")) + .val("None") + .text("None") + .prop("selected", true), + ); + + this.$(".keywords").append(row); + }, - var newValue = isNew ? $(e.target).siblings("input.hidden").val() : $(e.target).val(); + previewKeywordRemove: function (e) { + var row = $(e.target) + .parents(".keyword-row") + .toggleClass("remove-preview"); + }, - newValue = this.model.cleanXMLText(newValue); + /* + * Update the funding info when the form is changed + */ + updateFunding: function (e) { + if (!e) return; - if (typeof newValue == "string") { - newValue = newValue.trim(); - } + var row = $(e.target).parent(".funding-row").first(), + rowNum = this.$(".funding-row").index(row), + input = $(row).find("input"), + isNew = $(row).is(".new"); - //If there is no project model - if (!this.model.get("project")) { - var model = new EMLProject({ parentModel: this.model }); - this.model.set("project", model); - } - else - var model = this.model.get("project"); + var newValue = isNew + ? $(e.target).siblings("input.hidden").val() + : $(e.target).val(); - var currentFundingValues = model.get("funding"); + newValue = this.model.cleanXMLText(newValue); - //If the new value is an empty string, then remove that index in the array - if (typeof newValue == "string" && newValue.trim().length == 0) { - currentFundingValues = currentFundingValues.splice(rowNum, 1); - } - else { - currentFundingValues[rowNum] = newValue; - } + if (typeof newValue == "string") { + newValue = newValue.trim(); + } - if (isNew && newValue != '') { - $(row).removeClass("new"); + //If there is no project model + if (!this.model.get("project")) { + var model = new EMLProject({ parentModel: this.model }); + this.model.set("project", model); + } else var model = this.model.get("project"); - // Add in a remove button - $(e.target).parent().append(this.createRemoveButton('project', 'funding', '.funding-row', 'div.funding-container')); + var currentFundingValues = model.get("funding"); - this.addFunding(); - } + //If the new value is an empty string, then remove that index in the array + if (typeof newValue == "string" && newValue.trim().length == 0) { + currentFundingValues = currentFundingValues.splice(rowNum, 1); + } else { + currentFundingValues[rowNum] = newValue; + } - this.model.trickleUpChange(); + if (isNew && newValue != "") { + $(row).removeClass("new"); - }, + // Add in a remove button + $(e.target) + .parent() + .append( + this.createRemoveButton( + "project", + "funding", + ".funding-row", + "div.funding-container", + ), + ); - //TODO: Comma and semi-colon separate keywords - updateKeywords: function (e) { + this.addFunding(); + } - var keywordSets = this.model.get("keywordSets"), - newKeywordSets = []; + this.model.trickleUpChange(); + }, - //Get all the keywords in the view - _.each(this.$(".keyword-row"), function (thisRow) { - var thesaurus = this.model.cleanXMLText($(thisRow).find("select").val()), + //TODO: Comma and semi-colon separate keywords + updateKeywords: function (e) { + var keywordSets = this.model.get("keywordSets"), + newKeywordSets = []; + + //Get all the keywords in the view + _.each( + this.$(".keyword-row"), + function (thisRow) { + var thesaurus = this.model.cleanXMLText( + $(thisRow).find("select").val(), + ), keyword = this.model.cleanXMLText($(thisRow).find("input").val()); if (!keyword) return; @@ -1444,241 +1694,261 @@

Source: src/js/views/metadata/EML211View.js

if (typeof keywordSet != "undefined") { keywordSet.get("keywords").push(keyword); + } else { + newKeywordSets.push( + new EMLKeywordSet({ + parentModel: this.model, + keywords: [keyword], + thesaurus: thesaurus, + }), + ); } - else { - newKeywordSets.push(new EMLKeywordSet({ - parentModel: this.model, - keywords: [keyword], - thesaurus: thesaurus - })); - } - - }, this); - - //Update the EML model - this.model.set("keywordSets", newKeywordSets); - - if (e) { - var row = $(e.target).parent(".keyword-row"); - - //Add a new row when the user has added a new keyword just now - if (row.is(".new")) { - row.removeClass("new"); - row.append(this.createRemoveButton(null, "keywordSets", "div.keyword-row", "div.keywords")); - this.addKeyword(); - } + }, + this, + ); + + //Update the EML model + this.model.set("keywordSets", newKeywordSets); + + if (e) { + var row = $(e.target).parent(".keyword-row"); + + //Add a new row when the user has added a new keyword just now + if (row.is(".new")) { + row.removeClass("new"); + row.append( + this.createRemoveButton( + null, + "keywordSets", + "div.keyword-row", + "div.keywords", + ), + ); + this.addKeyword(); } - }, - - /* - * Update the EML Geo Coverage models and views when the user interacts with the locations section - */ - updateLocations: function (e) { - if (!e) return; - - e.preventDefault(); - - var viewEl = $(e.target).parents(".eml-geocoverage"), - geoCovModel = viewEl.data("model"); + } + }, - //If the EMLGeoCoverage is new - if (viewEl.is(".new")) { + /* + * Update the EML Geo Coverage models and views when the user interacts with the locations section + */ + updateLocations: function (e) { + if (!e) return; - if (this.$(".eml-geocoverage.new").length > 1) - return; + e.preventDefault(); - //Render the new geo coverage view - var newGeo = new EMLGeoCoverageView({ - edit: this.edit, - model: new EMLGeoCoverage({ parentModel: this.model, isNew: true }), - isNew: true - }); - this.$(".locations-table").append(newGeo.render().el); - newGeo.$el.find(".remove-container").append(this.createRemoveButton(null, "geoCoverage", ".eml-geocoverage", ".locations-table")); - - //Unmark the view as new - viewEl.data("view").notNew(); - - //Get the EMLGeoCoverage model attached to this EMlGeoCoverageView - var geoModel = viewEl.data("model"), - //Get the current EMLGeoCoverage models set on the parent EML model - currentCoverages = this.model.get("geoCoverage"); - - //Add this new geo coverage model to the parent EML model - if (Array.isArray(currentCoverages)) { - if (!_.contains(currentCoverages, geoModel)) { - currentCoverages.push(geoModel); - this.model.trigger("change:geoCoverage"); - } - } - else { - currentCoverages = [currentCoverages, geoModel]; - this.model.set("geoCoverage", currentCoverages); - } - } - }, + var viewEl = $(e.target).parents(".eml-geocoverage"), + geoCovModel = viewEl.data("model"); - /* - * If all the EMLGeoCoverage models are valid, remove the error messages for the Locations section - */ - updateLocationsError: function () { - var allValid = _.every(this.model.get("geoCoverage"), function (geoCoverageModel) { - - return geoCoverageModel.isValid(); + //If the EMLGeoCoverage is new + if (viewEl.is(".new")) { + if (this.$(".eml-geocoverage.new").length > 1) return; + //Render the new geo coverage view + var newGeo = new EMLGeoCoverageView({ + edit: this.edit, + model: new EMLGeoCoverage({ parentModel: this.model, isNew: true }), + isNew: true, }); + this.$(".locations-table").append(newGeo.render().el); + newGeo.$el + .find(".remove-container") + .append( + this.createRemoveButton( + null, + "geoCoverage", + ".eml-geocoverage", + ".locations-table", + ), + ); - if (allValid) { - this.$(".side-nav-item.error[data-category='geoCoverage']") - .removeClass("error") - .find(".icon.error").hide(); - this.$(".section[data-section='locations'] .notification.error") - .removeClass("error") - .text(""); - } - - }, + //Unmark the view as new + viewEl.data("view").notNew(); - /* - * Creates the text elements - */ - createEMLText: function (textModel, edit, category) { + //Get the EMLGeoCoverage model attached to this EMlGeoCoverageView + var geoModel = viewEl.data("model"), + //Get the current EMLGeoCoverage models set on the parent EML model + currentCoverages = this.model.get("geoCoverage"); - if (!textModel && edit) { - return $(document.createElement("textarea")) - .attr("data-category", category) - .addClass("xlarge text"); - } - else if (!textModel && !edit) { - return $(document.createElement("div")) - .attr("data-category", category); + //Add this new geo coverage model to the parent EML model + if (Array.isArray(currentCoverages)) { + if (!_.contains(currentCoverages, geoModel)) { + currentCoverages.push(geoModel); + this.model.trigger("change:geoCoverage"); + } + } else { + currentCoverages = [currentCoverages, geoModel]; + this.model.set("geoCoverage", currentCoverages); } + } + }, - //Get the EMLText from the EML model - var finishedEl; - - //Get the text attribute from the EMLText model - var paragraphs = textModel.get("text"), - paragraphsString = ""; - - //If the text should be editable, - if (edit) { - //Format the paragraphs with carriage returns between paragraphs - paragraphsString = paragraphs.join(String.fromCharCode(13)); - - //Create the textarea element - finishedEl = $(document.createElement("textarea")) - .addClass("xlarge text") - .attr("data-category", category) - .html(paragraphsString); - } - else { - //Format the paragraphs with HTML - _.each(paragraphs, function (p) { - paragraphsString += "<p>" + p + "</p>"; - }); + /* + * If all the EMLGeoCoverage models are valid, remove the error messages for the Locations section + */ + updateLocationsError: function () { + var allValid = _.every( + this.model.get("geoCoverage"), + function (geoCoverageModel) { + return geoCoverageModel.isValid(); + }, + ); + + if (allValid) { + this.$(".side-nav-item.error[data-category='geoCoverage']") + .removeClass("error") + .find(".icon.error") + .hide(); + this.$(".section[data-section='locations'] .notification.error") + .removeClass("error") + .text(""); + } + }, - //Create a div - finishedEl = $(document.createElement("div")) - .attr("data-category", category) - .append(paragraphsString); - } + /* + * Creates the text elements + */ + createEMLText: function (textModel, edit, category) { + if (!textModel && edit) { + return $(document.createElement("textarea")) + .attr("data-category", category) + .addClass("xlarge text"); + } else if (!textModel && !edit) { + return $(document.createElement("div")).attr( + "data-category", + category, + ); + } - $(finishedEl).data({ model: textModel }); + //Get the EMLText from the EML model + var finishedEl; - //Return the finished DOM element - return finishedEl; - }, + //Get the text attribute from the EMLText model + var paragraphs = textModel.get("text"), + paragraphsString = ""; - /* - * Updates a basic text field in the EML after the user changes the value - */ - updateText: function (e) { - if (!e) return false; + //If the text should be editable, + if (edit) { + //Format the paragraphs with carriage returns between paragraphs + paragraphsString = paragraphs.join(String.fromCharCode(13)); - var category = $(e.target).attr("data-category"), - currentValue = this.model.get(category), - textModel = $(e.target).data("model"), - value = this.model.cleanXMLText($(e.target).val()); + //Create the textarea element + finishedEl = $(document.createElement("textarea")) + .addClass("xlarge text") + .attr("data-category", category) + .html(paragraphsString); + } else { + //Format the paragraphs with HTML + _.each(paragraphs, function (p) { + paragraphsString += "<p>" + p + "</p>"; + }); - //We can't update anything without a category - if (!category) return false; + //Create a div + finishedEl = $(document.createElement("div")) + .attr("data-category", category) + .append(paragraphsString); + } - //Get the list of paragraphs - checking for carriage returns and line feeds - var paragraphsCR = value.split(String.fromCharCode(13)); - var paragraphsLF = value.split(String.fromCharCode(10)); + $(finishedEl).data({ model: textModel }); - //Use the paragraph list that has the most - var paragraphs = (paragraphsCR > paragraphsLF) ? paragraphsCR : paragraphsLF; + //Return the finished DOM element + return finishedEl; + }, - //If this category isn't set yet, then create a new EMLText model - if (!textModel) { + /* + * Updates a basic text field in the EML after the user changes the value + */ + updateText: function (e) { + if (!e) return false; + + var category = $(e.target).attr("data-category"), + currentValue = this.model.get(category), + textModel = $(e.target).data("model"), + value = this.model.cleanXMLText($(e.target).val()); + + //We can't update anything without a category + if (!category) return false; + + //Get the list of paragraphs - checking for carriage returns and line feeds + var paragraphsCR = value.split(String.fromCharCode(13)); + var paragraphsLF = value.split(String.fromCharCode(10)); + + //Use the paragraph list that has the most + var paragraphs = + paragraphsCR > paragraphsLF ? paragraphsCR : paragraphsLF; + + //If this category isn't set yet, then create a new EMLText model + if (!textModel) { + //Get the current value for this category and create a new EMLText model + var newTextModel = new EMLText({ + text: paragraphs, + parentModel: this.model, + }); - //Get the current value for this category and create a new EMLText model - var newTextModel = new EMLText({ text: paragraphs, parentModel: this.model }); + // Save the new model onto the underlying DOM node + $(e.target).data({ model: newTextModel }); - // Save the new model onto the underlying DOM node - $(e.target).data({ "model": newTextModel }); + //Set the new EMLText model on the EML model + if (Array.isArray(currentValue)) { + currentValue.push(newTextModel); + this.model.trigger("change:" + category); + this.model.trigger("change"); + } else { + this.model.set(category, newTextModel); + } + } + //Update the existing EMLText model + else { + //If there are no paragraphs or all the paragraphs are empty... + if ( + !paragraphs.length || + _.every(paragraphs, function (p) { + return p.trim() == ""; + }) + ) { + //Remove this text model from the array of text models since it is empty + var newValue = _.without(currentValue, textModel); + this.model.set(category, newValue); + } else { + textModel.set("text", paragraphs); + textModel.trigger("change:text"); - //Set the new EMLText model on the EML model - if (Array.isArray(currentValue)) { - currentValue.push(newTextModel); + //Is this text model set on the EML model? + if ( + Array.isArray(currentValue) && + !_.contains(currentValue, textModel) + ) { + //Push this text model into the array of EMLText models + currentValue.push(textModel); this.model.trigger("change:" + category); this.model.trigger("change"); } - else { - this.model.set(category, newTextModel); - } - - } - //Update the existing EMLText model - else { - - //If there are no paragraphs or all the paragraphs are empty... - if (!paragraphs.length || _.every(paragraphs, function (p) { return p.trim() == "" })) { - - //Remove this text model from the array of text models since it is empty - var newValue = _.without(currentValue, textModel); - this.model.set(category, newValue); - - } - else { - - textModel.set("text", paragraphs); - textModel.trigger("change:text"); - - //Is this text model set on the EML model? - if (Array.isArray(currentValue) && !_.contains(currentValue, textModel)) { - - //Push this text model into the array of EMLText models - currentValue.push(textModel); - this.model.trigger("change:" + category); - this.model.trigger("change"); - - } - - } - } + } + }, - }, - - /* - * Creates and returns an array of basic text input field for editing - */ - createBasicTextFields: function (category, placeholder) { - - var textContainer = $(document.createElement("div")).addClass("text-container"), - modelValues = this.model.get(category), - textRow; // Holds the DOM for each field - - //Format as an array - if (!Array.isArray(modelValues) && modelValues) modelValues = [modelValues]; - - //For each value in this category, create an HTML element with the value inserted - _.each(modelValues, function (value, i, allModelValues) { + /* + * Creates and returns an array of basic text input field for editing + */ + createBasicTextFields: function (category, placeholder) { + var textContainer = $(document.createElement("div")).addClass( + "text-container", + ), + modelValues = this.model.get(category), + textRow; // Holds the DOM for each field + + //Format as an array + if (!Array.isArray(modelValues) && modelValues) + modelValues = [modelValues]; + + //For each value in this category, create an HTML element with the value inserted + _.each( + modelValues, + function (value, i, allModelValues) { if (this.edit) { - var textRow = $(document.createElement("div")).addClass("basic-text-row"), + var textRow = $(document.createElement("div")).addClass( + "basic-text-row", + ), input = $(document.createElement("input")) .attr("type", "text") .attr("data-category", category) @@ -1686,338 +1956,396 @@

Source: src/js/views/metadata/EML211View.js

textRow.append(input.clone().val(value)); if (category != "title") - textRow.append(this.createRemoveButton(null, category, 'div.basic-text-row', 'div.text-container')); + textRow.append( + this.createRemoveButton( + null, + category, + "div.basic-text-row", + "div.text-container", + ), + ); textContainer.append(textRow); //At the end, append an empty input for the user to add a new one if (i + 1 == allModelValues.length && category != "title") { - var newRow = $($(document.createElement("div")).addClass("basic-text-row")); - newRow.append(input.clone().addClass("new").attr("placeholder", placeholder || "Add a new " + category)); + var newRow = $( + $(document.createElement("div")).addClass("basic-text-row"), + ); + newRow.append( + input + .clone() + .addClass("new") + .attr( + "placeholder", + placeholder || "Add a new " + category, + ), + ); textContainer.append(newRow); } - - } - else { - textContainer.append($(document.createElement("div")) - .addClass("basic-text-row") - .attr("data-category", category) - .text(value)); + } else { + textContainer.append( + $(document.createElement("div")) + .addClass("basic-text-row") + .attr("data-category", category) + .text(value), + ); } - }, this); - - if ((!modelValues || !modelValues.length) && this.edit) { - var input = $(document.createElement("input")) - .attr("type", "text") - .attr("data-category", category) - .addClass("basic-text new") - .attr("placeholder", placeholder || "Add a new " + category); + }, + this, + ); - textContainer.append($(document.createElement("div")).addClass("basic-text-row").append(input)); - } - - return textContainer; - }, - - updateBasicText: function (e) { - if (!e) return false; - - //Get the category, new value, and model - var category = $(e.target).attr("data-category"), - value = this.model.cleanXMLText($(e.target).val()), - model = $(e.target).data("model") || this.model; - - //We can't update anything without a category - if (!category) return false; - - //Get the current value - var currentValue = model.get(category); - - //Insert the new value into the array - if (Array.isArray(currentValue)) { + if ((!modelValues || !modelValues.length) && this.edit) { + var input = $(document.createElement("input")) + .attr("type", "text") + .attr("data-category", category) + .addClass("basic-text new") + .attr("placeholder", placeholder || "Add a new " + category); + + textContainer.append( + $(document.createElement("div")) + .addClass("basic-text-row") + .append(input), + ); + } - //Find the position this text input is in - var position = $(e.target).parents("div.text-container").first().children("div").index($(e.target).parent()); + return textContainer; + }, - //Set the value in that position in the array - currentValue[position] = value; + updateBasicText: function (e) { + if (!e) return false; - //Set the changed array on this model - model.set(category, currentValue); - model.trigger("change:" + category); + //Get the category, new value, and model + var category = $(e.target).attr("data-category"), + value = this.model.cleanXMLText($(e.target).val()), + model = $(e.target).data("model") || this.model; - } - //Update the model if the current value is a string - else if (typeof currentValue == "string") { - model.set(category, [value]); - model.trigger("change:" + category); - } - else if (!currentValue) { - model.set(category, [value]); - model.trigger("change:" + category); - } + //We can't update anything without a category + if (!category) return false; - //Add another blank text input - if ($(e.target).is(".new") && value != '' && category != "title") { - $(e.target).removeClass("new"); - this.addBasicText(e); - } + //Get the current value + var currentValue = model.get(category); - // Trigger a change on the entire package - MetacatUI.rootDataPackage.packageModel.set("changed", true); + //Insert the new value into the array + if (Array.isArray(currentValue)) { + //Find the position this text input is in + var position = $(e.target) + .parents("div.text-container") + .first() + .children("div") + .index($(e.target).parent()); - }, + //Set the value in that position in the array + currentValue[position] = value; - /* One-off handler for updating pubDate on the model when the form - input changes. Fairly similar but just a pared down version of - updateBasicText. */ - updatePubDate: function (e) { - if (!e) return false; + //Set the changed array on this model + model.set(category, currentValue); + model.trigger("change:" + category); + } + //Update the model if the current value is a string + else if (typeof currentValue == "string") { + model.set(category, [value]); + model.trigger("change:" + category); + } else if (!currentValue) { + model.set(category, [value]); + model.trigger("change:" + category); + } - this.model.set('pubDate', $(e.target).val().trim()); - this.model.trigger("change"); + //Add another blank text input + if ($(e.target).is(".new") && value != "" && category != "title") { + $(e.target).removeClass("new"); + this.addBasicText(e); + } - // Trigger a change on the entire package - MetacatUI.rootDataPackage.packageModel.set("changed", true); - }, + // Trigger a change on the entire package + MetacatUI.rootDataPackage.packageModel.set("changed", true); + }, - /* - * Adds a basic text input - */ - addBasicText: function (e) { - var category = $(e.target).attr("data-category"), - allBasicTexts = $(".basic-text.new[data-category='" + category + "']"); + /* One-off handler for updating pubDate on the model when the form + input changes. Fairly similar but just a pared down version of + updateBasicText. */ + updatePubDate: function (e) { + if (!e) return false; - //Only show one new row at a time - if ((allBasicTexts.length == 1) && !allBasicTexts.val()) - return; - else if (allBasicTexts.length > 1) - return; - //We are only supporting one title right now - else if (category == "title") - return; + this.model.set("pubDate", $(e.target).val().trim()); + this.model.trigger("change"); - //Add another blank text input - var newRow = $(document.createElement("div")).addClass("basic-text-row"); + // Trigger a change on the entire package + MetacatUI.rootDataPackage.packageModel.set("changed", true); + }, - newRow.append($(document.createElement("input")) + /* + * Adds a basic text input + */ + addBasicText: function (e) { + var category = $(e.target).attr("data-category"), + allBasicTexts = $( + ".basic-text.new[data-category='" + category + "']", + ); + + //Only show one new row at a time + if (allBasicTexts.length == 1 && !allBasicTexts.val()) return; + else if (allBasicTexts.length > 1) return; + //We are only supporting one title right now + else if (category == "title") return; + + //Add another blank text input + var newRow = $(document.createElement("div")).addClass( + "basic-text-row", + ); + + newRow.append( + $(document.createElement("input")) .attr("type", "text") .attr("data-category", category) .attr("placeholder", $(e.target).attr("placeholder")) - .addClass("new basic-text")); - - $(e.target).parent().after(newRow); - - $(e.target).after(this.createRemoveButton(null, category, '.basic-text-row', "div.text-container")); - }, - - previewTextRemove: function (e) { - $(e.target).parents(".basic-text-row").toggleClass("remove-preview"); - }, + .addClass("new basic-text"), + ); + + $(e.target).parent().after(newRow); + + $(e.target).after( + this.createRemoveButton( + null, + category, + ".basic-text-row", + "div.text-container", + ), + ); + }, - // publication date validation. - isDateFormatValid: function (dateString) { + previewTextRemove: function (e) { + $(e.target).parents(".basic-text-row").toggleClass("remove-preview"); + }, - //Date strings that are four characters should be a full year. Make sure all characters are numbers - if (dateString.length == 4) { - var digits = dateString.match(/[0-9]/g); - return (digits.length == 4) - } - //Date strings that are 10 characters long should be a valid date - else { - var dateParts = dateString.split("-"); - - if (dateParts.length != 3 || dateParts[0].length != 4 || dateParts[1].length != 2 || dateParts[2].length != 2) - return false; - - dateYear = dateParts[0]; - dateMonth = dateParts[1]; - dateDay = dateParts[2]; - - // Validating the values for the date and month if in YYYY-MM-DD format. - if (dateMonth < 1 || dateMonth > 12) - return false; - else if (dateDay < 1 || dateDay > 31) - return false; - else if ((dateMonth == 4 || dateMonth == 6 || dateMonth == 9 || dateMonth == 11) && dateDay == 31) - return false; - else if (dateMonth == 2) { - // Validation for leap year dates. - var isleap = (dateYear % 4 == 0 && (dateYear % 100 != 0 || dateYear % 400 == 0)); - if ((dateDay > 29) || (dateDay == 29 && !isleap)) - return false; - } + // publication date validation. + isDateFormatValid: function (dateString) { + //Date strings that are four characters should be a full year. Make sure all characters are numbers + if (dateString.length == 4) { + var digits = dateString.match(/[0-9]/g); + return digits.length == 4; + } + //Date strings that are 10 characters long should be a valid date + else { + var dateParts = dateString.split("-"); - var digits = _.filter(dateParts, function (part) { - return (part.match(/[0-9]/g).length == part.length); - }); + if ( + dateParts.length != 3 || + dateParts[0].length != 4 || + dateParts[1].length != 2 || + dateParts[2].length != 2 + ) + return false; + + dateYear = dateParts[0]; + dateMonth = dateParts[1]; + dateDay = dateParts[2]; + + // Validating the values for the date and month if in YYYY-MM-DD format. + if (dateMonth < 1 || dateMonth > 12) return false; + else if (dateDay < 1 || dateDay > 31) return false; + else if ( + (dateMonth == 4 || + dateMonth == 6 || + dateMonth == 9 || + dateMonth == 11) && + dateDay == 31 + ) + return false; + else if (dateMonth == 2) { + // Validation for leap year dates. + var isleap = + dateYear % 4 == 0 && (dateYear % 100 != 0 || dateYear % 400 == 0); + if (dateDay > 29 || (dateDay == 29 && !isleap)) return false; + } + + var digits = _.filter(dateParts, function (part) { + return part.match(/[0-9]/g).length == part.length; + }); - return (digits.length == 3); - } - }, + return digits.length == 3; + } + }, - /* Event handler for showing validation messaging for the pubDate input + /* Event handler for showing validation messaging for the pubDate input which has to conform to the EML yearDate type (YYYY or YYYY-MM-DD) */ - showPubDateValidation: function (e) { - var container = $(e.target).parents(".pubDate").first(), - input = $(e.target), - messageEl = $(container).find('.notification'), - value = input.val(), - errors = []; - - // Remove existing error borders and notifications - input.removeClass("error"); - messageEl.text(""); - messageEl.removeClass("error"); - - if (value != "" && value.length > 0) { - if (!this.isDateFormatValid(value)) { - errors.push("The value entered for publication date, '" + + showPubDateValidation: function (e) { + var container = $(e.target).parents(".pubDate").first(), + input = $(e.target), + messageEl = $(container).find(".notification"), + value = input.val(), + errors = []; + + // Remove existing error borders and notifications + input.removeClass("error"); + messageEl.text(""); + messageEl.removeClass("error"); + + if (value != "" && value.length > 0) { + if (!this.isDateFormatValid(value)) { + errors.push( + "The value entered for publication date, '" + value + - "' is not a valid value for this field. Enter either a year (e.g. 2017) or a date in the format YYYY-MM-DD."); - - input.addClass("error") - } - } + "' is not a valid value for this field. Enter either a year (e.g. 2017) or a date in the format YYYY-MM-DD.", + ); - if (errors.length > 0) { - messageEl.text(errors[0]).addClass("error"); + input.addClass("error"); } - }, - - // Creates a table to hold a single EMLTaxonCoverage element (table) for - // each root-level taxonomicClassification - createTaxonomicCoverage: function (coverage) { - var finishedEls = $(this.taxonomicCoverageTemplate({ - generalTaxonomicCoverage: coverage.get('generalTaxonomicCoverage') || "" - })), - coverageEl = finishedEls.filter(".taxonomic-coverage"); + } - coverageEl.data({ model: coverage }); + if (errors.length > 0) { + messageEl.text(errors[0]).addClass("error"); + } + }, - var classifications = coverage.get("taxonomicClassification"); + // Creates a table to hold a single EMLTaxonCoverage element (table) for + // each root-level taxonomicClassification + createTaxonomicCoverage: function (coverage) { + var finishedEls = $( + this.taxonomicCoverageTemplate({ + generalTaxonomicCoverage: + coverage.get("generalTaxonomicCoverage") || "", + }), + ), + coverageEl = finishedEls.filter(".taxonomic-coverage"); + + coverageEl.data({ model: coverage }); + + var classifications = coverage.get("taxonomicClassification"); + + // Makes a table... for the root level + for (var i = 0; i < classifications.length; i++) { + coverageEl.append( + this.createTaxonomicClassificationTable(classifications[i]), + ); + } - // Makes a table... for the root level - for (var i = 0; i < classifications.length; i++) { - coverageEl.append( - this.createTaxonomicClassificationTable(classifications[i]) - ); - } + // Create a new, blank table for another taxonomicClassification + var newTableEl = this.createTaxonomicClassificationTable(); - // Create a new, blank table for another taxonomicClassification - var newTableEl = this.createTaxonomicClassificationTable(); + coverageEl.append(newTableEl); - coverageEl.append(newTableEl); + return finishedEls; + }, - return finishedEls; - }, + createTaxonomicClassificationTable: function (classification) { + // updating the taxonomic table indexes before adding a new table to the page. + var taxaNums = this.$(".editor-header-index"); + for (var i = 0; i < taxaNums.length; i++) { + $(taxaNums[i]).text(i + 1); + } - createTaxonomicClassificationTable: function (classification) { + // Adding the taxoSpeciesCounter to the table header for enhancement of the view + var finishedEl = $( + '<div class="row-striped root-taxonomic-classification-container"></div>', + ); + $(finishedEl).append( + '<h6>Species <span class="editor-header-index">' + + (taxaNums.length + 1) + + "</span> </h6>", + ); + + // Add a remove button if this is not a new table + if (!(typeof classification === "undefined")) { + $(finishedEl).append( + this.createRemoveButton( + "taxonCoverage", + "taxonomicClassification", + ".root-taxonomic-classification-container", + ".taxonomic-coverage", + ), + ); + } - // updating the taxonomic table indexes before adding a new table to the page. - var taxaNums = this.$(".editor-header-index"); - for (var i = 0; i < taxaNums.length; i++) { - $(taxaNums[i]).text(i + 1); - } + var tableEl = $(this.taxonomicClassificationTableTemplate()); + var tableBodyEl = $(document.createElement("tbody")); - // Adding the taxoSpeciesCounter to the table header for enhancement of the view - var finishedEl = $('<div class="row-striped root-taxonomic-classification-container"></div>'); - $(finishedEl).append('<h6>Species <span class="editor-header-index">' + (taxaNums.length + 1) + '</span> </h6>'); + var queue = [classification], + rows = [], + cur; + while (queue.length > 0) { + cur = queue.pop(); - // Add a remove button if this is not a new table - if (!(typeof classification === "undefined")) { - $(finishedEl).append(this.createRemoveButton('taxonCoverage', 'taxonomicClassification', '.root-taxonomic-classification-container', '.taxonomic-coverage')); + // I threw this in here so I can this function without an + // argument to generate a new table from scratch + if (typeof cur === "undefined") { + continue; } + cur.taxonRankName = cur.taxonRankName?.toLowerCase(); + rows.push(cur); + if (cur.taxonomicClassification) { + for (var i = 0; i < cur.taxonomicClassification.length; i++) { + queue.push(cur.taxonomicClassification[i]); + } + } + } + for (var j = 0; j < rows.length; j++) { + tableBodyEl.append(this.makeTaxonomicClassificationRow(rows[j])); + } - var tableEl = $(this.taxonomicClassificationTableTemplate()); - var tableBodyEl = $(document.createElement("tbody")); - - var queue = [classification], - rows = [], - cur; - - while (queue.length > 0) { - cur = queue.pop(); + var newRowEl = this.makeNewTaxonomicClassificationRow(); - // I threw this in here so I can this function without an - // argument to generate a new table from scratch - if (typeof cur === "undefined") { - continue; - } - cur.taxonRankName = cur.taxonRankName?.toLowerCase() - rows.push(cur); - if (cur.taxonomicClassification) { - for (var i = 0; i < cur.taxonomicClassification.length; i++) { - queue.push(cur.taxonomicClassification[i]); - } - } - } + $(tableBodyEl).append(newRowEl); + $(tableEl).append(tableBodyEl); - for (var j = 0; j < rows.length; j++) { - tableBodyEl.append(this.makeTaxonomicClassificationRow(rows[j])); - } + // Add the new class to the entire table if it's a new one + if (typeof classification === "undefined") { + $(tableEl).addClass("new"); + } - var newRowEl = this.makeNewTaxonomicClassificationRow(); + $(finishedEl).append(tableEl); - $(tableBodyEl).append(newRowEl); - $(tableEl).append(tableBodyEl); + return finishedEl; + }, - // Add the new class to the entire table if it's a new one - if (typeof classification === "undefined") { - $(tableEl).addClass("new"); + /** + * Create the HTML for a single row in a taxonomicClassification table + * @param {EMLTaxonCoverage#taxonomicClassification} classification A + * classification object from an EMLTaxonCoverage model, may include + * a taxonRank, taxonValue, taxonId, commonName, and nested + * taxonomicClassification objects + * @returns {jQuery} A jQuery object containing the HTML for a single + * row in a taxonomicClassification table + * @since 2.24.0 + */ + makeTaxonomicClassificationRow: function (classification) { + try { + if (!classification) classification = {}; + var finishedEl = $( + this.taxonomicClassificationRowTemplate({ + taxonRankName: classification.taxonRankName || "", + taxonRankValue: classification.taxonRankValue || "", + }), + ); + // Save a reference to other taxon attributes that we need to keep + // when serializing the model + if (classification.taxonId) { + $(finishedEl).data("taxonId", classification.taxonId); + } + if (classification.commonName) { + $(finishedEl).data("commonName", classification.commonName); } + return finishedEl; + } catch (e) { + console.log("Error making taxonomic classification row: ", e); + } + }, - $(finishedEl).append(tableEl); + /** + * Create the HTML for a new row in a taxonomicClassification table + * @returns {jQuery} A jQuery object containing the HTML for a new row + * in a taxonomicClassification table + * @since 2.24.0 + */ + makeNewTaxonomicClassificationRow: function () { + const row = this.makeTaxonomicClassificationRow({}); + $(row).addClass("new"); + return row; + }, - return finishedEl; - }, - - /** - * Create the HTML for a single row in a taxonomicClassification table - * @param {EMLTaxonCoverage#taxonomicClassification} classification A - * classification object from an EMLTaxonCoverage model, may include - * a taxonRank, taxonValue, taxonId, commonName, and nested - * taxonomicClassification objects - * @returns {jQuery} A jQuery object containing the HTML for a single - * row in a taxonomicClassification table - * @since 2.24.0 - */ - makeTaxonomicClassificationRow: function (classification) { - try { - if (!classification) classification = {}; - var finishedEl = $(this.taxonomicClassificationRowTemplate({ - taxonRankName: classification.taxonRankName || '', - taxonRankValue: classification.taxonRankValue || '' - })); - // Save a reference to other taxon attributes that we need to keep - // when serializing the model - if (classification.taxonId) { - $(finishedEl).data("taxonId", classification.taxonId); - } - if (classification.commonName) { - $(finishedEl).data("commonName", classification.commonName); - } - return finishedEl; - } catch (e) { - console.log("Error making taxonomic classification row: ", e); - } - }, - - /** - * Create the HTML for a new row in a taxonomicClassification table - * @returns {jQuery} A jQuery object containing the HTML for a new row - * in a taxonomicClassification table - * @since 2.24.0 - */ - makeNewTaxonomicClassificationRow: function () { - const row = this.makeTaxonomicClassificationRow({}); - $(row).addClass("new"); - return row; - }, - - /* Update the underlying model and DOM for an EML TaxonomicCoverage + /* Update the underlying model and DOM for an EML TaxonomicCoverage section. This method handles updating the underlying TaxonomicCoverage models when the user changes form fields as well as inserting new form fields automatically when the user needs them. @@ -2029,19 +2357,17 @@

Source: src/js/views/metadata/EML211View.js

TODO: Finish this function TODO: Link this function into the DOM */ - updateTaxonCoverage: function (options) { - - if (options.target) { + updateTaxonCoverage: function (options) { + if (options.target) { + // Ignore the event if the target is a quick add taxon UI element. + const quickAddEl = $(this.taxonQuickAddEl); + if (quickAddEl && quickAddEl.has(options.target).length) { + return; + } - // Ignore the event if the target is a quick add taxon UI element. - const quickAddEl = $(this.taxonQuickAddEl) - if (quickAddEl && quickAddEl.has(options.target).length) { - return - } - - var e = options; + var e = options; - /* Getting `model` here is different than in other places because + /* Getting `model` here is different than in other places because the thing being updated is an `input` or `select` element which is part of a `taxonomicClassification`. The model is `TaxonCoverage` which has one or more @@ -2049,176 +2375,187 @@

Source: src/js/views/metadata/EML211View.js

hierarchy from input < td < tr < tbody < table < div to get at the underlying TaxonCoverage model. */ - var coverage = $(e.target).parents(".taxonomic-coverage"), - classificationEl = $(e.target).parents(".root-taxonomic-classification"), - model = $(coverage).data("model") || this.model, - category = $(e.target).attr("data-category"), - value = this.model.cleanXMLText($(e.target).val()); - - //We can't update anything without a coverage, or - //classification - if (!coverage) return false; - if (!classificationEl) return false; - - // Use `category` to determine if we're updating the generalTaxonomicCoverage or - // the taxonomicClassification - if (category && category === "generalTaxonomicCoverage") { - model.set('generalTaxonomicCoverage', value); - - return; - } - } - else { - var coverage = options.coverage, - model = $(coverage).data("model"); - } + var coverage = $(e.target).parents(".taxonomic-coverage"), + classificationEl = $(e.target).parents( + ".root-taxonomic-classification", + ), + model = $(coverage).data("model") || this.model, + category = $(e.target).attr("data-category"), + value = this.model.cleanXMLText($(e.target).val()); - // Find all of the root-level taxonomicClassifications - var classificationTables = $(coverage).find(".root-taxonomic-classification"); + //We can't update anything without a coverage, or + //classification + if (!coverage) return false; + if (!classificationEl) return false; - if (!classificationTables) return false; + // Use `category` to determine if we're updating the generalTaxonomicCoverage or + // the taxonomicClassification + if (category && category === "generalTaxonomicCoverage") { + model.set("generalTaxonomicCoverage", value); - //TODO :This should probably (at least) be in its own View and - //definitely refactored into tidy functions.*/ + return; + } + } else { + var coverage = options.coverage, + model = $(coverage).data("model"); + } - var rows, - collectedClassifications = []; + // Find all of the root-level taxonomicClassifications + var classificationTables = $(coverage).find( + ".root-taxonomic-classification", + ); - for (var i = 0; i < classificationTables.length; i++) { + if (!classificationTables) return false; - rows = $(classificationTables[i]).find("tbody tr"); + //TODO :This should probably (at least) be in its own View and + //definitely refactored into tidy functions.*/ - if (!rows) continue; + var rows, + collectedClassifications = []; - var topLevelClassification = {}, - classification = topLevelClassification, - currentRank, - currentValue; + for (var i = 0; i < classificationTables.length; i++) { + rows = $(classificationTables[i]).find("tbody tr"); - for (var j = 0; j < rows.length; j++) { + if (!rows) continue; - const thisRow = rows[j]; + var topLevelClassification = {}, + classification = topLevelClassification, + currentRank, + currentValue; - currentRank = this.model.cleanXMLText($(thisRow).find("select").val()) || ""; - currentValue = this.model.cleanXMLText($(thisRow).find("input").val()) || ""; + for (var j = 0; j < rows.length; j++) { + const thisRow = rows[j]; - // Maintain classification attributes that exist in the EML but are not visible in the editor - const taxonId = $(thisRow).data("taxonId") - const commonName = $(thisRow).data("commonName") + currentRank = + this.model.cleanXMLText($(thisRow).find("select").val()) || ""; + currentValue = + this.model.cleanXMLText($(thisRow).find("input").val()) || ""; - // Skip over rows with empty Rank or Value - if (!currentRank.length || !currentValue.length) { - continue; - } + // Maintain classification attributes that exist in the EML but are not visible in the editor + const taxonId = $(thisRow).data("taxonId"); + const commonName = $(thisRow).data("commonName"); - //After the first row, start nesting taxonomicClassification objects - if (j > 0) { - classification.taxonomicClassification = [{}]; - classification = classification.taxonomicClassification[0]; - } + // Skip over rows with empty Rank or Value + if (!currentRank.length || !currentValue.length) { + continue; + } - // Add it to the classification object - classification.taxonRankName = currentRank; - classification.taxonRankValue = currentValue; - classification.taxonId = taxonId; - classification.commonName = commonName; + //After the first row, start nesting taxonomicClassification objects + if (j > 0) { + classification.taxonomicClassification = [{}]; + classification = classification.taxonomicClassification[0]; + } + // Add it to the classification object + classification.taxonRankName = currentRank; + classification.taxonRankValue = currentValue; + classification.taxonId = taxonId; + classification.commonName = commonName; + } - } + //Add the top level classification to the array + if (Object.keys(topLevelClassification).length) + collectedClassifications.push(topLevelClassification); + } + if ( + !_.isEqual( + collectedClassifications, + model.get("taxonomicClassification"), + ) + ) { + model.set("taxonomicClassification", collectedClassifications); + this.model.trigger("change"); + } - //Add the top level classification to the array - if (Object.keys(topLevelClassification).length) - collectedClassifications.push(topLevelClassification); + // Handle adding new tables and rows + // Do nothing if the value isn't set + if (value) { + // Add a new row if this is itself a new row + if ($(e.target).parents("tr").first().is(".new")) { + var newRowEl = this.makeNewTaxonomicClassificationRow(); + $(e.target).parents("tbody").first().append(newRowEl); + $(e.target).parents("tr").first().removeClass("new"); + } + + // Add a new classification table if this is itself a new table + if ($(classificationEl).is(".new")) { + $(classificationEl).removeClass("new"); + $(classificationEl).append( + this.createRemoveButton( + "taxonCoverage", + "taxonomicClassification", + ".root-taxonomic-classification-container", + ".taxonomic-coverage", + ), + ); + $(coverage).append(this.createTaxonomicClassificationTable()); } + } - if (!(_.isEqual(collectedClassifications, model.get('taxonomicClassification')))) { - model.set('taxonomicClassification', collectedClassifications); - this.model.trigger("change"); - } + // update the quick add interface + this.updateQuickAddTaxa(); + }, - // Handle adding new tables and rows - // Do nothing if the value isn't set - if (value) { - // Add a new row if this is itself a new row - if ($(e.target).parents("tr").first().is(".new")) { - var newRowEl = this.makeNewTaxonomicClassificationRow(); - $(e.target).parents("tbody").first().append(newRowEl); - $(e.target).parents("tr").first().removeClass("new"); - } + /** + * Update the options for the quick add taxon select interface. This + * ensures that only taxonomic classifications that are not already + * included in the taxonomic coverage are available for selection. + * @since 2.24.0 + */ + updateQuickAddTaxa: function () { + const selects = this.taxonSelects; + if (!selects || !selects.length) return; + const taxa = this.getTaxonQuickAddOptions(); + if (!taxa || !taxa.length) return; + selects.forEach((select, i) => { + select.updateOptions(taxa[i].options); + }); + }, - // Add a new classification table if this is itself a new table - if ($(classificationEl).is(".new")) { - $(classificationEl).removeClass("new"); - $(classificationEl).append(this.createRemoveButton('taxonCoverage', 'taxonomicClassification', '.root-taxonomic-classification-container', '.taxonomic-coverage')); - $(coverage).append(this.createTaxonomicClassificationTable()); - } - } + /* + * Adds a new row and/or table to the taxonomic coverage section + */ + addNewTaxon: function (e) { + // Don't do anything if the current classification doesn't have new content + if ($(e.target).val().trim() === "") return; + + // If the row is new, add a new row to the table + if ($(e.target).parents("tr").is(".new")) { + var newRow = this.makeNewTaxonomicClassificationRow(); + //Append the new row and remove the new class from the old row + $(e.target).parents("tr").removeClass("new").after(newRow); + } + }, - // update the quick add interface - this.updateQuickAddTaxa() - }, - - /** - * Update the options for the quick add taxon select interface. This - * ensures that only taxonomic classifications that are not already - * included in the taxonomic coverage are available for selection. - * @since 2.24.0 - */ - updateQuickAddTaxa: function () { - const selects = this.taxonSelects - if (!selects || !selects.length) return - const taxa = this.getTaxonQuickAddOptions() - if (!taxa || !taxa.length) return - selects.forEach((select, i) => { - select.updateOptions(taxa[i].options) - }) - }, - - /* - * Adds a new row and/or table to the taxonomic coverage section - */ - addNewTaxon: function (e) { - // Don't do anything if the current classification doesn't have new content - if ($(e.target).val().trim() === "") return; - - // If the row is new, add a new row to the table - if ($(e.target).parents("tr").is(".new")) { - var newRow = this.makeNewTaxonomicClassificationRow(); - //Append the new row and remove the new class from the old row - $(e.target).parents("tr").removeClass("new").after(newRow); + /** + * Insert the "quick add" interface for adding common taxa to the + * taxonomic coverage section. Only renders if there is a list of taxa + * configured in the appModel. + */ + renderTaxaQuickAdd: function () { + try { + const view = this; + // To render the taxon select, the view must be in editor mode and we + // need a list of taxa configured for the theme + if (!view.edit) return; + // remove any existing quick add interface: + if (view.taxonQuickAddEl) view.taxonQuickAddEl.remove(); + + const quickAddTaxa = view.getTaxonQuickAddOptions(); + if (!quickAddTaxa || !quickAddTaxa.length) { + // If the taxa are configured as SID for a dataObject, then wait + // for the dataObject to be loaded + this.listenToOnce( + MetacatUI.appModel, + "change:quickAddTaxa", + this.renderTaxaQuickAdd, + ); + return; } - }, - - /** - * Insert the "quick add" interface for adding common taxa to the - * taxonomic coverage section. Only renders if there is a list of taxa - * configured in the appModel. - */ - renderTaxaQuickAdd: function () { - try { - - const view = this; - // To render the taxon select, the view must be in editor mode and we - // need a list of taxa configured for the theme - if (!view.edit) return - // remove any existing quick add interface: - if (view.taxonQuickAddEl) view.taxonQuickAddEl.remove() - - const quickAddTaxa = view.getTaxonQuickAddOptions() - if (!quickAddTaxa || !quickAddTaxa.length) { - // If the taxa are configured as SID for a dataObject, then wait - // for the dataObject to be loaded - this.listenToOnce( - MetacatUI.appModel, - "change:quickAddTaxa", - this.renderTaxaQuickAdd - ) - return - } - // Create & insert the basic HTML for the taxon select interface - const template = `<div class="taxa-quick-add"> + // Create & insert the basic HTML for the taxon select interface + const template = `<div class="taxa-quick-add"> <p class="taxa-quick-add__text"> <b>⭐️ Quick Add Taxa:</b> Select one or more common taxa. Click "Add" to add them to the list. </p> @@ -2226,134 +2563,145 @@

Source: src/js/views/metadata/EML211View.js

<div class="taxa-quick-add__selects"></div> <button class="btn btn-primary taxa-quick-add__button">Add Taxa</button> </div> - </div>` - const parser = new DOMParser() - const doc = parser.parseFromString(template, "text/html") - const quickAddEl = doc.body.firstChild - const button = quickAddEl.querySelector("button") - const container = quickAddEl.querySelector(".taxa-quick-add__selects") - const rowSelector = ".root-taxonomic-classification-container" - const firstRow = document.querySelector(rowSelector) - firstRow.parentNode.insertBefore(quickAddEl, firstRow) - view.taxonQuickAddEl = quickAddEl - - // Update the taxon coverage when the button is clicked - const onButtonClick = () => { - const taxonSelects = view.taxonSelects - if (!taxonSelects || !taxonSelects.length) return - const selectedItems = taxonSelects.map(select => select.selected).flat() - if (!selectedItems || !selectedItems.length) return - const selectedItemObjs = selectedItems.map((item) => { - try { - // It will be encoded JSON if it's a pre-defined taxon - return JSON.parse(decodeURIComponent(item)) - } catch (e) { - // Otherwise it will be a string a user typed in - return { - taxonRankName: "", - taxonRankValue: item - } - } - }) - view.addTaxa(selectedItemObjs) - taxonSelects.forEach(select => select.changeSelection([], true)) - } - button.removeEventListener("click", onButtonClick) - button.addEventListener("click", onButtonClick); - - // Create the search selects - view.taxonSelects = [] - const componentPath = 'views/searchSelect/SearchableSelectView' - require([componentPath], function (SearchSelect) { - quickAddTaxa.forEach((taxaList, i) => { - try { - const taxaInput = new SearchSelect({ - options: taxaList.options, - placeholderText: taxaList.placeholder, - inputLabel: taxaList.label, - allowMulti: true, - allowAdditions: true, - separatorTextOptions: false, - selected: [] - }) - container.appendChild(taxaInput.el) - taxaInput.render() - view.taxonSelects.push(taxaInput) - } catch (e) { - console.log("Failed to create taxon select: ", e) - } - }) - }) - - }catch(e){ - console.log("Failed to render taxon select: ", e) - } - }, - - /** - * Get the list of options for the taxon quick add interface. Filter - * out any that have already been added to the taxonomic coverage. - * @returns {Object[]} An array of search select options - * @since 2.24.0 - */ - getTaxonQuickAddOptions: function () { - const quickAddTaxa = MetacatUI.appModel.getQuickAddTaxa() - if (!quickAddTaxa || !quickAddTaxa.length) return - const coverages = this.model.get("taxonCoverage") - for (const taxaList of quickAddTaxa) { - const opts = [] - for (const taxon of taxaList.taxa) { - // check that it is not a duplicate in any coverages - let isDuplicate = false - for (cov of coverages) { - if (cov.isDuplicate(taxon)) { - isDuplicate = true - break - } + </div>`; + const parser = new DOMParser(); + const doc = parser.parseFromString(template, "text/html"); + const quickAddEl = doc.body.firstChild; + const button = quickAddEl.querySelector("button"); + const container = quickAddEl.querySelector( + ".taxa-quick-add__selects", + ); + const rowSelector = ".root-taxonomic-classification-container"; + const firstRow = document.querySelector(rowSelector); + firstRow.parentNode.insertBefore(quickAddEl, firstRow); + view.taxonQuickAddEl = quickAddEl; + + // Update the taxon coverage when the button is clicked + const onButtonClick = () => { + const taxonSelects = view.taxonSelects; + if (!taxonSelects || !taxonSelects.length) return; + const selectedItems = taxonSelects + .map((select) => select.selected) + .flat(); + if (!selectedItems || !selectedItems.length) return; + const selectedItemObjs = selectedItems.map((item) => { + try { + // It will be encoded JSON if it's a pre-defined taxon + return JSON.parse(decodeURIComponent(item)); + } catch (e) { + // Otherwise it will be a string a user typed in + return { + taxonRankName: "", + taxonRankValue: item, + }; } - if (!isDuplicate) { - opts.push(this.taxonOptionToSearchSelectItem(taxon)) + }); + view.addTaxa(selectedItemObjs); + taxonSelects.forEach((select) => select.changeSelection([], true)); + }; + button.removeEventListener("click", onButtonClick); + button.addEventListener("click", onButtonClick); + + // Create the search selects + view.taxonSelects = []; + const componentPath = "views/searchSelect/SearchableSelectView"; + require([componentPath], function (SearchSelect) { + quickAddTaxa.forEach((taxaList, i) => { + try { + const taxaInput = new SearchSelect({ + options: taxaList.options, + placeholderText: taxaList.placeholder, + inputLabel: taxaList.label, + allowMulti: true, + allowAdditions: true, + separatorTextOptions: false, + selected: [], + }); + container.appendChild(taxaInput.el); + taxaInput.render(); + view.taxonSelects.push(taxaInput); + } catch (e) { + console.log("Failed to create taxon select: ", e); + } + }); + }); + } catch (e) { + console.log("Failed to render taxon select: ", e); + } + }, + + /** + * Get the list of options for the taxon quick add interface. Filter + * out any that have already been added to the taxonomic coverage. + * @returns {Object[]} An array of search select options + * @since 2.24.0 + */ + getTaxonQuickAddOptions: function () { + const quickAddTaxa = MetacatUI.appModel.getQuickAddTaxa(); + if (!quickAddTaxa || !quickAddTaxa.length) return; + const coverages = this.model.get("taxonCoverage"); + for (const taxaList of quickAddTaxa) { + const opts = []; + for (const taxon of taxaList.taxa) { + // check that it is not a duplicate in any coverages + let isDuplicate = false; + for (cov of coverages) { + if (cov.isDuplicate(taxon)) { + isDuplicate = true; + break; } } - taxaList.options = opts - } - return quickAddTaxa - }, - - /** - * Reformats a taxon option, as provided in the appModel - * {@link AppModel#quickAddTaxa}, as a search select item. - * @param {Object} option A single taxon classification with at least a - * taxonRankValue and taxonRankName. It may also have a taxonId (object - * with provider and value) and a commonName. - * @returns {Object} A search select item with label, value, and - * description properties. - */ - taxonOptionToSearchSelectItem: function (option) { - try { - // option must have a taxonRankValue and taxonRankName or it is invalid - if (!option.taxonRankValue || !option.taxonRankName) { - console.log("Invalid taxon option: ", option); - return null; - } - // Create a description - let description = option.taxonRankName + ": " + option.taxonRankValue - if (option.taxonId) { - description += " (" + option.taxonId.provider + ": " + option.taxonId.value + ")" - } - // search select doesn't work with some of the json characters - const val = encodeURIComponent(JSON.stringify(option)); - return { - label: option.commonName || option.taxonRankValue, - value: val, - description: description + if (!isDuplicate) { + opts.push(this.taxonOptionToSearchSelectItem(taxon)); } - } catch (e) { - console.log("Failed to reformat taxon option as search select item: ", e); + } + taxaList.options = opts; + } + return quickAddTaxa; + }, + + /** + * Reformats a taxon option, as provided in the appModel + * {@link AppModel#quickAddTaxa}, as a search select item. + * @param {Object} option A single taxon classification with at least a + * taxonRankValue and taxonRankName. It may also have a taxonId (object + * with provider and value) and a commonName. + * @returns {Object} A search select item with label, value, and + * description properties. + */ + taxonOptionToSearchSelectItem: function (option) { + try { + // option must have a taxonRankValue and taxonRankName or it is invalid + if (!option.taxonRankValue || !option.taxonRankName) { + console.log("Invalid taxon option: ", option); return null; } - }, - + // Create a description + let description = option.taxonRankName + ": " + option.taxonRankValue; + if (option.taxonId) { + description += + " (" + + option.taxonId.provider + + ": " + + option.taxonId.value + + ")"; + } + // search select doesn't work with some of the json characters + const val = encodeURIComponent(JSON.stringify(option)); + return { + label: option.commonName || option.taxonRankValue, + value: val, + description: description, + }; + } catch (e) { + console.log( + "Failed to reformat taxon option as search select item: ", + e, + ); + return null; + } + }, + /** * Add new taxa to the EML model and re-render the taxa section. The new * taxa will be added to the first <taxonomicCoverage> element in the EML @@ -2380,7 +2728,7 @@

Source: src/js/views/metadata/EML211View.js

* value: "202423" * }]); */ - addTaxa: function (newClassifications) { + addTaxa: function (newClassifications) { try { // TODO: validate the new taxon before adding it to the model? const taxonCoverages = this.model.get("taxonCoverage"); @@ -2389,11 +2737,13 @@

Source: src/js/views/metadata/EML211View.js

// <taxonomicCoverage> element. Add the new taxon to its // <taxonomicClassification> array. If there is more than one, then the // new taxon will be added to the first <taxonomicCoverage> element. - if (taxonCoverages && taxonCoverages.length >= 1){ - const taxonCoverage = taxonCoverages[0]; - const classifications = taxonCoverage.get("taxonomicClassification"); - const allClass = classifications.concat(newClassifications); - taxonCoverage.set("taxonomicClassification", allClass); + if (taxonCoverages && taxonCoverages.length >= 1) { + const taxonCoverage = taxonCoverages[0]; + const classifications = taxonCoverage.get( + "taxonomicClassification", + ); + const allClass = classifications.concat(newClassifications); + taxonCoverage.set("taxonomicClassification", allClass); } else { // If there is no <taxonomicCoverage> element for some reason, // create one and add the new taxon to its <taxonomicClassification> @@ -2411,418 +2761,422 @@

Source: src/js/views/metadata/EML211View.js

} }, - removeTaxonRank: function (e) { - var row = $(e.target).parents(".taxonomic-coverage-row"), - coverageEl = $(row).parents(".taxonomic-coverage"), - view = this; - - //Animate the row away and then remove it - row.slideUp("fast", function () { - row.remove(); - view.updateTaxonCoverage({ coverage: coverageEl }); - }); - }, - - /* - * After the user focuses out, show validation help, if needed - */ - showTaxonValidation: function (e) { - - //Get the text inputs and select menus - var row = $(e.target).parents("tr"), - allInputs = row.find("input, select"), - tableContainer = $(e.target).parents("table"), - errorInputs = []; - - //If none of the inputs have a value and this is a new row, then do nothing - if (_.every(allInputs, function (i) { return !i.value }) && row.is(".new")) - return; - - //Add the error styling to any input with no value - _.each(allInputs, function (input) { - // Keep track of the number of clicks of each input element so we only show the - // error message after the user has focused on both input elements - if (!input.value) - errorInputs.push(input); - }); + removeTaxonRank: function (e) { + var row = $(e.target).parents(".taxonomic-coverage-row"), + coverageEl = $(row).parents(".taxonomic-coverage"), + view = this; - if (errorInputs.length) { + //Animate the row away and then remove it + row.slideUp("fast", function () { + row.remove(); + view.updateTaxonCoverage({ coverage: coverageEl }); + }); + }, - //Show the error message after a brief delay - setTimeout(function () { - //If the user focused on another element in the same row, don't do anything - if (_.contains(allInputs, document.activeElement)) - return; + /* + * After the user focuses out, show validation help, if needed + */ + showTaxonValidation: function (e) { + //Get the text inputs and select menus + var row = $(e.target).parents("tr"), + allInputs = row.find("input, select"), + tableContainer = $(e.target).parents("table"), + errorInputs = []; + + //If none of the inputs have a value and this is a new row, then do nothing + if ( + _.every(allInputs, function (i) { + return !i.value; + }) && + row.is(".new") + ) + return; + + //Add the error styling to any input with no value + _.each(allInputs, function (input) { + // Keep track of the number of clicks of each input element so we only show the + // error message after the user has focused on both input elements + if (!input.value) errorInputs.push(input); + }); + + if (errorInputs.length) { + //Show the error message after a brief delay + setTimeout(function () { + //If the user focused on another element in the same row, don't do anything + if (_.contains(allInputs, document.activeElement)) return; - //Add the error styling - $(errorInputs).addClass("error"); + //Add the error styling + $(errorInputs).addClass("error"); - //Add the error message - if (!tableContainer.prev(".notification").length) { - tableContainer.before($(document.createElement("p")) + //Add the error message + if (!tableContainer.prev(".notification").length) { + tableContainer.before( + $(document.createElement("p")) .addClass("error notification") - .text("Enter a rank name AND value in each row.")); - } - - }, 200); - } - else { - allInputs.removeClass("error"); - - if (!tableContainer.find(".error").length) - tableContainer.prev(".notification").remove(); - } + .text("Enter a rank name AND value in each row."), + ); + } + }, 200); + } else { + allInputs.removeClass("error"); - }, + if (!tableContainer.find(".error").length) + tableContainer.prev(".notification").remove(); + } + }, - previewTaxonRemove: function (e) { - var removeBtn = $(e.target); + previewTaxonRemove: function (e) { + var removeBtn = $(e.target); - if (removeBtn.parent().is(".root-taxonomic-classification")) { - removeBtn.parent().toggleClass("remove-preview"); - } - else { - removeBtn.parents(".taxonomic-coverage-row").toggleClass("remove-preview"); - } + if (removeBtn.parent().is(".root-taxonomic-classification")) { + removeBtn.parent().toggleClass("remove-preview"); + } else { + removeBtn + .parents(".taxonomic-coverage-row") + .toggleClass("remove-preview"); + } + }, - }, + updateRadioButtons: function (e) { + //Get the element of this radio button set that is checked + var choice = this.$( + "[name='" + $(e.target).attr("name") + "']:checked", + ).val(); - updateRadioButtons: function (e) { - //Get the element of this radio button set that is checked - var choice = this.$("[name='" + $(e.target).attr("name") + "']:checked").val(); + if (typeof choice == "undefined" || !choice) + this.model.set($(e.target).attr("data-category"), ""); + else this.model.set($(e.target).attr("data-category"), choice); - if (typeof choice == "undefined" || !choice) - this.model.set($(e.target).attr("data-category"), ""); - else - this.model.set($(e.target).attr("data-category"), choice); + this.model.trickleUpChange(); + }, - this.model.trickleUpChange(); - }, + /* + * Switch to the given section + */ + switchSection: function (e) { + if (!e) return; - /* - * Switch to the given section - */ - switchSection: function (e) { - if (!e) return; + e.preventDefault(); - e.preventDefault(); + var clickedEl = $(e.target), + section = + clickedEl.attr("data-section") || + clickedEl.children("[data-section]").attr("data-section") || + clickedEl.parents("[data-section]").attr("data-section"); - var clickedEl = $(e.target), - section = clickedEl.attr("data-section") || - clickedEl.children("[data-section]").attr("data-section") || - clickedEl.parents("[data-section]").attr("data-section"); + if (this.visibleSection == "all") this.scrollToSection(section); + else { + this.$(".section." + this.activeSection).hide(); + this.$(".section." + section).show(); - if (this.visibleSection == "all") - this.scrollToSection(section); - else { - this.$(".section." + this.activeSection).hide() - this.$(".section." + section).show(); + this.highlightTOC(section); - this.highlightTOC(section); + this.activeSection = section; + this.visibleSection = section; - this.activeSection = section; - this.visibleSection = section; + $("body").scrollTop( + this.$(".section." + section).offset().top - $("#Navbar").height(), + ); + } + }, - $("body").scrollTop(this.$(".section." + section).offset().top - $("#Navbar").height()); - } + /* + * When a user clicks on the section names in the side tabs, jump to the section + */ + scrollToSection: function (e) { + if (!e) return false; + //Stop navigation + e.preventDefault(); - }, + var section = $(e.target).attr("data-section"), + sectionEl = this.$(".section." + section); - /* - * When a user clicks on the section names in the side tabs, jump to the section - */ - scrollToSection: function (e) { - if (!e) return false; + if (!sectionEl) return false; - //Stop navigation - e.preventDefault(); + //Temporarily unbind the scroll listener while we scroll to the clicked section + $(document).unbind("scroll"); - var section = $(e.target).attr("data-section"), - sectionEl = this.$(".section." + section); + var view = this; + setTimeout(function () { + $(document).scroll(view.highlightTOC.call(view)); + }, 1500); - if (!sectionEl) return false; + //Scroll to the section + if (sectionEl == section[0]) MetacatUI.appView.scrollToTop(); + else MetacatUI.appView.scrollTo(sectionEl, $("#Navbar").outerHeight()); - //Temporarily unbind the scroll listener while we scroll to the clicked section - $(document).unbind("scroll"); + //Remove the active class from all the menu items + $(".side-nav-item a.active").removeClass("active"); + //Set the clicked item to active + $(".side-nav-item a[data-section='" + section + "']").addClass( + "active", + ); - var view = this; - setTimeout(function () { - $(document).scroll(view.highlightTOC.call(view)); - }, 1500); + //Set the active section on this view + this.activeSection = section; + }, - //Scroll to the section - if (sectionEl == section[0]) - MetacatUI.appView.scrollToTop(); - else - MetacatUI.appView.scrollTo(sectionEl, $("#Navbar").outerHeight()); + /* + * Highlight the given menu item. + * The first argument is either an event object or the section name + */ + highlightTOC: function (section) { + this.resizeTOC(); + //Now change sections + if (typeof section == "string") { //Remove the active class from all the menu items $(".side-nav-item a.active").removeClass("active"); - //Set the clicked item to active - $(".side-nav-item a[data-section='" + section + "']").addClass("active"); - //Set the active section on this view + $(".side-nav-item a[data-section='" + section + "']").addClass( + "active", + ); this.activeSection = section; - }, - - /* - * Highlight the given menu item. - * The first argument is either an event object or the section name - */ - highlightTOC: function (section) { - - this.resizeTOC(); + this.visibleSection = section; + return; + } else if (this.visibleSection == "all") { + //Remove the active class from all the menu items + $(".side-nav-item a.active").removeClass("active"); - //Now change sections - if (typeof section == "string") { - //Remove the active class from all the menu items - $(".side-nav-item a.active").removeClass("active"); + //Get the section + var top = $(window).scrollTop() + $("#Navbar").outerHeight() + 70, + sections = $(".metadata-container .section"); - $(".side-nav-item a[data-section='" + section + "']").addClass("active"); - this.activeSection = section; - this.visibleSection = section; - return; - } - else if (this.visibleSection == "all") { - //Remove the active class from all the menu items - $(".side-nav-item a.active").removeClass("active"); - - //Get the section - var top = $(window).scrollTop() + $("#Navbar").outerHeight() + 70, - sections = $(".metadata-container .section"); - - //If we're somewhere in the middle, find the right section - for (var i = 0; i < sections.length; i++) { - if (top > $(sections[i]).offset().top && top < $(sections[i + 1]).offset().top) { - $($(".side-nav-item a")[i]).addClass("active"); - this.activeSection = $(sections[i]).attr("data-section"); - this.visibleSection = $(sections[i]).attr("data-section"); - break; - } + //If we're somewhere in the middle, find the right section + for (var i = 0; i < sections.length; i++) { + if ( + top > $(sections[i]).offset().top && + top < $(sections[i + 1]).offset().top + ) { + $($(".side-nav-item a")[i]).addClass("active"); + this.activeSection = $(sections[i]).attr("data-section"); + this.visibleSection = $(sections[i]).attr("data-section"); + break; } - - } - }, - - /* - * Resizes the vertical table of contents so it's always the same height as the editor body - */ - resizeTOC: function () { - var tableBottomHandle = $("#editor-body .ui-resizable-handle"); + } + }, - if (!tableBottomHandle.length) - return; + /* + * Resizes the vertical table of contents so it's always the same height as the editor body + */ + resizeTOC: function () { + var tableBottomHandle = $("#editor-body .ui-resizable-handle"); - var tableBottom = tableBottomHandle[0].getBoundingClientRect().bottom, - navTop = tableBottom; + if (!tableBottomHandle.length) return; - if (tableBottom < $("#Navbar").outerHeight()) { - if ($("#Navbar").css("position") == "fixed") - navTop = $("#Navbar").outerHeight(); - else - navTop = 0; - } + var tableBottom = tableBottomHandle[0].getBoundingClientRect().bottom, + navTop = tableBottom; + if (tableBottom < $("#Navbar").outerHeight()) { + if ($("#Navbar").css("position") == "fixed") + navTop = $("#Navbar").outerHeight(); + else navTop = 0; + } - $(".metadata-toc").css("top", navTop); - }, + $(".metadata-toc").css("top", navTop); + }, - /* - * -- This function is for development/testing purposes only -- - * Trigger a change on all the form elements - * so that when values are changed by Javascript, we make sure the change event - * is fired. This is good for capturing changes by Javascript, or - * browser plugins that fill-in forms, etc. - */ - triggerChanges: function () { - $("#metadata-container input").change(); - $("#metadata-container textarea").change(); - $("#metadata-container select").change(); - }, + /* + * -- This function is for development/testing purposes only -- + * Trigger a change on all the form elements + * so that when values are changed by Javascript, we make sure the change event + * is fired. This is good for capturing changes by Javascript, or + * browser plugins that fill-in forms, etc. + */ + triggerChanges: function () { + $("#metadata-container input").change(); + $("#metadata-container textarea").change(); + $("#metadata-container select").change(); + }, - /* Creates "Remove" buttons for removing non-required sections + /* Creates "Remove" buttons for removing non-required sections of the EML from the DOM */ - createRemoveButton: function (submodel, attribute, selector, container) { - return $(document.createElement("span")) - .addClass("icon icon-remove remove pointer") - .attr("title", "Remove") - .data({ - 'submodel': submodel, - 'attribute': attribute, - 'selector': selector, - 'container': container - }) - }, + createRemoveButton: function (submodel, attribute, selector, container) { + return $(document.createElement("span")) + .addClass("icon icon-remove remove pointer") + .attr("title", "Remove") + .data({ + submodel: submodel, + attribute: attribute, + selector: selector, + container: container, + }); + }, - /* Generic event handler for removing sections of the EML (both + /* Generic event handler for removing sections of the EML (both the DOM and inside the EML211Model) */ - handleRemove: function (e) { - var submodel = $(e.target).data('submodel'), // Optional sub-model to remove attribute from - attribute = $(e.target).data('attribute'), // Attribute on the EML211 model we're removing from - selector = $(e.target).data('selector'), // Selector to find the parent DOM elemente we'll remove - container = $(e.target).data('container'), // Selector to find the parent container so we can remove by index - parentEl, // Element we'll remove - model; // Specific sub-model we're removing - - if (!attribute) return; - if (!container) return; - - // Find the element we'll remove from the DOM - if (selector) { - parentEl = $(e.target).parents(selector).first(); - } else { - parentEl = $(e.target).parents().first(); - } - - if (parentEl.length == 0) return; + handleRemove: function (e) { + var submodel = $(e.target).data("submodel"), // Optional sub-model to remove attribute from + attribute = $(e.target).data("attribute"), // Attribute on the EML211 model we're removing from + selector = $(e.target).data("selector"), // Selector to find the parent DOM elemente we'll remove + container = $(e.target).data("container"), // Selector to find the parent container so we can remove by index + parentEl, // Element we'll remove + model; // Specific sub-model we're removing + + if (!attribute) return; + if (!container) return; + + // Find the element we'll remove from the DOM + if (selector) { + parentEl = $(e.target).parents(selector).first(); + } else { + parentEl = $(e.target).parents().first(); + } - // Handle remove on a EML model / sub-model - if (submodel) { + if (parentEl.length == 0) return; - model = this.model.get(submodel); + // Handle remove on a EML model / sub-model + if (submodel) { + model = this.model.get(submodel); - if (!model) return; + if (!model) return; - // Get the current value of the attribute so we can remove from it - var currentValue, - submodelIndex; + // Get the current value of the attribute so we can remove from it + var currentValue, submodelIndex; - if (Array.isArray(this.model.get(submodel))) { - // Stop now if there's nothing to remove in the first place - if (this.model.get(submodel).length == 0) return; + if (Array.isArray(this.model.get(submodel))) { + // Stop now if there's nothing to remove in the first place + if (this.model.get(submodel).length == 0) return; - // For multi-valued submodels, find *which* submodel we are removing or - // removingn from - submodelIndex = $(container).index($(e.target).parents(container).first()); - if (submodelIndex === -1) return; + // For multi-valued submodels, find *which* submodel we are removing or + // removingn from + submodelIndex = $(container).index( + $(e.target).parents(container).first(), + ); + if (submodelIndex === -1) return; - currentValue = this.model.get(submodel)[submodelIndex].get(attribute); - } else { - currentValue = this.model.get(submodel).get(attribute); - } + currentValue = this.model + .get(submodel) + [submodelIndex].get(attribute); + } else { + currentValue = this.model.get(submodel).get(attribute); + } - //FInd the position of this field in the list of fields - var position = $(e.target).parents(container) - .first() - .children(selector) - .index($(e.target).parents(selector)); - - // Remove from the EML Model - if (position >= 0) { - if (Array.isArray(this.model.get(submodel))) { - currentValue.splice(position, 1); // Splice returns the removed members - this.model.get(submodel)[submodelIndex].set(attribute, currentValue); - } else { - currentValue.splice(position, 1); // Splice returns the removed members - this.model.get(submodel).set(attribute, currentValue); - } + //FInd the position of this field in the list of fields + var position = $(e.target) + .parents(container) + .first() + .children(selector) + .index($(e.target).parents(selector)); + // Remove from the EML Model + if (position >= 0) { + if (Array.isArray(this.model.get(submodel))) { + currentValue.splice(position, 1); // Splice returns the removed members + this.model + .get(submodel) + [submodelIndex].set(attribute, currentValue); + } else { + currentValue.splice(position, 1); // Splice returns the removed members + this.model.get(submodel).set(attribute, currentValue); } + } + } else if (selector) { + // Find the index this attribute is in the DOM + var position = $(e.target) + .parents(container) + .first() + .children(selector) + .index($(e.target).parents(selector)); - } else if (selector) { - // Find the index this attribute is in the DOM - var position = $(e.target).parents(container).first() - .children(selector) - .index($(e.target).parents(selector)); - - //Remove this index of the array - var currentValue = this.model.get(attribute); + //Remove this index of the array + var currentValue = this.model.get(attribute); - if (Array.isArray(currentValue)) - currentValue.splice(position, 1); + if (Array.isArray(currentValue)) currentValue.splice(position, 1); - //Set the array on the model so the 'set' function is executed + //Set the array on the model so the 'set' function is executed + this.model.set(attribute, currentValue); + } + // Handle remove on a basic text field + else { + // The DOM order matches the EML model attribute order so we can remove + // by that + var position = $(e.target) + .parents(container) + .first() + .children(selector) + .index(selector); + var currentValue = this.model.get(attribute); + + // Remove from the EML Model + if (position >= 0) { + currentValue.splice(position, 1); this.model.set(attribute, currentValue); - - } - // Handle remove on a basic text field - else { - // The DOM order matches the EML model attribute order so we can remove - // by that - var position = $(e.target).parents(container).first().children(selector).index(selector); - var currentValue = this.model.get(attribute); - - // Remove from the EML Model - if (position >= 0) { - currentValue.splice(position, 1); - this.model.set(attribute, currentValue); - } } + } - // Trigger a change on the entire package - MetacatUI.rootDataPackage.packageModel.set("changed", true); - - // Remove the DOM - $(parentEl).remove(); + // Trigger a change on the entire package + MetacatUI.rootDataPackage.packageModel.set("changed", true); - //updating the tablesIndex once the element has been removed - var tableNums = this.$(".editor-header-index"); - for (var i = 0; i < tableNums.length; i++) { - $(tableNums[i]).text(i + 1); - } + // Remove the DOM + $(parentEl).remove(); - // If this was a taxon, update the quickAdd interface - if (submodel === "taxonCoverage"){ - this.updateQuickAddTaxa(); - } - }, + //updating the tablesIndex once the element has been removed + var tableNums = this.$(".editor-header-index"); + for (var i = 0; i < tableNums.length; i++) { + $(tableNums[i]).text(i + 1); + } - /** - * Adds an {@link EMLAnnotation} to the {@link EML211} model currently being edited. - * Attributes for the annotation are retreived from the HTML attributes from the HTML element - * that was interacted with. - * @param {Event} e - An Event on an Element that contains {@link EMLAnnotation} data - */ - addAnnotation: function(e){ - try{ - if( !e || !e.target ){ - return; - } + // If this was a taxon, update the quickAdd interface + if (submodel === "taxonCoverage") { + this.updateQuickAddTaxa(); + } + }, - let annotationData = _.clone(e.target.dataset); + /** + * Adds an {@link EMLAnnotation} to the {@link EML211} model currently being edited. + * Attributes for the annotation are retreived from the HTML attributes from the HTML element + * that was interacted with. + * @param {Event} e - An Event on an Element that contains {@link EMLAnnotation} data + */ + addAnnotation: function (e) { + try { + if (!e || !e.target) { + return; + } - //If this is a radio button, we only want one annotation of this type. - if( e.target.getAttribute("type") == "radio" ){ - annotationData.allowDuplicates = false; - } + let annotationData = _.clone(e.target.dataset); - //Set the valueURI from the input value - annotationData.valueURI = $(e.target).val(); + //If this is a radio button, we only want one annotation of this type. + if (e.target.getAttribute("type") == "radio") { + annotationData.allowDuplicates = false; + } - //Reformat the propertyURI property - if( annotationData.propertyUri ){ - annotationData.propertyURI = annotationData.propertyUri; - delete annotationData.propertyUri; - } + //Set the valueURI from the input value + annotationData.valueURI = $(e.target).val(); - this.model.addAnnotation(annotationData); + //Reformat the propertyURI property + if (annotationData.propertyUri) { + annotationData.propertyURI = annotationData.propertyUri; + delete annotationData.propertyUri; } - catch(error){ - console.error("Couldn't add annotation: ", e); - } - - }, - /* Close the view and its sub views */ - onClose: function () { - this.remove(); // remove for the DOM, stop listening - this.off(); // remove callbacks, prevent zombies - this.model.off(); + this.model.addAnnotation(annotationData); + } catch (error) { + console.error("Couldn't add annotation: ", e); + } + }, - //Remove the scroll event listeners - $(document).unbind("scroll"); + /* Close the view and its sub views */ + onClose: function () { + this.remove(); // remove for the DOM, stop listening + this.off(); // remove callbacks, prevent zombies + this.model.off(); - this.model = null; + //Remove the scroll event listeners + $(document).unbind("scroll"); - this.subviews = []; - window.onbeforeunload = null; + this.model = null; - } - }); - return EMLView; - }); + this.subviews = []; + window.onbeforeunload = null; + }, + }, + ); + return EMLView; +});
diff --git a/docs/docs/src_js_views_metadata_EMLAttributeView.js.html b/docs/docs/src_js_views_metadata_EMLAttributeView.js.html index ee682c1b7..d58390a79 100644 --- a/docs/docs/src_js_views_metadata_EMLAttributeView.js.html +++ b/docs/docs/src_js_views_metadata_EMLAttributeView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/metadata/EMLAttributeView.js

-
/* global define */
-define([
+            
define([
   "underscore",
   "jquery",
   "backbone",
@@ -64,7 +63,7 @@ 

Source: src/js/views/metadata/EMLAttributeView.js

EMLMeasurementScale, EMLMeasurementScaleView, EML211MissingValueCodesView, - EMLAttributeTemplate + EMLAttributeTemplate, ) { /** * @class EMLAttributeView @@ -218,7 +217,7 @@

Source: src/js/views/metadata/EMLAttributeView.js

// Dynamically require since this view is feature-flagged off by default // and requires an API key require(["views/metadata/EMLMeasurementTypeView"], function ( - EMLMeasurementTypeView + EMLMeasurementTypeView, ) { var view = new EMLMeasurementTypeView({ model: viewRef.model, @@ -261,7 +260,7 @@

Source: src/js/views/metadata/EMLAttributeView.js

if (Array.isArray(currentValue)) { // Get the position of the updated DOM element var index = this.$(".input[data-category='" + category + "']").index( - e.target + e.target, ); // If there is at least one value already in the array... @@ -344,7 +343,7 @@

Source: src/js/views/metadata/EMLAttributeView.js

.text(errors[attr]) .addClass("error"); }, - view + view, ); view.$el.addClass("error"); @@ -374,7 +373,7 @@

Source: src/js/views/metadata/EMLAttributeView.js

.removeClass("error") .empty(); }, - } + }, ); return EMLAttributeView; diff --git a/docs/docs/src_js_views_metadata_EMLEntityView.js.html b/docs/docs/src_js_views_metadata_EMLEntityView.js.html index d3d4499d9..1b876c395 100644 --- a/docs/docs/src_js_views_metadata_EMLEntityView.js.html +++ b/docs/docs/src_js_views_metadata_EMLEntityView.js.html @@ -44,795 +44,843 @@

Source: src/js/views/metadata/EMLEntityView.js

-
/* global define */
-define(['underscore', 'jquery', 'backbone', 'localforage',
-        'models/DataONEObject', 'models/metadata/eml211/EMLAttribute', 'models/metadata/eml211/EMLEntity',
-        'views/DataPreviewView',
-        'views/metadata/EMLAttributeView',
-        'text!templates/metadata/eml-entity.html',
-        'text!templates/metadata/eml-attribute-menu-item.html',
-        'common/Utilities'],
-    function(_, $, Backbone, LocalForage, DataONEObject, EMLAttribute, EMLEntity,
-        DataPreviewView,
-        EMLAttributeView,
-        EMLEntityTemplate,
-        EMLAttributeMenuItemTemplate,
-        Utilities){
-
-        /**
-        * @class EMLEntityView
-        * @classdesc An EMLEntityView shows the basic attributes of a DataONEObject, as described by EML
-        * @classcategory Views/Metadata
-        * @screenshot views/metadata/EMLEntityView.png
-        * @extends Backbone.View
-        */
-        var EMLEntityView = Backbone.View.extend(
-          /** @lends EMLEntityView.prototype */{
-
-            tagName: "div",
-
-            className: "eml-entity modal hide fade",
-
-            id: null,
-
-            /* The HTML template for an entity */
-            template: _.template(EMLEntityTemplate),
-            attributeMenuItemTemplate: _.template(EMLAttributeMenuItemTemplate),
-            fillButtonTemplateString: '<button class="btn btn-primary fill-button"><i class="icon-magic"></i> Fill from file</button>',
-
-            /**
-             * A list of file formats that can be auto-filled with attribute information
-             * @type {string[]}
-             * @since 2.15.0
-             */
-            fillableFormats: [
-              "text/csv"
-            ],
-
-            /* Events this view listens to */
-            events: {
-              "change" : "saveDraft",
-              "change input" : "updateModel",
-              "change textarea" : "updateModel",
-              "click .entity-container > .nav-tabs a" : "showTab",
-              "click .attribute-menu-item" : "showAttribute",
-              "mouseover .attribute-menu-item .remove" : "previewAttrRemove",
-              "mouseout .attribute-menu-item .remove"  : "previewAttrRemove",
-              "click .attribute-menu-item .remove" : "removeAttribute",
-              "click .fill-button": "handleFill"
-            },
-
-            initialize: function(options){
-              if(!options)
-                var options = {};
-
-              this.model = options.model || new EMLEntity();
-              this.DataONEObject = options.DataONEObject;
-            },
-
-            render: function(){
-
-              this.renderEntityTemplate();
-
-              this.renderPreview();
-
-              this.renderAttributes();
-
-              this.renderFillButton();
-
-              this.listenTo(this.model, "invalid", this.showValidation);
-              this.listenTo(this.model, "valid", this.showValidation);
-
-            },
-
-            renderEntityTemplate: function(){
-              var modelAttr = this.model.toJSON();
-
-              if(!modelAttr.entityName)
-                modelAttr.title = "this data";
-              else
-                modelAttr.title = modelAttr.entityName;
-
-              modelAttr.uniqueId = this.model.cid;
-
-              this.$el.html(this.template( modelAttr ));
-
-              //Initialize the modal window
-              this.$el.modal();
-
-
-               //Set the menu height
-              var view = this;
-               this.$el.on("shown", function(){
-                 view.adjustHeight();
-                 view.setMenuWidth();
-
-                 window.addEventListener('resize', function(event){
-                   view.adjustHeight();
-                   view.setMenuWidth();
-                 });
-               });
-
-              this.$el.on("hidden", function(){
-                view.showValidation();
-              });
-
-            },
-
-            renderPreview: function(){
-              //Get the DataONEObject model
-              if(this.DataONEObject){
-                var dataPreview = new DataPreviewView({
-                  model: this.DataONEObject
-                });
-                dataPreview.render();
-                this.$(".preview-container").html(dataPreview.el);
-
-                if(dataPreview.$el.children().length){
-                  this.$(".description").css("width", "calc(100% - 310px)");
-                }
-                else
-                  dataPreview.$el.remove();
-              }
-            },
-
-            renderAttributes: function(){
-              //Render the attributes
-              var attributes      = this.model.get("attributeList"),
-                attributeListEl = this.$(".attribute-list"),
-                attributeMenuEl = this.$(".attribute-menu");
-
-              _.each(attributes, function(attr){
-
-                //Create an EMLAttributeView
-                var view = new EMLAttributeView({
-                  model: attr
-                });
-
-                //Create a link in the attribute menu
-                var menuItem = $(this.attributeMenuItemTemplate({
-                    attrId: attr.cid,
-                    attributeName: attr.get("attributeName"),
-                    classes: ""
-                  })).data({
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "localforage",
+  "models/DataONEObject",
+  "models/metadata/eml211/EMLAttribute",
+  "models/metadata/eml211/EMLEntity",
+  "views/DataPreviewView",
+  "views/metadata/EMLAttributeView",
+  "text!templates/metadata/eml-entity.html",
+  "text!templates/metadata/eml-attribute-menu-item.html",
+  "common/Utilities",
+], function (
+  _,
+  $,
+  Backbone,
+  LocalForage,
+  DataONEObject,
+  EMLAttribute,
+  EMLEntity,
+  DataPreviewView,
+  EMLAttributeView,
+  EMLEntityTemplate,
+  EMLAttributeMenuItemTemplate,
+  Utilities,
+) {
+  /**
+   * @class EMLEntityView
+   * @classdesc An EMLEntityView shows the basic attributes of a DataONEObject, as described by EML
+   * @classcategory Views/Metadata
+   * @screenshot views/metadata/EMLEntityView.png
+   * @extends Backbone.View
+   */
+  var EMLEntityView = Backbone.View.extend(
+    /** @lends EMLEntityView.prototype */ {
+      tagName: "div",
+
+      className: "eml-entity modal hide fade",
+
+      id: null,
+
+      /* The HTML template for an entity */
+      template: _.template(EMLEntityTemplate),
+      attributeMenuItemTemplate: _.template(EMLAttributeMenuItemTemplate),
+      fillButtonTemplateString:
+        '<button class="btn btn-primary fill-button"><i class="icon-magic"></i> Fill from file</button>',
+
+      /**
+       * A list of file formats that can be auto-filled with attribute information
+       * @type {string[]}
+       * @since 2.15.0
+       */
+      fillableFormats: ["text/csv"],
+
+      /* Events this view listens to */
+      events: {
+        change: "saveDraft",
+        "change input": "updateModel",
+        "change textarea": "updateModel",
+        "click .entity-container > .nav-tabs a": "showTab",
+        "click .attribute-menu-item": "showAttribute",
+        "mouseover .attribute-menu-item .remove": "previewAttrRemove",
+        "mouseout .attribute-menu-item .remove": "previewAttrRemove",
+        "click .attribute-menu-item .remove": "removeAttribute",
+        "click .fill-button": "handleFill",
+      },
+
+      initialize: function (options) {
+        if (!options) var options = {};
+
+        this.model = options.model || new EMLEntity();
+        this.DataONEObject = options.DataONEObject;
+      },
+
+      render: function () {
+        this.renderEntityTemplate();
+
+        this.renderPreview();
+
+        this.renderAttributes();
+
+        this.renderFillButton();
+
+        this.listenTo(this.model, "invalid", this.showValidation);
+        this.listenTo(this.model, "valid", this.showValidation);
+      },
+
+      renderEntityTemplate: function () {
+        var modelAttr = this.model.toJSON();
+
+        if (!modelAttr.entityName) modelAttr.title = "this data";
+        else modelAttr.title = modelAttr.entityName;
+
+        modelAttr.uniqueId = this.model.cid;
+
+        this.$el.html(this.template(modelAttr));
+
+        //Initialize the modal window
+        this.$el.modal();
+
+        //Set the menu height
+        var view = this;
+        this.$el.on("shown", function () {
+          view.adjustHeight();
+          view.setMenuWidth();
+
+          window.addEventListener("resize", function (event) {
+            view.adjustHeight();
+            view.setMenuWidth();
+          });
+        });
+
+        this.$el.on("hidden", function () {
+          view.showValidation();
+        });
+      },
+
+      renderPreview: function () {
+        //Get the DataONEObject model
+        if (this.DataONEObject) {
+          var dataPreview = new DataPreviewView({
+            model: this.DataONEObject,
+          });
+          dataPreview.render();
+          this.$(".preview-container").html(dataPreview.el);
+
+          if (dataPreview.$el.children().length) {
+            this.$(".description").css("width", "calc(100% - 310px)");
+          } else dataPreview.$el.remove();
+        }
+      },
+
+      renderAttributes: function () {
+        //Render the attributes
+        var attributes = this.model.get("attributeList"),
+          attributeListEl = this.$(".attribute-list"),
+          attributeMenuEl = this.$(".attribute-menu");
+
+        _.each(
+          attributes,
+          function (attr) {
+            //Create an EMLAttributeView
+            var view = new EMLAttributeView({
               model: attr,
-              attributeView: view
-              });
-                attributeMenuEl.append(menuItem);
-                menuItem.find(".tooltip-this").tooltip();
-
-                this.listenTo(attr, "change:attributeName", function(attr){
-                  menuItem.find(".name").text(attr.get("attributeName"));
-                });
-
-                view.render();
-
-                attributeListEl.append(view.el);
-
-                view.$el.hide();
-
-                this.listenTo(attr, "change",  this.addAttribute);
-                this.listenTo(attr, "invalid", this.showAttributeValidation);
-                this.listenTo(attr, "valid",   this.hideAttributeValidation);
-
-              }, this);
-
-              //Add a new blank attribute view at the end
-              this.addNewAttribute();
-
-              //If there are no attributes in this EML model yet,
-              //then make sure we show a new add attribute button when the user starts typing
-              if(attributes.length == 0){
-                var onlyAttrView = this.$(".attribute-menu-item").first().data("attributeView"),
-                  view = this,
-                  keyUpCallback = function(){
-                    //This attribute is no longer new
-                    view.$(".attribute-menu-item.new").first().removeClass("new");
-                    view.$(".attribute-list .eml-attribute.new").first().removeClass("new");
-
-                    //Add a new attribute link and view
-                    view.addNewAttribute();
-
-                    //Don't listen to keyup anymore
-                    onlyAttrView.$el.off("keyup", keyUpCallback);
-                  };
-
-                onlyAttrView.$el.on("keyup", keyUpCallback);
-              }
-
-            //Activate the first navigation item
-            var firstAttr = this.$(".side-nav-item").first();
-            firstAttr.addClass("active");
-
-            //Show the first attribute view
-            firstAttr.data("attributeView").$el.show();
-
-            firstAttr.data("attributeView").postRender();
-
-            },
-
-            renderFillButton: function() {
-              var formatGuess = this.model.get("dataONEObject")
-                ? this.model.get("dataONEObject").get("formatId")
-                : this.model.get("entityType");
-
-              if (!_.contains(this.fillableFormats, formatGuess)) {
-                return;
-              }
-
-              var target = this.$(".fill-button-container");
-
-              if (!target.length === 1) {
-                return;
-              }
-
-              var btn = $(this.fillButtonTemplateString);
-              $(target).html(btn);
-            },
-
-            updateModel: function(e){
-              var changedAttr = $(e.target).attr("data-category");
-
-              if(!changedAttr) return;
-
-              var emlModel = this.model.getParentEML(),
-                  newValue = emlModel? emlModel.cleanXMLText($(e.target).val()) : $(e.target).val();
-
-              this.model.set(changedAttr, newValue);
-
-              this.model.trickleUpChange();
-
-            },
-
-            addNewAttribute: function(){
-
-              //Check if there is already a new attribute view
-              if( this.$(".attribute-list .eml-attribute.new").length ){
-                return;
-              }
-
-              var newAttrModel = new EMLAttribute({
-                  parentModel: this.model,
-                        xmlID: DataONEObject.generateId()
-                  }),
-                  newAttrView  = new EMLAttributeView({
-                    isNew: true,
-                    model: newAttrModel
-                  });
-
-              newAttrView.render();
-              this.$(".attribute-list").append(newAttrView.el);
-              newAttrView.$el.hide();
-
-              //Change the last menu item if it still says "Add attribute"
-              if(this.$(".attribute-menu-item").length == 1){
-                var firstAttrMenuItem = this.$(".attribute-menu-item").first();
-
-                if( firstAttrMenuItem.find(".name").text() == "Add attribute" ){
-                  firstAttrMenuItem.find(".name").text("New attribute");
-                  firstAttrMenuItem.find(".add").hide();
-                }
-              }
-
-              //Create the new menu item
-              var menuItem = $(this.attributeMenuItemTemplate({
-                  attrId: newAttrModel.cid,
-                  attributeName: "Add attribute",
-                  classes: "new"
-                })).data({
-                  model: newAttrModel,
-                  attributeView: newAttrView
-                });
-              menuItem.find(".add").show();
-              this.$(".attribute-menu").append(menuItem);
-              menuItem.find(".tooltip-this").tooltip();
-
-              //When the attribute name is changed, update the navigation
-              this.listenTo(newAttrModel, "change:attributeName", function(attr){
-                menuItem.find(".name").text(attr.get("attributeName"));
-                menuItem.find(".add").hide();
-              });
-
-              this.listenTo(newAttrModel, "change",  this.addAttribute);
-              this.listenTo(newAttrModel, "invalid", this.showAttributeValidation);
-              this.listenTo(newAttrModel, "valid",   this.hideAttributeValidation);
-            },
-
-            addAttribute: function(emlAttribute){
-              //Add the attribute to the attribute list in the EMLEntity model
-              if( !_.contains(this.model.get("attributeList"), emlAttribute) )
-                this.model.addAttribute(emlAttribute);
-            },
-
-            removeAttribute: function(e){
-              var removeBtn = $(e.target);
-
-              var menuItem  = removeBtn.parents(".attribute-menu-item"),
-                attrModel = menuItem.data("model");
-
-              if(attrModel){
-                //Remove the attribute from the model
-                this.model.removeAttribute(attrModel);
-
-                //If this menu item is active, then make the next attribute active instead
-                if(menuItem.is(".active")){
-                  var nextMenuItem = menuItem.next();
-
-                  if(!nextMenuItem.length || nextMenuItem.is(".new")){
-                    nextMenuItem = menuItem.prev();
-                  }
-
-                  if(nextMenuItem.length){
-                    nextMenuItem.addClass("active");
-
-                    this.showAttribute(nextMenuItem.data("model"));
-                  }
-                }
-
-                //Remove the elements for this attribute from the page
-                menuItem.remove();
-                this.$(".eml-attribute[data-attribute-id='" + attrModel.cid + "']").remove();
-                $(".tooltip").remove();
-
-                this.model.trickleUpChange();
-              }
-            },
-
-            adjustHeight: function(e){
-              var contentAreaHeight = this.$(".modal-body").height() - this.$(".entity-container .nav-tabs").height();
-
-              this.$(".attribute-menu, .attribute-list").css("height", contentAreaHeight + "px");
-            },
-
-            setMenuWidth: function(){
-
-              this.$(".entity-container .nav").width( this.$el.width() );
-
-            },
-
-            /**
-             * Shows the attribute in the attribute editor
-             * @param {Event} e - JS event or attribute model
-             */
-            showAttribute: function(e){
-
-              if(e.target){
-                     var clickedEl = $(e.target),
-                         menuItem = clickedEl.is(".attribute-menu-item") || clickedEl.parents(".attribute-menu-item");
-
-                  if(clickedEl.is(".remove"))
-                    return;
-              }
-              else{
-                var menuItem = this.$(".attribute-menu-item[data-attribute-id='" + e.cid + "']");
-              }
-
-              if(!menuItem)
-                return;
-
-              //Validate the previously edited attribute
-              //Get the current active attribute
-              var activeAttrTab = this.$(".attribute-menu-item.active");
-
-              //If there is a currently-active attribute tab,
-              if( activeAttrTab.length ){
-                //Get the attribute list from this view's model
-                var emlAttributes = this.model.get("attributeList");
-
-                //If there is an EMLAttribute list,
-                if( emlAttributes && emlAttributes.length ){
-
-                  //Get the active EMLAttribute
-                  var activeEMLAttribute = _.findWhere(emlAttributes, { cid: activeAttrTab.attr("data-attribute-id") });
-
-                  //If there is an active EMLAttribute model, validate it
-                  if( activeEMLAttribute ){
-                    activeEMLAttribute.isValid();
-                  }
-
-                }
-
-              }
-
-              //If the user clicked on the add attribute link
-              if( menuItem.is(".new") && this.$(".new.attribute-menu-item").length < 2 ){
+            });
+
+            //Create a link in the attribute menu
+            var menuItem = $(
+              this.attributeMenuItemTemplate({
+                attrId: attr.cid,
+                attributeName: attr.get("attributeName"),
+                classes: "",
+              }),
+            ).data({
+              model: attr,
+              attributeView: view,
+            });
+            attributeMenuEl.append(menuItem);
+            menuItem.find(".tooltip-this").tooltip();
+
+            this.listenTo(attr, "change:attributeName", function (attr) {
+              menuItem.find(".name").text(attr.get("attributeName"));
+            });
+
+            view.render();
+
+            attributeListEl.append(view.el);
+
+            view.$el.hide();
+
+            this.listenTo(attr, "change", this.addAttribute);
+            this.listenTo(attr, "invalid", this.showAttributeValidation);
+            this.listenTo(attr, "valid", this.hideAttributeValidation);
+          },
+          this,
+        );
+
+        //Add a new blank attribute view at the end
+        this.addNewAttribute();
+
+        //If there are no attributes in this EML model yet,
+        //then make sure we show a new add attribute button when the user starts typing
+        if (attributes.length == 0) {
+          var onlyAttrView = this.$(".attribute-menu-item")
+              .first()
+              .data("attributeView"),
+            view = this,
+            keyUpCallback = function () {
+              //This attribute is no longer new
+              view.$(".attribute-menu-item.new").first().removeClass("new");
+              view
+                .$(".attribute-list .eml-attribute.new")
+                .first()
+                .removeClass("new");
+
+              //Add a new attribute link and view
+              view.addNewAttribute();
+
+              //Don't listen to keyup anymore
+              onlyAttrView.$el.off("keyup", keyUpCallback);
+            };
+
+          onlyAttrView.$el.on("keyup", keyUpCallback);
+        }
+
+        //Activate the first navigation item
+        var firstAttr = this.$(".side-nav-item").first();
+        firstAttr.addClass("active");
+
+        //Show the first attribute view
+        firstAttr.data("attributeView").$el.show();
+
+        firstAttr.data("attributeView").postRender();
+      },
+
+      renderFillButton: function () {
+        var formatGuess = this.model.get("dataONEObject")
+          ? this.model.get("dataONEObject").get("formatId")
+          : this.model.get("entityType");
+
+        if (!_.contains(this.fillableFormats, formatGuess)) {
+          return;
+        }
+
+        var target = this.$(".fill-button-container");
+
+        if (!target.length === 1) {
+          return;
+        }
+
+        var btn = $(this.fillButtonTemplateString);
+        $(target).html(btn);
+      },
+
+      updateModel: function (e) {
+        var changedAttr = $(e.target).attr("data-category");
+
+        if (!changedAttr) return;
+
+        var emlModel = this.model.getParentEML(),
+          newValue = emlModel
+            ? emlModel.cleanXMLText($(e.target).val())
+            : $(e.target).val();
+
+        this.model.set(changedAttr, newValue);
+
+        this.model.trickleUpChange();
+      },
+
+      addNewAttribute: function () {
+        //Check if there is already a new attribute view
+        if (this.$(".attribute-list .eml-attribute.new").length) {
+          return;
+        }
+
+        var newAttrModel = new EMLAttribute({
+            parentModel: this.model,
+            xmlID: DataONEObject.generateId(),
+          }),
+          newAttrView = new EMLAttributeView({
+            isNew: true,
+            model: newAttrModel,
+          });
 
-                //Change the attribute menu item
-                menuItem.removeClass("new").find(".name").text("New attribute");
-                this.$(".eml-attribute.new").removeClass("new");
-                menuItem.find(".add").hide();
+        newAttrView.render();
+        this.$(".attribute-list").append(newAttrView.el);
+        newAttrView.$el.hide();
+
+        //Change the last menu item if it still says "Add attribute"
+        if (this.$(".attribute-menu-item").length == 1) {
+          var firstAttrMenuItem = this.$(".attribute-menu-item").first();
+
+          if (firstAttrMenuItem.find(".name").text() == "Add attribute") {
+            firstAttrMenuItem.find(".name").text("New attribute");
+            firstAttrMenuItem.find(".add").hide();
+          }
+        }
+
+        //Create the new menu item
+        var menuItem = $(
+          this.attributeMenuItemTemplate({
+            attrId: newAttrModel.cid,
+            attributeName: "Add attribute",
+            classes: "new",
+          }),
+        ).data({
+          model: newAttrModel,
+          attributeView: newAttrView,
+        });
+        menuItem.find(".add").show();
+        this.$(".attribute-menu").append(menuItem);
+        menuItem.find(".tooltip-this").tooltip();
+
+        //When the attribute name is changed, update the navigation
+        this.listenTo(newAttrModel, "change:attributeName", function (attr) {
+          menuItem.find(".name").text(attr.get("attributeName"));
+          menuItem.find(".add").hide();
+        });
+
+        this.listenTo(newAttrModel, "change", this.addAttribute);
+        this.listenTo(newAttrModel, "invalid", this.showAttributeValidation);
+        this.listenTo(newAttrModel, "valid", this.hideAttributeValidation);
+      },
+
+      addAttribute: function (emlAttribute) {
+        //Add the attribute to the attribute list in the EMLEntity model
+        if (!_.contains(this.model.get("attributeList"), emlAttribute))
+          this.model.addAttribute(emlAttribute);
+      },
+
+      removeAttribute: function (e) {
+        var removeBtn = $(e.target);
+
+        var menuItem = removeBtn.parents(".attribute-menu-item"),
+          attrModel = menuItem.data("model");
+
+        if (attrModel) {
+          //Remove the attribute from the model
+          this.model.removeAttribute(attrModel);
+
+          //If this menu item is active, then make the next attribute active instead
+          if (menuItem.is(".active")) {
+            var nextMenuItem = menuItem.next();
+
+            if (!nextMenuItem.length || nextMenuItem.is(".new")) {
+              nextMenuItem = menuItem.prev();
+            }
 
-                //Add a new attribute view and menu item
-                this.addNewAttribute();
+            if (nextMenuItem.length) {
+              nextMenuItem.addClass("active");
 
-                //Scroll the attribute menu to the bottom so that the "Add New" button is always visible
-                var attrMenuHeight = this.$(".attribute-menu").scrollTop() + this.$(".attribute-menu").height();
-                this.$(".attribute-menu").scrollTop( attrMenuHeight );
+              this.showAttribute(nextMenuItem.data("model"));
+            }
+          }
+
+          //Remove the elements for this attribute from the page
+          menuItem.remove();
+          this.$(
+            ".eml-attribute[data-attribute-id='" + attrModel.cid + "']",
+          ).remove();
+          $(".tooltip").remove();
+
+          this.model.trickleUpChange();
+        }
+      },
+
+      adjustHeight: function (e) {
+        var contentAreaHeight =
+          this.$(".modal-body").height() -
+          this.$(".entity-container .nav-tabs").height();
+
+        this.$(".attribute-menu, .attribute-list").css(
+          "height",
+          contentAreaHeight + "px",
+        );
+      },
+
+      setMenuWidth: function () {
+        this.$(".entity-container .nav").width(this.$el.width());
+      },
+
+      /**
+       * Shows the attribute in the attribute editor
+       * @param {Event} e - JS event or attribute model
+       */
+      showAttribute: function (e) {
+        if (e.target) {
+          var clickedEl = $(e.target),
+            menuItem =
+              clickedEl.is(".attribute-menu-item") ||
+              clickedEl.parents(".attribute-menu-item");
+
+          if (clickedEl.is(".remove")) return;
+        } else {
+          var menuItem = this.$(
+            ".attribute-menu-item[data-attribute-id='" + e.cid + "']",
+          );
+        }
+
+        if (!menuItem) return;
+
+        //Validate the previously edited attribute
+        //Get the current active attribute
+        var activeAttrTab = this.$(".attribute-menu-item.active");
+
+        //If there is a currently-active attribute tab,
+        if (activeAttrTab.length) {
+          //Get the attribute list from this view's model
+          var emlAttributes = this.model.get("attributeList");
+
+          //If there is an EMLAttribute list,
+          if (emlAttributes && emlAttributes.length) {
+            //Get the active EMLAttribute
+            var activeEMLAttribute = _.findWhere(emlAttributes, {
+              cid: activeAttrTab.attr("data-attribute-id"),
+            });
+
+            //If there is an active EMLAttribute model, validate it
+            if (activeEMLAttribute) {
+              activeEMLAttribute.isValid();
+            }
+          }
+        }
+
+        //If the user clicked on the add attribute link
+        if (
+          menuItem.is(".new") &&
+          this.$(".new.attribute-menu-item").length < 2
+        ) {
+          //Change the attribute menu item
+          menuItem.removeClass("new").find(".name").text("New attribute");
+          this.$(".eml-attribute.new").removeClass("new");
+          menuItem.find(".add").hide();
+
+          //Add a new attribute view and menu item
+          this.addNewAttribute();
+
+          //Scroll the attribute menu to the bottom so that the "Add New" button is always visible
+          var attrMenuHeight =
+            this.$(".attribute-menu").scrollTop() +
+            this.$(".attribute-menu").height();
+          this.$(".attribute-menu").scrollTop(attrMenuHeight);
+        }
+
+        //Get the attribute view
+        var attrView = menuItem.data("attributeView");
+
+        //Change the active attribute in the menu
+        this.$(".attribute-menu-item.active").removeClass("active");
+        menuItem.addClass("active");
+
+        //Hide the old attribute view
+        this.$(".eml-attribute").hide();
+        //Show the new attribute view
+        attrView.$el.show();
+
+        //Scroll to the top of the attribute view
+        this.$(".attribute-list").scrollTop(0);
+
+        attrView.postRender();
+      },
+
+      /**
+       * Show the attribute validation errors in the attribute navigation menu
+       * @param {EMLAttribute} attr
+       */
+      showAttributeValidation: function (attr) {
+        var attrLink = this.$(
+          ".attribute-menu-item[data-attribute-id='" + attr.cid + "']",
+        ).find("a");
+
+        //If the validation is already displayed, then exit
+        if (attrLink.is(".error")) return;
+
+        var errorIcon = $(document.createElement("i")).addClass(
+          "icon icon-exclamation-sign error icon-on-left",
+        );
+
+        attrLink.addClass("error").prepend(errorIcon);
+      },
+
+      /**
+       * Hide the attribute validation errors from the attribute navigation menu
+       */
+      hideAttributeValidation: function (attr) {
+        this.$(".attribute-menu-item[data-attribute-id='" + attr.cid + "']")
+          .find("a")
+          .removeClass("error")
+          .find(".icon.error")
+          .remove();
+      },
+
+      /**
+       * Show the user what will be removed when this remove button is clicked
+       */
+      previewAttrRemove: function (e) {
+        var removeBtn = $(e.target);
+
+        removeBtn.parents(".attribute-menu-item").toggleClass("remove-preview");
+      },
+
+      /**
+       *
+       * Will display validation styling and messaging. Should be called after
+       * this view's model has been validated and there are error messages to display
+       */
+      showValidation: function () {
+        //Reset the error messages and styling
+        //Only change elements inside the overview-container which contains only the
+        // EMLEntity metadata. The Attributes will be changed by the EMLAttributeView.
+        this.$(".overview-container .notification").text("");
+        this.$(
+          ".overview-tab .icon.error, .attributes-tab .icon.error",
+        ).remove();
+        this.$(
+          ".overview-container, .overview-tab a, .attributes-tab a, .overview-container .error",
+        ).removeClass("error");
+
+        var overviewTabErrorIcon = false,
+          attributeTabErrorIcon = false;
+
+        _.each(
+          this.model.validationError,
+          function (errorMsg, category) {
+            if (category == "attributeList") {
+              //Create an error icon for the Attributes tab
+              if (!attributeTabErrorIcon) {
+                var errorIcon = $(document.createElement("i"))
+                  .addClass("icon icon-on-left icon-exclamation-sign error")
+                  .attr("title", "There is missing information in this tab");
+
+                //Add the icon to the Overview tab
+                this.$(".attributes-tab a")
+                  .prepend(errorIcon)
+                  .addClass("error");
               }
 
-              //Get the attribute view
-              var attrView = menuItem.data("attributeView");
-
-              //Change the active attribute in the menu
-              this.$(".attribute-menu-item.active").removeClass("active");
-              menuItem.addClass("active");
-
-              //Hide the old attribute view
-              this.$(".eml-attribute").hide();
-              //Show the new attribute view
-              attrView.$el.show();
-
-              //Scroll to the top of the attribute view
-              this.$(".attribute-list").scrollTop(0);
-
-              attrView.postRender();
-            },
-
-            /**
-             * Show the attribute validation errors in the attribute navigation menu
-             * @param {EMLAttribute} attr
-             */
-            showAttributeValidation: function(attr){
-
-              var attrLink = this.$(".attribute-menu-item[data-attribute-id='" + attr.cid + "']").find("a");
-
-              //If the validation is already displayed, then exit
-              if(attrLink.is(".error")) return;
-
-              var errorIcon = $(document.createElement("i")).addClass("icon icon-exclamation-sign error icon-on-left");
-
-              attrLink.addClass("error").prepend(errorIcon);
-            },
-
-            /**
-             * Hide the attribute validation errors from the attribute navigation menu
-             */
-            hideAttributeValidation: function(attr){
-              this.$(".attribute-menu-item[data-attribute-id='" + attr.cid + "']")
-                .find("a").removeClass("error").find(".icon.error").remove();
-            },
-
-            /**
-             * Show the user what will be removed when this remove button is clicked
-             */
-            previewAttrRemove: function(e){
-              var removeBtn = $(e.target);
-
-              removeBtn.parents(".attribute-menu-item").toggleClass("remove-preview");
-            },
-
-            /**
-            *
-            * Will display validation styling and messaging. Should be called after
-            * this view's model has been validated and there are error messages to display
-            */
-            showValidation: function(){
-
-              //Reset the error messages and styling
-              //Only change elements inside the overview-container which contains only the
-              // EMLEntity metadata. The Attributes will be changed by the EMLAttributeView.
-              this.$(".overview-container .notification").text("");
-              this.$(".overview-tab .icon.error, .attributes-tab .icon.error").remove();
-              this.$(".overview-container, .overview-tab a, .attributes-tab a, .overview-container .error").removeClass("error");
-
-              var overviewTabErrorIcon  = false,
-                  attributeTabErrorIcon = false;
-
-              _.each( this.model.validationError, function(errorMsg, category){
-
-                if( category == "attributeList" ){
-
-                  //Create an error icon for the Attributes tab
-                  if( !attributeTabErrorIcon ){
-                    var errorIcon = $(document.createElement("i"))
-                                      .addClass("icon icon-on-left icon-exclamation-sign error")
-                                      .attr("title", "There is missing information in this tab");
-
-                    //Add the icon to the Overview tab
-                    this.$(".attributes-tab a").prepend(errorIcon).addClass("error");
-                  }
+              return;
+            }
 
+            //Get all the elements for this category and add the error class
+            this.$(
+              ".overview-container [data-category='" + category + "']",
+            ).addClass("error");
+            //Get the notification element for this category and add the error message
+            this.$(
+              ".overview-container .notification[data-category='" +
+                category +
+                "']",
+            ).text(errorMsg);
+
+            //Create an error icon for the Overview tab
+            if (!overviewTabErrorIcon) {
+              var errorIcon = $(document.createElement("i"))
+                .addClass("icon icon-on-left icon-exclamation-sign error")
+                .attr("title", "There is missing information in this tab");
+
+              //Add the icon to the Overview tab
+              this.$(".overview-tab a").prepend(errorIcon).addClass("error");
+
+              overviewTabErrorIcon = true;
+            }
+          },
+          this,
+        );
+      },
+
+      /**
+       * Show the entity overview or attributes tab
+       * depending on the click target
+       * @param {Event} e
+       */
+      showTab: function (e) {
+        e.preventDefault();
+
+        //Get the clicked link
+        var link = $(e.target);
+
+        //Remove the active class from all links and add it to the new active link
+        this.$(".entity-container > .nav-tabs li").removeClass("active");
+        link.parent("li").addClass("active");
+
+        //Hide all the panes and show the correct one
+        this.$(".entity-container > .tab-content > .tab-pane").hide();
+        this.$(link.attr("href")).show();
+      },
+
+      /**
+       * Show the entity in a modal dialog
+       */
+      show: function () {
+        this.$el.modal("show");
+      },
+
+      /**
+       * Hide the entity modal dialog
+       */
+      hide: function () {
+        this.$el.modal("hide");
+      },
+
+      /**
+       * Save a draft of the parent EML model
+       */
+      saveDraft: function () {
+        var view = this;
+
+        try {
+          var model = this.model.getParentEML();
+          var draftModel = model.clone();
+          var title = model.get("title") || "No title";
+
+          LocalForage.setItem(model.get("id"), {
+            id: model.get("id"),
+            datetime: new Date().toISOString(),
+            title: Array.isArray(title) ? title[0] : title,
+            draft: draftModel.serialize(),
+          }).then(function () {
+            view.clearOldDrafts();
+          });
+        } catch (ex) {
+          console.log("Error saving draft:", ex);
+        }
+      },
+
+      /**
+       * Clear older drafts by iterating over the sorted list of drafts
+       * stored by LocalForage and removing any beyond a hardcoded limit.
+       */
+      clearOldDrafts: function () {
+        var drafts = [];
+
+        try {
+          LocalForage.iterate(function (value, key, iterationNumber) {
+            // Extract each draft
+            drafts.push({
+              key: key,
+              value: value,
+            });
+          })
+            .then(function () {
+              // Sort by datetime
+              drafts = _.sortBy(drafts, function (draft) {
+                return draft.value.datetime.toString();
+              }).reverse();
+            })
+            .then(function () {
+              _.each(drafts, function (draft, i) {
+                var age = new Date() - new Date(draft.value.datetime);
+                var isOld = age / 2678400000 > 1; // ~31days
+                // Delete this draft is not in the most recent 100 or
+                // if older than 31 days
+                var shouldDelete = i > 100 || isOld;
+                if (!shouldDelete) {
                   return;
                 }
 
-                //Get all the elements for this category and add the error class
-                this.$(".overview-container [data-category='" + category + "']").addClass("error");
-                //Get the notification element for this category and add the error message
-                this.$(".overview-container .notification[data-category='" + category + "']").text(errorMsg);
-
-                //Create an error icon for the Overview tab
-                if( !overviewTabErrorIcon ){
-                  var errorIcon = $(document.createElement("i"))
-                                    .addClass("icon icon-on-left icon-exclamation-sign error")
-                                    .attr("title", "There is missing information in this tab");
-
-                  //Add the icon to the Overview tab
-                  this.$(".overview-tab a").prepend(errorIcon).addClass("error");
-
-                  overviewTabErrorIcon = true;
-                }
-
-              }, this);
-
-            },
-
-            /**
-             * Show the entity overview or attributes tab
-             * depending on the click target
-             * @param {Event} e
-             */
-            showTab: function(e){
-              e.preventDefault();
-
-              //Get the clicked link
-               var link = $(e.target);
-
-               //Remove the active class from all links and add it to the new active link
-               this.$(".entity-container > .nav-tabs li").removeClass("active");
-               link.parent("li").addClass("active");
-
-               //Hide all the panes and show the correct one
-               this.$(".entity-container > .tab-content > .tab-pane").hide();
-               this.$(link.attr("href")).show();
-
-            },
-
-            /**
-             * Show the entity in a modal dialog
-             */
-            show: function(){
-
-              this.$el.modal('show');
-
-            },
-
-            /**
-             * Hide the entity modal dialog
-             */
-            hide: function(){
-              this.$el.modal('hide');
-            },
-
-            /**
-             * Save a draft of the parent EML model
-             */
-            saveDraft: function() {
-              var view = this;
-
-              try {
-                var model = this.model.getParentEML();
-                var draftModel = model.clone();
-                var title = model.get("title") || "No title";
-
-                LocalForage.setItem(model.get("id"),
-                {
-                  id: model.get("id"),
-                  datetime: (new Date()).toISOString(),
-                  title: Array.isArray(title) ? title[0] : title,
-                  draft: draftModel.serialize()
-                }).then(function() {
-                  view.clearOldDrafts();
+                LocalForage.removeItem(draft.key).then(function () {
+                  // Item should be removed
                 });
-              } catch (ex) {
-                console.log("Error saving draft:", ex);
-              }
-            },
-
-            /**
-             * Clear older drafts by iterating over the sorted list of drafts
-             * stored by LocalForage and removing any beyond a hardcoded limit.
-             */
-             clearOldDrafts: function() {
-               var drafts = [];
-
-              try {
-                LocalForage.iterate(function(value, key, iterationNumber) {
-                // Extract each draft
-                drafts.push({
-                    key: key,
-                    value: value
-                  });
-                }).then(function(){
-                  // Sort by datetime
-                  drafts = _.sortBy(drafts, function(draft) {
-                    return draft.value.datetime.toString();
-                  }).reverse();
-                }).then(function() {
-                  _.each(drafts, function(draft, i) {
-                    var age = (new Date()) - new Date(draft.value.datetime);
-                    var isOld = (age / 2678400000) > 1; // ~31days
-                    // Delete this draft is not in the most recent 100 or
-                    // if older than 31 days
-                    var shouldDelete = i > 100 || isOld;
-                      if (!shouldDelete) {
-                        return;
-                      }
-
-                      LocalForage.removeItem(draft.key).then(function() {
-                        // Item should be removed
-                      });
-                    })
-                  });
-              }
-              catch (ex) {
-                console.log("Failed to clear old drafts: ", ex);
-              }
-            },
-
-            /**
-             * Handle the click event on the fill button
-             *
-             * @param {Event} e - The click event
-             * @since 2.15.0
-             */
-            handleFill: function(e) {
-              var d1Object = this.model.get("dataONEObject");
-
-              if (!d1Object) {
-                return;
-              }
-
-              var file = d1Object.get("uploadFile");
-
-              try {
-                if (!file) {
-                  this.handleFillViaFetch();
-                } else {
-                  this.handleFillViaFile(file);
-                }
-              } catch (error) {
-                console.log("Error while attempting to fill", error);
-                view.updateFillButton(
-                  '<i class="icon-warning-sign"></i> Couldn\'t fill'
-                );
-              }
-            },
-
-            /**
-             * Handle the fill event using a File object
-             *
-             * @param {File} file - A File object to fill from
-             * @since 2.15.0
-             */
-            handleFillViaFile: function(file) {
-              var view = this;
-
-              Utilities.readSlice(file, this, function (event) {
-                if (event.target.readyState !== FileReader.DONE) {
-                  return;
-                }
-
-                view.tryParseAndFillAttributeNames.bind(view)(event.target.result);
               });
-            },
-
-            /**
-             * Handle the fill event by fetching the object
-             * @since 2.15.0
-             */
-            handleFillViaFetch: function() {
-              var view = this;
-
-              var requestSettings = {
-                url:  MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(this.model.get("dataONEObject").get("id")),
-                method: "get",
-                success: view.tryParseAndFillAttributeNames.bind(this),
-                error: function(error) {
-                  view.updateFillButton('<i class="icon-warning-sign"></i> Couldn\'t fill');
-                  console.error("Error fetching DataObject to parse out headers", error);
-                }
-              }
-
-              this.updateFillButton('<i class="icon-time"></i> Please wait...', true);
-              this.disableFillButton();
-
-              requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());
-              $.ajax(requestSettings);
-            },
-
-            /**
-             * Attempt to parse header and fill attributes names
-             *
-             * @param {string} content - Part of a file to attempt to parse
-             * @since 2.15.0
-             */
-            tryParseAndFillAttributeNames: function(content) {
-              var names = Utilities.tryParseCSVHeader(content);
-
-              if (names.length === 0) {
-                this.updateFillButton('<i class="icon-warning-sign"></i> Couldn\'t fill');
-              } else {
-                this.updateFillButton('<i class="icon-ok"></i> Filled!');
-              }
-
-              //Make sure the button is enabled
-              this.enableFillButton();
-
-              this.updateAttributeNames(names);
-            },
-
-            /**
-             * Update attribute names from an array
-             *
-             * This will update existing attributes' names or create new
-             * attributes as needed. This also performs a full re-render.
-             *
-             * @param {string[]} names - A list of names to apply
-             * @since 2.15.0
-             */
-            updateAttributeNames: function(names) {
-              if (!names) {
-                return;
-              }
-
-              var attributes = this.model.get("attributeList");
-
-              //Update the name of each attribute or create a new Attribute if one doesn't exist
-              for (var i = 0; i < names.length; i++) {
-                if (attributes.length - 1 >= i) {
-                  attributes[i].set("attributeName", names[i]);
-                } else {
-                  attributes.push(
-                    new EMLAttribute({
-                      parentModel: this.model,
-                      xmlID: DataONEObject.generateId(),
-                      attributeName: names[i],
-                    })
-                  );
-                }
-              }
-
-              //Update the attribute list
-              this.model.set("attributeList", attributes);
-
-              // Reset first
-              this.$(".attribute-menu.side-nav-items").empty();
-              this.$(".eml-attribute").remove();
-
-              // Then re-render
-              this.renderAttributes();
-            },
-
-            /**
-             * Update the Fill button temporarily and set it back to the default
-             *
-             * Used to show success or failure of the filling operation
-             *
-             * @param {string} messageHTML - HTML template string to set
-             *   temporarily
-             * @param {boolean} disableTimeout - If true, the timeout will not be set
-             * @since 2.15.0
-             */
-            updateFillButton: function(messageHTML, disableTimeout) {
-              var view = this;
-
-              this.$(".fill-button").html(messageHTML);
-
-              if( !disableTimeout ){
-                window.setTimeout(function () {
-                  view.$(".fill-button-container").html(view.fillButtonTemplateString);
-                }, 3000);
-              }
-            },
-
-            /**
-            * Disable the Fill Attributes button
-            * @since 2.15.0
-            */
-            disableFillButton: function(){
-              this.$(".fill-button").prop("disabled", true);
-            },
-
-            /**
-            * Enable the Fill Attributes button
-            * @since 2.15.0
-            */
-            enableFillButton: function(){
-              this.$(".fill-button").prop("disabled", false);
-            }
-          });
-
-        return EMLEntityView;
+            });
+        } catch (ex) {
+          console.log("Failed to clear old drafts: ", ex);
+        }
+      },
+
+      /**
+       * Handle the click event on the fill button
+       *
+       * @param {Event} e - The click event
+       * @since 2.15.0
+       */
+      handleFill: function (e) {
+        var d1Object = this.model.get("dataONEObject");
+
+        if (!d1Object) {
+          return;
+        }
+
+        var file = d1Object.get("uploadFile");
+
+        try {
+          if (!file) {
+            this.handleFillViaFetch();
+          } else {
+            this.handleFillViaFile(file);
+          }
+        } catch (error) {
+          console.log("Error while attempting to fill", error);
+          view.updateFillButton(
+            '<i class="icon-warning-sign"></i> Couldn\'t fill',
+          );
+        }
+      },
+
+      /**
+       * Handle the fill event using a File object
+       *
+       * @param {File} file - A File object to fill from
+       * @since 2.15.0
+       */
+      handleFillViaFile: function (file) {
+        var view = this;
+
+        Utilities.readSlice(file, this, function (event) {
+          if (event.target.readyState !== FileReader.DONE) {
+            return;
+          }
+
+          view.tryParseAndFillAttributeNames.bind(view)(event.target.result);
+        });
+      },
+
+      /**
+       * Handle the fill event by fetching the object
+       * @since 2.15.0
+       */
+      handleFillViaFetch: function () {
+        var view = this;
+
+        var requestSettings = {
+          url:
+            MetacatUI.appModel.get("objectServiceUrl") +
+            encodeURIComponent(this.model.get("dataONEObject").get("id")),
+          method: "get",
+          success: view.tryParseAndFillAttributeNames.bind(this),
+          error: function (error) {
+            view.updateFillButton(
+              '<i class="icon-warning-sign"></i> Couldn\'t fill',
+            );
+            console.error(
+              "Error fetching DataObject to parse out headers",
+              error,
+            );
+          },
+        };
+
+        this.updateFillButton('<i class="icon-time"></i> Please wait...', true);
+        this.disableFillButton();
+
+        requestSettings = _.extend(
+          requestSettings,
+          MetacatUI.appUserModel.createAjaxSettings(),
+        );
+        $.ajax(requestSettings);
+      },
+
+      /**
+       * Attempt to parse header and fill attributes names
+       *
+       * @param {string} content - Part of a file to attempt to parse
+       * @since 2.15.0
+       */
+      tryParseAndFillAttributeNames: function (content) {
+        var names = Utilities.tryParseCSVHeader(content);
+
+        if (names.length === 0) {
+          this.updateFillButton(
+            '<i class="icon-warning-sign"></i> Couldn\'t fill',
+          );
+        } else {
+          this.updateFillButton('<i class="icon-ok"></i> Filled!');
+        }
+
+        //Make sure the button is enabled
+        this.enableFillButton();
+
+        this.updateAttributeNames(names);
+      },
+
+      /**
+       * Update attribute names from an array
+       *
+       * This will update existing attributes' names or create new
+       * attributes as needed. This also performs a full re-render.
+       *
+       * @param {string[]} names - A list of names to apply
+       * @since 2.15.0
+       */
+      updateAttributeNames: function (names) {
+        if (!names) {
+          return;
+        }
+
+        var attributes = this.model.get("attributeList");
+
+        //Update the name of each attribute or create a new Attribute if one doesn't exist
+        for (var i = 0; i < names.length; i++) {
+          if (attributes.length - 1 >= i) {
+            attributes[i].set("attributeName", names[i]);
+          } else {
+            attributes.push(
+              new EMLAttribute({
+                parentModel: this.model,
+                xmlID: DataONEObject.generateId(),
+                attributeName: names[i],
+              }),
+            );
+          }
+        }
+
+        //Update the attribute list
+        this.model.set("attributeList", attributes);
+
+        // Reset first
+        this.$(".attribute-menu.side-nav-items").empty();
+        this.$(".eml-attribute").remove();
+
+        // Then re-render
+        this.renderAttributes();
+      },
+
+      /**
+       * Update the Fill button temporarily and set it back to the default
+       *
+       * Used to show success or failure of the filling operation
+       *
+       * @param {string} messageHTML - HTML template string to set
+       *   temporarily
+       * @param {boolean} disableTimeout - If true, the timeout will not be set
+       * @since 2.15.0
+       */
+      updateFillButton: function (messageHTML, disableTimeout) {
+        var view = this;
+
+        this.$(".fill-button").html(messageHTML);
+
+        if (!disableTimeout) {
+          window.setTimeout(function () {
+            view
+              .$(".fill-button-container")
+              .html(view.fillButtonTemplateString);
+          }, 3000);
+        }
+      },
+
+      /**
+       * Disable the Fill Attributes button
+       * @since 2.15.0
+       */
+      disableFillButton: function () {
+        this.$(".fill-button").prop("disabled", true);
+      },
+
+      /**
+       * Enable the Fill Attributes button
+       * @since 2.15.0
+       */
+      enableFillButton: function () {
+        this.$(".fill-button").prop("disabled", false);
+      },
+    },
+  );
+
+  return EMLEntityView;
 });
 
diff --git a/docs/docs/src_js_views_metadata_EMLGeoCoverageView.js.html b/docs/docs/src_js_views_metadata_EMLGeoCoverageView.js.html index 0088eb391..bb58da9b2 100644 --- a/docs/docs/src_js_views_metadata_EMLGeoCoverageView.js.html +++ b/docs/docs/src_js_views_metadata_EMLGeoCoverageView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/metadata/EMLGeoCoverageView.js

-
/* global define */
-define([
+            
define([
   "underscore",
   "jquery",
   "backbone",
@@ -131,7 +130,7 @@ 

Source: src/js/views/metadata/EMLGeoCoverageView.js

this.editTemplate({ edit: this.edit, model: this.model.toJSON(), - }) + }), ); if (this.isNew) { @@ -173,10 +172,10 @@

Source: src/js/views/metadata/EMLGeoCoverageView.js

//Are the NW and SE points the same? i.e. is this a single point and not //a box? var isSinglePoint = - this.model.get("north") != null && - this.model.get("north") == this.model.get("south") && - this.model.get("west") != null && - this.model.get("west") == this.model.get("east"), + this.model.get("north") != null && + this.model.get("north") == this.model.get("south") && + this.model.get("west") != null && + this.model.get("west") == this.model.get("east"), hasEmptyInputs = this.$("[data-attribute='north']").val() == "" || this.$("[data-attribute='south']").val() == "" || @@ -251,7 +250,7 @@

Source: src/js/views/metadata/EMLGeoCoverageView.js

} else { //Find out if we are missing a complete NW or SE point var isMissingNWPoint = - this.model.get("north") == null && this.model.get("west") == null, + this.model.get("north") == null && this.model.get("west") == null, isMissingSEPoint = this.model.get("south") == null && this.model.get("east") == null; @@ -285,7 +284,7 @@

Source: src/js/views/metadata/EMLGeoCoverageView.js

this.model.get("parentModel").type == "EML" && _.contains( MetacatUI.rootDataPackage.models, - this.model.get("parentModel") + this.model.get("parentModel"), ) ) { MetacatUI.rootDataPackage.packageModel.set("changed", true); @@ -307,7 +306,7 @@

Source: src/js/views/metadata/EMLGeoCoverageView.js

//Query for the EMlGeoCoverageView element that the user is actively //interacting with var activeGeoCovEl = $(document.activeElement).parents( - ".eml-geocoverage" + ".eml-geocoverage", ); //If the user is not actively in this view, then exit @@ -347,7 +346,7 @@

Source: src/js/views/metadata/EMLGeoCoverageView.js

// Highlight the fields that need to be fixed fields.forEach((field) => { this.$("[data-attribute='" + field + "']").addClass("error"); - }) + }); // Show the combined error message this.$(".notification").text(errorMessages).addClass("error"); }, @@ -366,7 +365,7 @@

Source: src/js/views/metadata/EMLGeoCoverageView.js

this.$el.removeClass("new"); this.isNew = false; }, - } + }, ); return EMLGeoCoverageView; diff --git a/docs/docs/src_js_views_metadata_EMLMeasurementScaleView.js.html b/docs/docs/src_js_views_metadata_EMLMeasurementScaleView.js.html index fa6401cdf..afe53c8e7 100644 --- a/docs/docs/src_js_views_metadata_EMLMeasurementScaleView.js.html +++ b/docs/docs/src_js_views_metadata_EMLMeasurementScaleView.js.html @@ -44,687 +44,776 @@

Source: src/js/views/metadata/EMLMeasurementScaleView.js<
-
/* global define */
-define(['underscore', 'jquery', 'backbone',
-        'models/DataONEObject',
-        'models/metadata/eml211/EMLMeasurementScale',
-        'text!templates/metadata/eml-measurement-scale.html',
-        'text!templates/metadata/codelist-row.html',
-        'text!templates/metadata/nonNumericDomain.html',
-        'text!templates/metadata/textDomain.html'],
-    function(_, $, Backbone, DataONEObject, EMLMeasurementScale,
-    		EMLMeasurementScaleTemplate, CodeListRowTemplate, NonNumericDomainTemplate, TextDomainTemplate){
-
-        /**
-        * @class EMLMeasurementScaleView
-        * @classdesc An EMLMeasurementScaleView displays the info about one the measurement scale or category of an eml attribute
-        * @classcategory Views/Metadata
-        * @extends Backbone.View
-        */
-        var EMLMeasurementScaleView = Backbone.View.extend(
-          /** @lends EMLMeasurementScaleView.prototype */{
-
-            tagName: "div",
-
-            className: "eml-measurement-scale",
-
-            id: null,
-
-            /* The HTML template for a measurement scale */
-            template: _.template(EMLMeasurementScaleTemplate),
-            codeListRowTemplate: _.template(CodeListRowTemplate),
-            nonNumericDomainTemplate: _.template(NonNumericDomainTemplate),
-            textDomainTemplate: _.template(TextDomainTemplate),
-
-            /* Events this view listens to */
-            events: {
-            	"click  .category"        : "switchCategory",
-            	"change .datetime-string" : "toggleCustomDateTimeFormat",
-            	"change .possible-text"   : "toggleNonNumericDomain",
-            	"keyup  .new .codelist"   : "addNewCodeRow",
-            	"click .code-row .remove" : "removeCodeRow",
-            	"mouseover .code-row .remove" : "previewCodeRemove",
-            	"mouseout .code-row .remove"  : "previewCodeRemove",
-            	"change .units"           : "updateModel",
-            	"change .datetime" 		  : "updateModel",
-            	"change .codelist"        : "updateModel",
-            	"change .textDomain"      : "updateModel",
-            	"focusout .code-row"      : "showValidation",
-            	"focusout .units.input"   : "showValidation"
-            },
-
-            initialize: function(options){
-            	if(!options)
-            		var options = {};
-
-            	this.isNew = (options.isNew === true) ? true : this.model? false : true;
-            	this.model = options.model || EMLMeasurementScale.getInstance();
-            	this.parentView = options.parentView || null;
-            },
-
-            render: function(){
-
-            	//Render the template
-            	var viewHTML = this.template(this.model.toJSON());
-
-            	if(this.isNew)
-            		this.$el.addClass("new");
-
-            	//Insert the template HTML
-            	this.$el.html(viewHTML);
-
-            	//Render any nonNumericDomain models
-        		this.$(".non-numeric-domain").append( this.nonNumericDomainTemplate(this.model.get("nonNumericDomain")) );
-
-        		//Render the text domain choices and details
-        		this.$(".text-domain").html( this.textDomainTemplate() );
-
-        		//If this attribute is already defined as nonNumericDomain, then fill in the metadata
-        		_.each(this.model.get("nonNumericDomain"), function(domain){
-
-        			var nominalTextDomain = this.$(".nominal-options .text-domain"),
-        				ordinalTextDomain = this.$(".ordinal-options .text-domain");
-
-        			if(domain.textDomain){
-            			if(this.model.get("measurementScale") == "nominal"){
-            				nominalTextDomain.html( this.textDomainTemplate(domain.textDomain) );
-            			}
-            			else{
-            				ordinalTextDomain.html( this.textDomainTemplate(domain.textDomain) );
-            			}
-
-            		}
-        			else if(domain.enumeratedDomain){
-        				this.renderCodeList(domain.enumeratedDomain);
-        			}
-
-        		}, this);
-
-        		//Add the new code rows in the code list table
-    			this.addNewCodeRow("nominal");
-    			this.addNewCodeRow("ordinal");
-
-            },
-
-            postRender: function(){
-            	//Determine which category to select
-            	//Interval measurement scales will be displayed as ratio
-            	var selectedCategory = this.model.get("measurementScale") == "interval" ? "ratio" : this.model.get("measurementScale");
-
-            	//Set the category
-    			this.$(".category[value='" + selectedCategory + "']").prop("checked", true);
-        		this.switchCategory();
-
-        		this.renderUnitDropdown();
-
-            	this.chooseDateTimeFormat();
-
-            	this.chooseNonNumericDomain();
-            },
-
-            /*
-             * Render the table of code definitions from the enumeratedDomain node of the EML
-             */
-            renderCodeList: function(codeList){
-
-            	var scaleType  = this.model.get("measurementScale"),
-            		$container = this.$("." + scaleType + "-options .enumeratedDomain.non-numeric-domain-type .table");
-
-            	_.each(codeList.codeDefinition, function(definition){
-            		var row = this.codeListRowTemplate(definition);
-
-            		//Add the row to the table
-            		$container.append(row);
-            	}, this);
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/DataONEObject",
+  "models/metadata/eml211/EMLMeasurementScale",
+  "text!templates/metadata/eml-measurement-scale.html",
+  "text!templates/metadata/codelist-row.html",
+  "text!templates/metadata/nonNumericDomain.html",
+  "text!templates/metadata/textDomain.html",
+], function (
+  _,
+  $,
+  Backbone,
+  DataONEObject,
+  EMLMeasurementScale,
+  EMLMeasurementScaleTemplate,
+  CodeListRowTemplate,
+  NonNumericDomainTemplate,
+  TextDomainTemplate,
+) {
+  /**
+   * @class EMLMeasurementScaleView
+   * @classdesc An EMLMeasurementScaleView displays the info about one the measurement scale or category of an eml attribute
+   * @classcategory Views/Metadata
+   * @extends Backbone.View
+   */
+  var EMLMeasurementScaleView = Backbone.View.extend(
+    /** @lends EMLMeasurementScaleView.prototype */ {
+      tagName: "div",
+
+      className: "eml-measurement-scale",
+
+      id: null,
+
+      /* The HTML template for a measurement scale */
+      template: _.template(EMLMeasurementScaleTemplate),
+      codeListRowTemplate: _.template(CodeListRowTemplate),
+      nonNumericDomainTemplate: _.template(NonNumericDomainTemplate),
+      textDomainTemplate: _.template(TextDomainTemplate),
+
+      /* Events this view listens to */
+      events: {
+        "click  .category": "switchCategory",
+        "change .datetime-string": "toggleCustomDateTimeFormat",
+        "change .possible-text": "toggleNonNumericDomain",
+        "keyup  .new .codelist": "addNewCodeRow",
+        "click .code-row .remove": "removeCodeRow",
+        "mouseover .code-row .remove": "previewCodeRemove",
+        "mouseout .code-row .remove": "previewCodeRemove",
+        "change .units": "updateModel",
+        "change .datetime": "updateModel",
+        "change .codelist": "updateModel",
+        "change .textDomain": "updateModel",
+        "focusout .code-row": "showValidation",
+        "focusout .units.input": "showValidation",
+      },
+
+      initialize: function (options) {
+        if (!options) var options = {};
+
+        this.isNew = options.isNew === true ? true : this.model ? false : true;
+        this.model = options.model || EMLMeasurementScale.getInstance();
+        this.parentView = options.parentView || null;
+      },
+
+      render: function () {
+        //Render the template
+        var viewHTML = this.template(this.model.toJSON());
+
+        if (this.isNew) this.$el.addClass("new");
+
+        //Insert the template HTML
+        this.$el.html(viewHTML);
+
+        //Render any nonNumericDomain models
+        this.$(".non-numeric-domain").append(
+          this.nonNumericDomainTemplate(this.model.get("nonNumericDomain")),
+        );
+
+        //Render the text domain choices and details
+        this.$(".text-domain").html(this.textDomainTemplate());
+
+        //If this attribute is already defined as nonNumericDomain, then fill in the metadata
+        _.each(
+          this.model.get("nonNumericDomain"),
+          function (domain) {
+            var nominalTextDomain = this.$(".nominal-options .text-domain"),
+              ordinalTextDomain = this.$(".ordinal-options .text-domain");
+
+            if (domain.textDomain) {
+              if (this.model.get("measurementScale") == "nominal") {
+                nominalTextDomain.html(
+                  this.textDomainTemplate(domain.textDomain),
+                );
+              } else {
+                ordinalTextDomain.html(
+                  this.textDomainTemplate(domain.textDomain),
+                );
+              }
+            } else if (domain.enumeratedDomain) {
+              this.renderCodeList(domain.enumeratedDomain);
+            }
+          },
+          this,
+        );
+
+        //Add the new code rows in the code list table
+        this.addNewCodeRow("nominal");
+        this.addNewCodeRow("ordinal");
+      },
+
+      postRender: function () {
+        //Determine which category to select
+        //Interval measurement scales will be displayed as ratio
+        var selectedCategory =
+          this.model.get("measurementScale") == "interval"
+            ? "ratio"
+            : this.model.get("measurementScale");
+
+        //Set the category
+        this.$(".category[value='" + selectedCategory + "']").prop(
+          "checked",
+          true,
+        );
+        this.switchCategory();
+
+        this.renderUnitDropdown();
+
+        this.chooseDateTimeFormat();
+
+        this.chooseNonNumericDomain();
+      },
+
+      /*
+       * Render the table of code definitions from the enumeratedDomain node of the EML
+       */
+      renderCodeList: function (codeList) {
+        var scaleType = this.model.get("measurementScale"),
+          $container = this.$(
+            "." +
+              scaleType +
+              "-options .enumeratedDomain.non-numeric-domain-type .table",
+          );
+
+        _.each(
+          codeList.codeDefinition,
+          function (definition) {
+            var row = this.codeListRowTemplate(definition);
+
+            //Add the row to the table
+            $container.append(row);
+          },
+          this,
+        );
+      },
+
+      showValidation: function (e) {
+        //Reset the error messages and styling
+        this.$(".error").removeClass("error");
+        this.$(".notification").text("");
+
+        //If the measurement scale model is NOT valid
+        if (!this.$(".category:checked").length) {
+          this.$(".category-container")
+            .addClass("error")
+            .find(".notification")
+            .text("Choose a category")
+            .addClass("error");
+
+          //Trigger the invalid event on the attribute model
+          this.model
+            .get("parentModel")
+            .trigger("invalid", this.model.get("parentModel"));
+        } else if (!this.model.isValid()) {
+          //Get the errors
+          var errors = this.model.validationError,
+            modelType = this.model.get("measurementScale");
+
+          //Display error messages for each type of error
+          _.each(
+            Object.keys(errors),
+            function (attr) {
+              //If this is an enumeratedDomain error
+              if (attr == "enumeratedDomain") {
+                var view = this;
+
+                //Give the user a few milliseconds to focus on a new element
+                setTimeout(function () {
+                  //Highlight the inputs in code rows that are empty
+                  var emptyInputs = view
+                    .$("." + modelType + "-options .codelist.input")
+                    .not(document.activeElement)
+                    .filter(function () {
+                      if ($(this).val()) return false;
+                      else return true;
+                    });
+                  emptyInputs.addClass("error");
+
+                  if (emptyInputs.length)
+                    view
+                      .$(
+                        "." +
+                          modelType +
+                          "-options [data-category='enumeratedDomain'] .notification",
+                      )
+                      .text(errors[attr])
+                      .addClass("error");
+                }, 200);
+              }
+              //For all other attributes, just display the errors the same way
+              else {
+                this.$(
+                  "." +
+                    modelType +
+                    "-options [data-category='" +
+                    attr +
+                    "'] .notification",
+                )
+                  .text(errors[attr])
+                  .addClass("error");
+                this.$(
+                  "." +
+                    modelType +
+                    "-options .input[data-category='" +
+                    attr +
+                    "']",
+                ).addClass("error");
+              }
 
+              //Highlight the border of the non numeric domain container
+              if (attr == "nonNumericDomain") {
+                this.$(
+                  "." + modelType + "-options.non-numeric-domain",
+                ).addClass("error");
+              }
             },
+            this,
+          );
+
+          //Trigger the invalid event on the attribute model
+          //	this.model.get("parentModel").trigger("invalid", this.model.get("parentModel"));
+        } else {
+          //Trigger the valid event on the attribute model
+          //	this.model.get("parentModel").trigger("valid", this.model.get("parentModel"));
+        }
+      },
+
+      switchCategory: function () {
+        //Switch the category in the view
+        var chosenCategory = this.$(
+          "input[name='measurementScale']:checked",
+        ).val();
+
+        //Show the new category options
+        this.$(".options").hide();
+        this.$("." + chosenCategory + "-options.options").show();
+
+        //Get the current category
+        var modelCategory = this.model.get("measurementScale");
+
+        //Get the parent attribute model
+        var parentEMLAttrModel = this.model.get("parentModel");
+
+        //Switch the model type, if needed
+        if (
+          chosenCategory &&
+          modelCategory != chosenCategory &&
+          !(modelCategory == "interval" && chosenCategory == "ratio")
+        ) {
+          var newModel;
+
+          if (typeof this.modelCache != "object") {
+            this.modelCache = {};
+          }
 
-            showValidation: function(e){
-
-				//Reset the error messages and styling
-				this.$(".error").removeClass("error");
-				this.$(".notification").text("");
-
-				//If the measurement scale model is NOT valid
-				if( !this.$(".category:checked").length ){
-					this.$(".category-container")
-						.addClass("error")
-						.find(".notification")
-						.text("Choose a category")
-						.addClass("error");
-
-					//Trigger the invalid event on the attribute model
-                	this.model.get("parentModel").trigger("invalid", this.model.get("parentModel"));
+          //Get the model type from this view's cache
+          if (this.modelCache[chosenCategory])
+            newModel = this.modelCache[chosenCategory];
+          else if (chosenCategory == "ratio" && this.modelCache["interval"])
+            newModel = this.modelCache["interval"];
+          //Get a new model instance based on the type
+          else newModel = EMLMeasurementScale.getInstance(chosenCategory);
+
+          //Save this model for later in case the user switches back
+          if (modelCategory) this.modelCache[modelCategory] = this.model;
+
+          //save the new model
+          this.model = newModel;
+
+          //Set references to and from this model and the parent attribute model
+          this.model.set("parentModel", parentEMLAttrModel);
+          parentEMLAttrModel.set("measurementScale", this.model);
+
+          //Update the codelist values, if needed
+          if (
+            chosenCategory == "nominal" ||
+            (chosenCategory == "ordinal" &&
+              this.model.get("nonNumericDomain").length &&
+              this.model.get("nonNumericDomain")[0].enumeratedDomain)
+          ) {
+            this.updateCodeList();
+          }
+        }
+      },
+
+      renderUnitDropdown: function () {
+        if (this.$("select.units").length) return;
+
+        //Create a dropdown menu
+        var select = $(document.createElement("select"))
+          .addClass("units full-width input")
+          .attr("data-category", "unit");
+
+        var eml = this.model.getParentEML();
+
+        //Get the units collection or wait until it has been fetched
+        if (!eml.units.length) {
+          this.listenTo(eml.units, "sync", this.renderUnitDropdown);
+          return;
+        }
+
+        //Create a default option
+        var defaultOption = $(document.createElement("option")).text(
+          "Choose a standard unit",
+        );
+        select.append(defaultOption);
+
+        //Create an "Other" option to show at the top
+        var otherOption = $(document.createElement("option"))
+          .text("Other / None")
+          .attr("value", "dimensionless");
+        select.append(otherOption);
+
+        //Create each unit option in the unit dropdown
+        eml.units.each(function (unit) {
+          var option = $(document.createElement("option"))
+            .val(unit.get("_name"))
+            .text(
+              unit.get("_name").charAt(0).toUpperCase() +
+                unit.get("_name").slice(1) +
+                " (" +
+                unit.get("description") +
+                ")",
+            )
+            .data({ model: unit });
+          select.append(option);
+        }, this);
+
+        //Add the dropdown to the page
+        this.$(".units-container").append(select);
+
+        //Select the unit from the EML, if there is one
+        var currentUnit = this.model.get("unit");
+        if (currentUnit && currentUnit.standardUnit) {
+          //Get the dropdown for this measurement scale
+          // (We default interval to ratio in the editor)
+          var currentDropdown = this.$(".ratio-options select");
+
+          //Select the unit from the EML
+          currentDropdown.val(currentUnit.standardUnit);
+        }
+        //If this unit is a custom unit
+        else if (currentUnit && currentUnit.customUnit) {
+          //Create an <option> for this custom unit
+          var customUnitOption = $(document.createElement("option"))
+            .val(currentUnit.customUnit)
+            .text(currentUnit.customUnit)
+            .addClass("custom");
+
+          //Add it to the <select> and select it as the active option
+          select.append(customUnitOption).val(currentUnit.customUnit);
+        }
+      },
+
+      /*
+       *  Chooses the date-time format from the dropdown menu
+       */
+      chooseDateTimeFormat: function () {
+        if (this.model.type == "EMLDateTimeDomain") {
+          var formatString = this.model.get("formatString");
+
+          //Go back to the default option when the model isn't set yet
+          if (!formatString) {
+            var options = this.$("select.datetime-string option");
+            this.$("select.datetime-string").val(options.first().val());
+            return;
+          }
 
-				}
-				else if( !this.model.isValid() ){
-            		//Get the errors
-            		var errors = this.model.validationError,
-            			modelType = this.model.get("measurementScale");
+          var matchingOption = this.$(
+            "select.datetime-string [value='" + formatString + "']",
+          );
+
+          if (matchingOption.length) {
+            this.$("select.datetime-string").val(formatString);
+            this.$(".datetime-string-custom-container").hide();
+          } else {
+            this.$("select.datetime-string").val("custom");
+            this.$(".datetime-string-custom").val(formatString);
+            this.$(".datetime-string-custom-container").show();
+          }
+        }
+      },
+
+      toggleCustomDateTimeFormat: function (e) {
+        var choice = this.$("select.datetime-string").val();
+
+        if (choice == "custom") {
+          this.$(".datetime-string-custom-container").show();
+        } else {
+          this.$(".datetime-string-custom-container").hide();
+        }
+      },
+
+      chooseNonNumericDomain: function () {
+        if (
+          this.model.get("nonNumericDomain") &&
+          this.model.get("nonNumericDomain").length
+        ) {
+          //Hide all the details first
+          this.$(".non-numeric-domain-type").hide();
+
+          //Get the domain from the model
+          var domain = this.model.get("nonNumericDomain")[0];
+
+          //If the domain type is text, select it and show it
+          if (domain.textDomain) {
+            //If the pattern is just a wildcard, then check the "anything" radio button
+            if (
+              domain.textDomain.pattern &&
+              domain.textDomain.pattern.length &&
+              domain.textDomain.pattern[0] == "*"
+            )
+              this.$(
+                "." +
+                  this.model.get("measurementScale") +
+                  "-options .possible-text[value='anything']",
+              ).prop("checked", true);
+            //Otherwise, check the pattern radio button
+            else {
+              this.$(
+                "." +
+                  this.model.get("measurementScale") +
+                  "-options .possible-text[value='pattern']",
+              ).prop("checked", true);
+              this.$(
+                "." +
+                  this.model.get("measurementScale") +
+                  "-options .non-numeric-domain-type.pattern",
+              ).show();
+            }
+          }
+          //If the domain type is a code list, select it and show it
+          else if (domain.enumeratedDomain) {
+            this.$(
+              "." +
+                this.model.get("measurementScale") +
+                "-options .possible-text[value='enumeratedDomain']",
+            ).prop("checked", true);
+            this.$(".non-numeric-domain-type.enumeratedDomain").show();
+          }
+        }
+      },
 
-            		//Display error messages for each type of error
-            		_.each(Object.keys(errors), function(attr){
+      toggleNonNumericDomain: function (e) {
+        //Hide the domain type details
+        this.$(".non-numeric-domain-type").hide();
 
-            			//If this is an enumeratedDomain error
-            			if(attr == "enumeratedDomain"){
+        //Get the new value selected
+        var value = this.$(".non-numeric-domain .possible-text:checked").val();
 
-            				var view = this;
+        var activeScale = this.$(".nominal-options").is(":visible")
+          ? "nominal"
+          : "ordinal";
 
-            				//Give the user a few milliseconds to focus on a new element
-            				setTimeout(function(){
+        //Show the form elements for that non numeric type
+        this.$(
+          "." + activeScale + "-options .non-numeric-domain-type." + value,
+        ).show();
 
-            					//Highlight the inputs in code rows that are empty
-                				var emptyInputs = view.$("." + modelType + "-options .codelist.input")
-					                					.not(document.activeElement)
-					                					.filter(function(){
-					                						if( $(this).val() ) return false;
-					                						else return true;
-					                					});
-                				emptyInputs.addClass("error");
+        this.updateModel(e);
+      },
 
-                				if(emptyInputs.length)
-                					view.$("." + modelType + "-options [data-category='enumeratedDomain'] .notification").text(errors[attr]).addClass("error");
+      addNewCodeRow: function (e) {
+        if (typeof e == "object") {
+          var $row = $(e.target).parents(".code-row"),
+            code = $row.find(".code").val(),
+            definition = $row.find(".definition").val();
 
-                        	}, 200);
+          //Only add a row when there is a value for the code and code definition
+          if (!code || !definition) return false;
 
-            			}
-            			//For all other attributes, just display the errors the same way
-            			else{
-                			this.$("." + modelType + "-options [data-category='" + attr + "'] .notification").text(errors[attr]).addClass("error");
-                			this.$("." + modelType + "-options .input[data-category='" + attr + "']").addClass("error");
-            			}
+          $row.removeClass("new");
 
-            			//Highlight the border of the non numeric domain container
-            			if(attr == "nonNumericDomain"){
-            				this.$("." + modelType + "-options.non-numeric-domain").addClass("error");
-            			}
+          var newRow = this.addCodeRow();
+        } else if (typeof e == "string") {
+          var newRow = this.addCodeRow(e);
+        }
 
-            		}, this);
+        newRow.addClass("new");
+      },
 
-            		//Trigger the invalid event on the attribute model
-                //	this.model.get("parentModel").trigger("invalid", this.model.get("parentModel"));
+      addCodeRow: function (scaleType) {
+        if (!scaleType) var scaleType = this.model.get("measurementScale");
 
-            	}
-            	else{
-            		//Trigger the valid event on the attribute model
-            	//	this.model.get("parentModel").trigger("valid", this.model.get("parentModel"));
-            	}
+        var $container = this.$(
+          "." +
+            scaleType +
+            "-options .enumeratedDomain.non-numeric-domain-type .table",
+        );
 
-            },
+        //Create a code list row from the template
+        var row = $(this.codeListRowTemplate({ code: "", definition: "" }));
 
-            switchCategory: function(){
-            	//Switch the category in the view
-            	var chosenCategory = this.$("input[name='measurementScale']:checked").val();
+        $container.append(row);
 
-            	//Show the new category options
-            	this.$(".options").hide();
-            	this.$("." + chosenCategory + "-options.options").show();
+        return row;
+      },
 
-            	//Get the current category
-            	var modelCategory = this.model.get("measurementScale");
+      removeCodeRow: function (e) {
+        var codeRow = $($(e.target).parents(".code-row")),
+          allRows = codeRow.parents(".enumerated-domain").find(".code-row"),
+          index = allRows.index(codeRow);
 
-            	//Get the parent attribute model
-            	var parentEMLAttrModel = this.model.get("parentModel");
+        this.model.removeCode(index);
 
-            	//Switch the model type, if needed
-            	if(chosenCategory && (modelCategory != chosenCategory) && !(modelCategory == "interval" && chosenCategory == "ratio")){
-            		var newModel;
+        codeRow.remove();
 
-            		if(typeof this.modelCache != "object"){
-            			this.modelCache = {};
-            		}
+        this.showValidation();
 
-            		//Get the model type from this view's cache
-            		if(this.modelCache[chosenCategory])
-            			newModel = this.modelCache[chosenCategory];
-                else if( chosenCategory == "ratio" && this.modelCache["interval"] )
-                  newModel = this.modelCache["interval"];
-            		//Get a new model instance based on the type
-            		else
-            			newModel = EMLMeasurementScale.getInstance(chosenCategory);
+        this.parentView.showValidation();
+      },
 
-            		//Save this model for later in case the user switches back
-            		if(modelCategory)
-            			this.modelCache[modelCategory] = this.model;
+      /*
+       * When the user changes the value of the form, update the model
+       */
+      updateModel: function (e) {
+        var updatedInput = $(e.target);
 
-            		//save the new model
-            		this.model = newModel;
+        var emlModel = this.model.getParentEML();
 
-            		//Set references to and from this model and the parent attribute model
-            		this.model.set("parentModel", parentEMLAttrModel);
-            		parentEMLAttrModel.set("measurementScale", this.model);
+        //Update the standard unit
+        if (updatedInput.is(".units")) {
+          var chosenUnit = updatedInput.val(),
+            chosenOption = updatedInput.children(
+              "[value='" + chosenUnit + "']",
+            );
 
-            		//Update the codelist values, if needed
-            		if(chosenCategory == "nominal" || chosenCategory == "ordinal" &&
-            				this.model.get("nonNumericDomain").length &&
-            				this.model.get("nonNumericDomain")[0].enumeratedDomain){
-            			this.updateCodeList();
-            		}
-            	}
+          if (chosenOption.is(".custom")) {
+            this.model.set("unit", { customUnit: chosenUnit });
+          } else {
+            this.model.set("unit", { standardUnit: chosenUnit });
+          }
 
-            },
+          // Hard-code the numberType for now
+          this.model.set("numericDomain", { numberType: "real" });
+
+          //Trickle up the change to the most parent-level metadata model
+          this.model.trickleUpChange();
+        }
+        //Update the datetime format
+        else if (updatedInput.is(".datetime")) {
+          var format = emlModel
+            ? emlModel.cleanXMLText(updatedInput.val())
+            : updatedInput.val();
+
+          if (format == "custom") {
+            format = emlModel
+              ? emlModel.cleanXMLText(this.$(".datetime-string-custom").val())
+              : this.$(".datetime-string-custom").val();
+          }
 
-            renderUnitDropdown: function(){
-            	if(this.$("select.units").length) return;
-
-            	//Create a dropdown menu
-            	var select = $(document.createElement("select"))
-            					.addClass("units full-width input")
-            					.attr("data-category", "unit");
-
-              var eml = this.model.getParentEML();
-
-            	//Get the units collection or wait until it has been fetched
-            	if(!eml.units.length){
-            		this.listenTo(eml.units, "sync", this.renderUnitDropdown);
-            		return;
-            	}
-
-            	//Create a default option
-            	var defaultOption = $(document.createElement("option"))
-										.text("Choose a standard unit");
-				select.append(defaultOption);
-
-				//Create an "Other" option to show at the top
-				var otherOption = $(document.createElement("option"))
-									.text("Other / None")
-									.attr("value", "dimensionless");
-				select.append(otherOption);
-
-            	//Create each unit option in the unit dropdown
-            	eml.units.each(function(unit){
-            		var option = $(document.createElement("option"))
-            						.val(unit.get("_name"))
-            						.text(unit.get("_name").charAt(0).toUpperCase() +
-            								unit.get("_name").slice(1) +
-            								" (" + unit.get("description") + ")")
-            						.data({ model: unit });
-            		select.append(option);
-            	}, this);
-
-            	//Add the dropdown to the page
-            	this.$(".units-container").append(select);
-
-            	//Select the unit from the EML, if there is one
-            	var currentUnit = this.model.get("unit");
-            	if(currentUnit && currentUnit.standardUnit){
-
-            		//Get the dropdown for this measurement scale
-                // (We default interval to ratio in the editor)
-                var currentDropdown = this.$(".ratio-options select");
-
-            		//Select the unit from the EML
-            		currentDropdown.val(currentUnit.standardUnit);
-            	}
-              //If this unit is a custom unit
-              else if( currentUnit && currentUnit.customUnit ){
-                //Create an <option> for this custom unit
-                var customUnitOption = $(document.createElement("option"))
-                                        .val( currentUnit.customUnit )
-                                        .text( currentUnit.customUnit )
-                                        .addClass("custom");
-
-                //Add it to the <select> and select it as the active option
-                select.append(customUnitOption)
-                      .val(currentUnit.customUnit);
+          //If no format string was provided, then set the default value
+          if (typeof format == "string" && !format.trim().length)
+            this.model.set("formatString", this.model.defaults().formatString);
+          else this.model.set("formatString", format);
+        } else if (updatedInput.is(".possible-text")) {
+          var possibleText = emlModel
+            ? emlModel.cleanXMLText(updatedInput.val())
+            : updatedInput.val();
+
+          if (possibleText == "enumeratedDomain") {
+            //Update the code list
+            this.updateCodeList();
+          } else if (possibleText == "pattern") {
+            if (
+              !this.model.get("nonNumericDomain").length ||
+              !this.model.get("nonNumericDomain")[0].textDomain
+            ) {
+              var textDomain = {
+                definition: null,
+                pattern: [],
+                source: null,
+              };
+
+              this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
+            } else {
+              //Get the value of the text input fields for the definition and pattern
+              var definition = this.$(
+                  "." +
+                    this.model.get("measurementScale") +
+                    "-options .textDomain[data-category='definition']",
+                ).val(),
+                pattern = this.$(
+                  "." +
+                    this.model.get("measurementScale") +
+                    "-options .textDomain[data-category='pattern']",
+                ).val();
+
+              definition = emlModel
+                ? emlModel.cleanXMLText(definition)
+                : definition;
+              pattern = emlModel ? emlModel.cleanXMLText(pattern) : pattern;
+
+              // If the pattern is an empty string, then set an empty array on the model
+              if (typeof pattern == "string" && !pattern.trim().length) {
+                pattern = new Array();
+              }
+              // For all other values, put it in an array
+              else {
+                pattern = [pattern];
               }
-            },
-
-            /*
-             *  Chooses the date-time format from the dropdown menu
-             */
-            chooseDateTimeFormat: function(){
-            	if(this.model.type == "EMLDateTimeDomain"){
-                	var formatString = this.model.get("formatString");
-
-                	//Go back to the default option when the model isn't set yet
-                	if(!formatString){
-                		var options = this.$("select.datetime-string option");
-                		this.$("select.datetime-string").val(options.first().val());
-                		return;
-                	}
-
-                	var matchingOption = this.$("select.datetime-string [value='" + formatString + "']");
-
-                	if(matchingOption.length){
-                		this.$("select.datetime-string").val(formatString);
-                		this.$(".datetime-string-custom-container").hide();
-                	}
-                	else{
-                		this.$("select.datetime-string").val("custom");
-                		this.$(".datetime-string-custom").val(formatString);
-                		this.$(".datetime-string-custom-container").show();
-                	}
-
-            	}
-            },
-
-            toggleCustomDateTimeFormat: function(e){
-            	var choice = this.$("select.datetime-string").val();
-
-            	if(choice == "custom"){
-            		this.$(".datetime-string-custom-container").show();
-            	}
-            	else{
-            		this.$(".datetime-string-custom-container").hide();
-            	}
-
-            },
-
-            chooseNonNumericDomain: function(){
-
-            	if(this.model.get("nonNumericDomain") && this.model.get("nonNumericDomain").length){
-
-            		//Hide all the details first
-            		this.$(".non-numeric-domain-type").hide();
-
-            		//Get the domain from the model
-            		var domain = this.model.get("nonNumericDomain")[0];
-
-            		//If the domain type is text, select it and show it
-            		if( domain.textDomain ){
-
-            			//If the pattern is just a wildcard, then check the "anything" radio button
-            			if(domain.textDomain.pattern && domain.textDomain.pattern.length && domain.textDomain.pattern[0] == "*")
-            				this.$("." + this.model.get("measurementScale") + "-options .possible-text[value='anything']").prop("checked", true);
-            			//Otherwise, check the pattern radio button
-            			else{
-            				this.$("." + this.model.get("measurementScale") + "-options .possible-text[value='pattern']").prop("checked", true);
-            				this.$("." + this.model.get("measurementScale") + "-options .non-numeric-domain-type.pattern").show();
-            			}
-
-            		}
-            		//If the domain type is a code list, select it and show it
-            		else if( domain.enumeratedDomain ){
-            			this.$("." + this.model.get("measurementScale") + "-options .possible-text[value='enumeratedDomain']").prop("checked", true);
-            			this.$(".non-numeric-domain-type.enumeratedDomain").show();
-            		}
-            	}
-            },
-
-            toggleNonNumericDomain: function(e){
-            	//Hide the domain type details
-        		this.$(".non-numeric-domain-type").hide();
-
-        		//Get the new value selected
-            	var value = this.$(".non-numeric-domain .possible-text:checked").val();
-
-            	var activeScale = this.$(".nominal-options").is(":visible")? "nominal" : "ordinal";
-
-            	//Show the form elements for that non numeric type
-            	this.$("." + activeScale + "-options .non-numeric-domain-type." + value).show();
-
-            	this.updateModel(e);
-
-            },
-
-            addNewCodeRow: function(e){
-            	if(typeof e == "object"){
-	            	var $row 	   = $(e.target).parents(".code-row"),
-	            		code 	   = $row.find(".code").val(),
-	            		definition = $row.find(".definition").val();
-
-	            	//Only add a row when there is a value for the code and code definition
-	            	if(!code || !definition) return false;
-
-	            	$row.removeClass("new");
-
-	            	var newRow = this.addCodeRow();
-            	}
-            	else if(typeof e == "string"){
-	            	var newRow = this.addCodeRow(e);
-            	}
-
-            	newRow.addClass("new");
-            },
-
-            addCodeRow: function(scaleType){
-            	if(!scaleType)
-            		var scaleType = this.model.get("measurementScale");
-
-        		var	$container = this.$("." + scaleType + "-options .enumeratedDomain.non-numeric-domain-type .table");
-
-            	//Create a code list row from the template
-            	var row = $(this.codeListRowTemplate({ code: "", definition: ""}));
-
-            	$container.append(row);
-
-            	return row;
-            },
 
-            removeCodeRow: function(e){
-            	var codeRow = $($(e.target).parents(".code-row")),
-            		allRows = codeRow.parents(".enumerated-domain").find(".code-row"),
-            		index   = allRows.index(codeRow);
+              // If the definition is a string of space characters, then set it to an empty string
+              if (typeof definition == "string" && !definition.trim().length) {
+                definition = "";
+              }
 
-            	this.model.removeCode(index);
+              var textDomain = {
+                definition: definition,
+                pattern: pattern,
+                source: null,
+              };
+              this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
+            }
+          } else if (possibleText == "anything") {
+            var textDomain = {
+              definition: "Any text",
+              pattern: ["*"],
+              source: null,
+            };
+
+            this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
+          }
+        } else if (updatedInput.is(".textDomain")) {
+          // If there is no nonNumericDomain object set on the model, create a new empty one
+          if (typeof this.model.get("nonNumericDomain")[0] != "object") {
+            this.model.get("nonNumericDomain")[0] = {
+              textDomain: { definition: null, pattern: [], source: null },
+            };
+          }
 
-            	codeRow.remove();
+          //Get the textDomain object
+          var textDomain = this.model.get("nonNumericDomain")[0].textDomain;
 
-            	this.showValidation();
+          //If the text definition was updated...
+          if (updatedInput.attr("data-category") == "definition") {
+            //Get the value that was input by the user
+            var definition = emlModel
+              ? emlModel.cleanXMLText(updatedInput.val())
+              : updatedInput.val();
 
-            	this.parentView.showValidation();
+            // If the definition is a string of space characters, then set it to an empty string
+            if (typeof definition == "string" && !definition.trim().length) {
+              definition = "";
+            }
 
-            },
+            //Update the textDomain object
+            textDomain.definition = definition;
+          }
+          //If the text pattern was updated...
+          else if (updatedInput.attr("data-category") == "pattern") {
+            //Get the value that was input by the user
+            var pattern = emlModel
+              ? emlModel.cleanXMLText(updatedInput.val())
+              : updatedInput.val();
+
+            // If the pattern is a string of space characters, then set it to an empty string
+            if (typeof pattern == "string" && !pattern.trim().length) {
+              textDomain.pattern = [];
+            }
+            //Put the value inside a new array and update the textDomain object
+            else {
+              textDomain.pattern = [pattern];
+            }
+          }
 
-            /*
-             * When the user changes the value of the form, update the model
-             */
-            updateModel: function(e){
-
-            	var updatedInput = $(e.target);
-
-              var emlModel = this.model.getParentEML();
-
-            	//Update the standard unit
-            	if(updatedInput.is(".units")){
-            		var chosenUnit = updatedInput.val(),
-                    chosenOption = updatedInput.children("[value='" + chosenUnit + "']");
-
-                if( chosenOption.is(".custom") ){
-                  this.model.set("unit", {customUnit: chosenUnit});
-                }
-                else{
-                  this.model.set("unit", {standardUnit: chosenUnit});
-                }
-
-                // Hard-code the numberType for now
-                this.model.set("numericDomain", {numberType: "real"});
-
-                //Trickle up the change to the most parent-level metadata model
-                this.model.trickleUpChange();
-            	}
-            	//Update the datetime format
-            	else if(updatedInput.is(".datetime")){
-            		var format = emlModel? emlModel.cleanXMLText( updatedInput.val() ) : updatedInput.val();
-
-            		if(format == "custom"){
-            			format = emlModel? emlModel.cleanXMLText( this.$(".datetime-string-custom").val() ) : this.$(".datetime-string-custom").val();
-            		}
-
-                //If no format string was provided, then set the default value
-                if( typeof format == "string" && !format.trim().length )
-                  this.model.set("formatString", this.model.defaults().formatString);
-                else
-                  this.model.set("formatString", format);
-            	}
-            	else if(updatedInput.is(".possible-text")){
-            		var possibleText = emlModel? emlModel.cleanXMLText( updatedInput.val() ) : updatedInput.val();
-
-            		if(possibleText == "enumeratedDomain"){
-
-        				//Update the code list
-        				this.updateCodeList();
-
-            		}
-            		else if(possibleText == "pattern"){
-            			if(!this.model.get("nonNumericDomain").length || !this.model.get("nonNumericDomain")[0].textDomain){
-
-	            			var textDomain = {
-	            					definition: null,
-	            					pattern: [],
-	            					source: null
-	            			}
-
-	            			this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
-            			}
-            			else{
-                    //Get the value of the text input fields for the definition and pattern
-                    var definition = this.$("." + this.model.get("measurementScale") + "-options .textDomain[data-category='definition']").val(),
-                        pattern = this.$("." + this.model.get("measurementScale") + "-options .textDomain[data-category='pattern']").val();
-
-                    definition = emlModel? emlModel.cleanXMLText( definition ) : definition;
-                    pattern = emlModel? emlModel.cleanXMLText( pattern ) : pattern;
-
-                    // If the pattern is an empty string, then set an empty array on the model
-                    if( typeof pattern == "string" && !pattern.trim().length ){
-                      pattern = new Array();
-                    }
-                    // For all other values, put it in an array
-                    else {
-                      pattern = [pattern];
-                    }
-
-                    // If the definition is a string of space characters, then set it to an empty string
-                    if( typeof definition == "string" && !definition.trim().length ){
-                      definition = "";
-                    }
-
-            				var textDomain = {
-            						definition: definition,
-            						pattern: pattern,
-            						source: null
-            				}
-            				this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
-            			}
-            		}
-            		else if(possibleText == "anything"){
-            			var textDomain = {
-            					definition: "Any text",
-            					pattern: ["*"],
-            					source: null
-            			}
-
-            			this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
-            		}
-            	}
-            	else if(updatedInput.is(".textDomain")){
-
-                // If there is no nonNumericDomain object set on the model, create a new empty one
-                if(typeof this.model.get("nonNumericDomain")[0] != "object"){
-            			this.model.get("nonNumericDomain")[0] = { textDomain: { definition: null, pattern: [], source: null } };
-                }
-
-                //Get the textDomain object
-            		var textDomain = this.model.get("nonNumericDomain")[0].textDomain;
-
-                //If the text definition was updated...
-            		if(updatedInput.attr("data-category") == "definition"){
-
-                  //Get the value that was input by the user
-                  var definition = emlModel? emlModel.cleanXMLText( updatedInput.val() ) : updatedInput.val();
-
-                  // If the definition is a string of space characters, then set it to an empty string
-                  if( typeof definition == "string" && !definition.trim().length ){
-                    definition = "";
-                  }
-
-                  //Update the textDomain object
-                	textDomain.definition = definition;
-                }
-                //If the text pattern was updated...
-            		else if(updatedInput.attr("data-category") == "pattern"){
-                  //Get the value that was input by the user
-                  var pattern = emlModel? emlModel.cleanXMLText( updatedInput.val() ) : updatedInput.val();
-
-                  // If the pattern is a string of space characters, then set it to an empty string
-                  if( typeof pattern == "string" && !pattern.trim().length ){
-                    textDomain.pattern = [];
-                  }
-                  //Put the value inside a new array and update the textDomain object
-                  else{
-                    textDomain.pattern = [pattern];
-                  }
-                }
-
-                //Manually trigger a change on the nonNumericDomain attribute
-                this.model.trigger("change:nonNumericDomain");
-
-            	}
-            	else if(updatedInput.is(".codelist")){
-            		var row = updatedInput.parents(".code-row"),
-            			index = this.$("." + this.model.get("measurementScale") + "-options .code-row").index(row);
-
-            		this.updateCodeList(index);
-            	}
-
-            	//Add this EMLMeasurementScale model to the EMLAttribute model when it is updated in the view
-            	var attributeModel = this.model.get("parentModel");
-
-            	if( attributeModel )
-            		attributeModel.set("measurementScale", this.model);
+          //Manually trigger a change on the nonNumericDomain attribute
+          this.model.trigger("change:nonNumericDomain");
+        } else if (updatedInput.is(".codelist")) {
+          var row = updatedInput.parents(".code-row"),
+            index = this.$(
+              "." + this.model.get("measurementScale") + "-options .code-row",
+            ).index(row);
+
+          this.updateCodeList(index);
+        }
+
+        //Add this EMLMeasurementScale model to the EMLAttribute model when it is updated in the view
+        var attributeModel = this.model.get("parentModel");
+
+        if (attributeModel) attributeModel.set("measurementScale", this.model);
+      },
+
+      updateCodeList: function (rowNum) {
+        //If the model is not set as an enumerated domain yet
+        if (
+          !this.model.get("nonNumericDomain").length ||
+          !this.model.get("nonNumericDomain")[0] ||
+          !this.model.get("nonNumericDomain")[0].enumeratedDomain
+        ) {
+          var isEmpty = false;
+
+          var emlModel = this.model.getParentEML();
+
+          //Go through each code row in this view and grab the values
+          _.each(
+            this.$(
+              "." + this.model.get("measurementScale") + "-options .code-row",
+            ),
+            function (row, i, rows) {
+              var $row = $(row),
+                code = $row.find(".code").val(),
+                def = $row.find(".definition").val();
+
+              code = emlModel ? emlModel.cleanXMLText(code) : code;
+              def = emlModel ? emlModel.cleanXMLText(def) : def;
+
+              //Update the enumerated domain with this code
+              if (code || def) {
+                this.model.updateEnumeratedDomain(code, def, i);
+              }
+              //If there is only one row and it has no code or definition,
+              //then this is an empty code list
+              else if (rows.length == 1 && i == 0) {
+                isEmpty = true;
+              }
             },
+            this,
+          );
 
-            updateCodeList: function(rowNum){
-
-            	//If the model is not set as an enumerated domain yet
-        			if(!this.model.get("nonNumericDomain").length ||
-        					!this.model.get("nonNumericDomain")[0] ||
-        					!this.model.get("nonNumericDomain")[0].enumeratedDomain){
-
-      				var isEmpty = false;
-
-              var emlModel = this.model.getParentEML();
-
-      				//Go through each code row in this view and grab the values
-      				_.each(this.$("." + this.model.get("measurementScale") + "-options .code-row"), function(row, i, rows){
-      					var $row = $(row),
-      						code = $row.find(".code").val(),
-      						def  = $row.find(".definition").val();
-
-                code = emlModel? emlModel.cleanXMLText( code ) : code;
-                def  = emlModel? emlModel.cleanXMLText( def ) : def;
-
-      					//Update the enumerated domain with this code
-      					if(code || def){
-          					this.model.updateEnumeratedDomain(code, def, i);
-      					}
-      					//If there is only one row and it has no code or definition,
-      					//then this is an empty code list
-      					else if( rows.length == 1 && i == 0){
-      						isEmpty = true;
-      					}
-
-      				}, this);
-
-      				//If there are no codes in the list, update the enumerated domain with blank values
-      				if( isEmpty ){
-      					this.model.updateEnumeratedDomain(null, null, rowNum);
-      				}
-      			}
-      			else if(rowNum > -1){
-      				var $row = $(this.$("." + this.model.get("measurementScale") + "-options .code-row")[rowNum]),
-      						code = $row.find(".code").val(),
-      						def  = $row.find(".definition").val();
-
-              code = emlModel? emlModel.cleanXMLText( code ) : code;
-              def  = emlModel? emlModel.cleanXMLText( def ) : def;
-
-    					if(code || def){
-    						this.model.updateEnumeratedDomain(code, def, rowNum);
-    					}
-      			}
-
-
-          },
-
-          previewCodeRemove: function(e){
-          	$(e.target).parents(".code-row").toggleClass("remove-preview");
+          //If there are no codes in the list, update the enumerated domain with blank values
+          if (isEmpty) {
+            this.model.updateEnumeratedDomain(null, null, rowNum);
+          }
+        } else if (rowNum > -1) {
+          var $row = $(
+              this.$(
+                "." + this.model.get("measurementScale") + "-options .code-row",
+              )[rowNum],
+            ),
+            code = $row.find(".code").val(),
+            def = $row.find(".definition").val();
+
+          code = emlModel ? emlModel.cleanXMLText(code) : code;
+          def = emlModel ? emlModel.cleanXMLText(def) : def;
+
+          if (code || def) {
+            this.model.updateEnumeratedDomain(code, def, rowNum);
           }
+        }
+      },
 
-        });
+      previewCodeRemove: function (e) {
+        $(e.target).parents(".code-row").toggleClass("remove-preview");
+      },
+    },
+  );
 
-        return EMLMeasurementScaleView;
+  return EMLMeasurementScaleView;
 });
 
diff --git a/docs/docs/src_js_views_metadata_EMLMeasurementTypeView.js.html b/docs/docs/src_js_views_metadata_EMLMeasurementTypeView.js.html index 5451c04b6..4e09af7a2 100644 --- a/docs/docs/src_js_views_metadata_EMLMeasurementTypeView.js.html +++ b/docs/docs/src_js_views_metadata_EMLMeasurementTypeView.js.html @@ -44,13 +44,25 @@

Source: src/js/views/metadata/EMLMeasurementTypeView.js
-
/* global define */
-define(['underscore', 'jquery', 'backbone', 'bioportal',
-  'models/metadata/eml211/EMLAnnotation',
-  'views/AnnotationView',
-  'text!templates/metadata/eml-measurement-type.html',
-  'text!templates/metadata/eml-measurement-type-annotations.html'],
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "bioportal",
+  "models/metadata/eml211/EMLAnnotation",
+  "views/AnnotationView",
+  "text!templates/metadata/eml-measurement-type.html",
+  "text!templates/metadata/eml-measurement-type-annotations.html",
+], function (
+  _,
+  $,
+  Backbone,
+  BioPortal,
+  EMLAnnotation,
+  AnnotationView,
+  EMLMeasurementTypeTemplate,
+  EMLMeasurementTypeAnnotationsTemplate,
+) {
   /**
    * @class EMLMeasurementTypeView
    * @classdec The EMLMeasurementTypeView is a view to render a specialized
@@ -59,10 +71,8 @@ 

Source: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.jsSource: src/js/views/metadata/EMLMeasurementTypeView.js

diff --git a/docs/docs/src_js_views_metadata_EMLMethodsView.js.html b/docs/docs/src_js_views_metadata_EMLMethodsView.js.html index cbf45eef2..bc840a005 100644 --- a/docs/docs/src_js_views_metadata_EMLMethodsView.js.html +++ b/docs/docs/src_js_views_metadata_EMLMethodsView.js.html @@ -44,242 +44,269 @@

Source: src/js/views/metadata/EMLMethodsView.js

-
/* global define */
-define(['underscore', 'jquery', 'backbone', 'models/metadata/eml211/EMLMethods', 'models/metadata/eml/EMLMethodStep',
-        'models/metadata/eml211/EMLText', 'models/metadata/eml/EMLSpecializedText',
-        'text!templates/metadata/EMLMethods.html'],
-    function(_, $, Backbone, EMLMethods, EMLMethodStep, EMLText, EMLSpecializedText, EMLMethodsTemplate){
-
-        /**
-        * @class EMLMethodsView
-        * @classdesc The EMLMethods renders the content of an EMLMethods model
-        * @classcategory Views/Metadata
-        * @extends Backbone.View
-        */
-        var EMLMethodsView = Backbone.View.extend(
-          /** @lends EMLMethodsView.prototype */{
-
-          type: "EMLMethodsView",
-
-          tagName: "div",
-
-          className: "row-fluid eml-methods",
-
-          stepsContainerSelector: "#eml-method-steps-container",
-
-          editTemplate: _.template(EMLMethodsTemplate),
-
-          /**
-          * A small template to display each EMLMethodStep.
-          * If you are going to extend this template for a theme, note that:
-          * This template must keep the ".step-container" wrapper class.
-          * This template must keep the textarea with the default data attributes.
-          * The remove button must have a "remove" class
-          * @type {UnderscoreTemplate}
-          */
-          stepTemplate: _.template('<div class="step-container">\
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/metadata/eml211/EMLMethods",
+  "models/metadata/eml/EMLMethodStep",
+  "models/metadata/eml211/EMLText",
+  "models/metadata/eml/EMLSpecializedText",
+  "text!templates/metadata/EMLMethods.html",
+], function (
+  _,
+  $,
+  Backbone,
+  EMLMethods,
+  EMLMethodStep,
+  EMLText,
+  EMLSpecializedText,
+  EMLMethodsTemplate,
+) {
+  /**
+   * @class EMLMethodsView
+   * @classdesc The EMLMethods renders the content of an EMLMethods model
+   * @classcategory Views/Metadata
+   * @extends Backbone.View
+   */
+  var EMLMethodsView = Backbone.View.extend(
+    /** @lends EMLMethodsView.prototype */ {
+      type: "EMLMethodsView",
+
+      tagName: "div",
+
+      className: "row-fluid eml-methods",
+
+      stepsContainerSelector: "#eml-method-steps-container",
+
+      editTemplate: _.template(EMLMethodsTemplate),
+
+      /**
+       * A small template to display each EMLMethodStep.
+       * If you are going to extend this template for a theme, note that:
+       * This template must keep the ".step-container" wrapper class.
+       * This template must keep the textarea with the default data attributes.
+       * The remove button must have a "remove" class
+       * @type {UnderscoreTemplate}
+       */
+      stepTemplate: _.template(
+        '<div class="step-container">\
                 <h5>Step <span class="step-num"><%=num%></span></h5>\
                 <p class="notification" data-attribute="methodStepDescription"></p>\
                 <textarea data-attribute="methodStepDescription"\
                       data-step-attribute="description"\
                       rows="7" class="method-step"><%=text%></textarea>\
                 <i class="remove icon-remove"></i>\
-              </div>'),
-
-          /**
-          * A reference to the EML211View that contains this EMLMethodsView.
-          * @type {EML211View}
-          */
-          parentEMLView: null,
-
-          /**
-          * jQuery selector for the element that contains the Custom Methods
-          * @type {string}
-          */
-          customMethodsSelector: ".custom-methods-container",
-
-          initialize: function(options){
-            options = options || {};
-
-            this.isNew = options.isNew || (options.model? false : true);
-            this.model = options.model || new EMLMethods();
-            this.edit  = options.edit  || false;
-            this.parentEMLView = options.parentEMLView || null;
-
-            this.$el.data({ model: this.model });
-          },
-
-          events: {
-            "change" : "updateModel",
-            "keyup .method-step.new" : "renderNewMethodStep",
-            "click .remove" : "removeMethodStep",
-            "mouseover .remove" : "previewRemove",
-            "mouseout .remove"  : "previewRemove"
-          },
-
-          render: function() {
-            //Save the view and model on the element
-            this.$el.data({
-                model: this.model,
-                view: this
-              })
-              .attr("data-category", "methods");
-
-            if (this.edit) {
-
-              this.$el.html(this.editTemplate({
-                studyExtentDescription: this.model.get('studyExtentDescription'),
-                samplingDescription: this.model.get('samplingDescription')
-              }));
-
-              //Render each EMLMethodStep
-              let regularMethodSteps = this.model.getNonCustomSteps();
-              regularMethodSteps.forEach(step => {
-                this.renderMethodStep(step)
-              });
+              </div>',
+      ),
+
+      /**
+       * A reference to the EML211View that contains this EMLMethodsView.
+       * @type {EML211View}
+       */
+      parentEMLView: null,
 
-              //Create a blank step for the user to make a new one
-              this.renderMethodStep();
+      /**
+       * jQuery selector for the element that contains the Custom Methods
+       * @type {string}
+       */
+      customMethodsSelector: ".custom-methods-container",
 
-              //Populate all the step numbers
-              this.updateMethodStepNums();
+      initialize: function (options) {
+        options = options || {};
 
-              //Render the custom methods differently
-              this.renderCustomMethods();
-            }
+        this.isNew = options.isNew || (options.model ? false : true);
+        this.model = options.model || new EMLMethods();
+        this.edit = options.edit || false;
+        this.parentEMLView = options.parentEMLView || null;
 
-            return this;
-          },
+        this.$el.data({ model: this.model });
+      },
 
-      /**
-      * Renders a single EMLMethodStep model
-      * @param {EMLMethodStep} [step]
-      * @since 2.19.0
-      */
-      renderMethodStep: function(step){
-        try{
+      events: {
+        change: "updateModel",
+        "keyup .method-step.new": "renderNewMethodStep",
+        "click .remove": "removeMethodStep",
+        "mouseover .remove": "previewRemove",
+        "mouseout .remove": "previewRemove",
+      },
+
+      render: function () {
+        //Save the view and model on the element
+        this.$el
+          .data({
+            model: this.model,
+            view: this,
+          })
+          .attr("data-category", "methods");
+
+        if (this.edit) {
+          this.$el.html(
+            this.editTemplate({
+              studyExtentDescription: this.model.get("studyExtentDescription"),
+              samplingDescription: this.model.get("samplingDescription"),
+            }),
+          );
+
+          //Render each EMLMethodStep
+          let regularMethodSteps = this.model.getNonCustomSteps();
+          regularMethodSteps.forEach((step) => {
+            this.renderMethodStep(step);
+          });
+
+          //Create a blank step for the user to make a new one
+          this.renderMethodStep();
+
+          //Populate all the step numbers
+          this.updateMethodStepNums();
+
+          //Render the custom methods differently
+          this.renderCustomMethods();
+        }
+
+        return this;
+      },
 
+      /**
+       * Renders a single EMLMethodStep model
+       * @param {EMLMethodStep} [step]
+       * @since 2.19.0
+       */
+      renderMethodStep: function (step) {
+        try {
           let stepEl;
 
-          if(step){
+          if (step) {
             //Render the step HTML
-            stepEl = $(this.stepTemplate({
-              text: step.get("description").toString(),
-              num: ""
-            }));
+            stepEl = $(
+              this.stepTemplate({
+                text: step.get("description").toString(),
+                num: "",
+              }),
+            );
             //Attach the model to the elements that will be interacted with
-            stepEl.find("textarea[data-attribute='methodStepDescription'], .remove").data({ methodStepModel: step });
-          }
-          else{
-
+            stepEl
+              .find("textarea[data-attribute='methodStepDescription'], .remove")
+              .data({ methodStepModel: step });
+          } else {
             //Only one new method step should be displayed at the same time
-            if( this.$(".method-step.new").length ){
+            if (this.$(".method-step.new").length) {
               return;
             }
 
             //Render the step HTML
-            stepEl = $(this.stepTemplate({
-              text: "",
-              num: ""
-            }));
-
-            stepEl.find("textarea[data-attribute='methodStepDescription']").addClass("new");
+            stepEl = $(
+              this.stepTemplate({
+                text: "",
+                num: "",
+              }),
+            );
+
+            stepEl
+              .find("textarea[data-attribute='methodStepDescription']")
+              .addClass("new");
           }
 
           //Add the step to the page
           this.$(this.stepsContainerSelector).append(stepEl);
-
-        }
-        catch(e){
+        } catch (e) {
           console.error("Failed to render a method step: ", e);
         }
       },
 
       /**
-      * Renders the inputs for the custom EML Methods that are configured in the {@link AppConfig}
-      * If none are configured, nothing will be shown.
-      * @since 2.19.0
-      */
-      renderCustomMethods: function(){
-
+       * Renders the inputs for the custom EML Methods that are configured in the {@link AppConfig}
+       * If none are configured, nothing will be shown.
+       * @since 2.19.0
+       */
+      renderCustomMethods: function () {
         //Get the custom EML Methods that are configured in the AppConfig
         let customMethodsOptions = MetacatUI.appModel.get("customEMLMethods");
 
         //If there is at least one custom Method configured, proceed with rendering it
-        if( Array.isArray(customMethodsOptions) && customMethodsOptions.length ){
-
+        if (
+          Array.isArray(customMethodsOptions) &&
+          customMethodsOptions.length
+        ) {
           let view = this;
 
           //Get the custom Methods template
-          require(['text!templates/metadata/eml-custom-methods.html'], function(CustomMethodsTemplate){
-
-            try{
-
+          require([
+            "text!templates/metadata/eml-custom-methods.html",
+          ], function (CustomMethodsTemplate) {
+            try {
               //Get the Methods from the EMLMethods model
               let allMethodSteps = view.model.get("methodSteps"),
-              //Find the custom methods set on the model
-                  allCustomMethods = allMethodSteps.filter(step => { return step.isCustom() }),
-              //Start a literal object to send to the custom methods template
-                  templateInfo = {};
+                //Find the custom methods set on the model
+                allCustomMethods = allMethodSteps.filter((step) => {
+                  return step.isCustom();
+                }),
+                //Start a literal object to send to the custom methods template
+                templateInfo = {};
 
               //Add each custom method model to the template info
-              allCustomMethods.forEach(step => {
-                templateInfo[step.get("customMethodID")] = step
+              allCustomMethods.forEach((step) => {
+                templateInfo[step.get("customMethodID")] = step;
               });
 
               //Insert the custom methods template into the page
               let customMethodsTemplate = _.template(CustomMethodsTemplate);
-              view.$(view.customMethodsSelector).html(customMethodsTemplate(templateInfo));
+              view
+                .$(view.customMethodsSelector)
+                .html(customMethodsTemplate(templateInfo));
 
               //Attach each custom method model to it's textarea or input
-              allCustomMethods.forEach(step => {
-                view.$(view.customMethodsSelector).find("[data-custom-method-id='" + step.get("customMethodID") + "']").data({ methodStepModel: step })
+              allCustomMethods.forEach((step) => {
+                view
+                  .$(view.customMethodsSelector)
+                  .find(
+                    "[data-custom-method-id='" +
+                      step.get("customMethodID") +
+                      "']",
+                  )
+                  .data({ methodStepModel: step });
               });
 
               //If this is inside a parent EML View (most likely), trigger the event
               //that lets the parent view know that new editor components have been added to the page.
-              if( view.parentEMLView ){
+              if (view.parentEMLView) {
                 view.parentEMLView.trigger("editorInputsAdded");
               }
-            }
-            catch(e){
+            } catch (e) {
               console.error("Couldn't show the custom EML Methods: ", e);
               return;
             }
-
           });
-
         }
       },
 
-      updateModel: function(e){
-        if(!e) return false;
+      updateModel: function (e) {
+        if (!e) return false;
 
         var updatedInput = $(e.target);
 
         //Get the attribute that was changed
         var changedAttr = updatedInput.attr("data-attribute");
-        if(!changedAttr) return false;
+        if (!changedAttr) return false;
 
         // Method Step Descriptions are ordered arrays, so update them with special rules
         if (changedAttr == "methodStepDescription") {
-
           // Get the EMLMethodStep model
           var methodStep = updatedInput.data("methodStepModel");
 
           //If there is already an EMLMethodStep model created, then update it
-          if( methodStep ){
+          if (methodStep) {
             let desc = methodStep.get("description");
             desc.setText(updatedInput.val());
-          }
-          else{
+          } else {
             //Create a new EMLMethodStep model
             var newMethodStep = this.model.addMethodStep();
 
             //Attach the model to the elements that will be interacted with
-            updatedInput.parents(".step-container")
-                        .find("textarea[data-attribute='methodStepDescription'], .remove")
-                        .data({ methodStepModel: newMethodStep });
+            updatedInput
+              .parents(".step-container")
+              .find("textarea[data-attribute='methodStepDescription'], .remove")
+              .data({ methodStepModel: newMethodStep });
 
             //Update the model with the textarea value
             newMethodStep.get("description").setText(updatedInput.val());
@@ -287,23 +314,25 @@ 

Source: src/js/views/metadata/EMLMethodsView.js

// Trigger the change event manually because, without this, the change event // never fires. - this.model.trigger('change:methodSteps'); + this.model.trigger("change:methodSteps"); } //All other attributes on this model are updated differently else { - //Get the EMLText model to update var textModelToUpdate = this.model.get(changedAttr); //Double-check that this is an EMLText model, then update it - if( textModelToUpdate && typeof textModelToUpdate == "object" && textModelToUpdate.type == "EMLText"){ + if ( + textModelToUpdate && + typeof textModelToUpdate == "object" && + textModelToUpdate.type == "EMLText" + ) { textModelToUpdate.setText(updatedInput.val()); } //If there's no value set on this attribute yet, create a new EMLText model - else if(!textModelToUpdate){ - + else if (!textModelToUpdate) { let textType; - switch(changedAttr){ + switch (changedAttr) { case "studyExtentDescription": textType = "description"; break; @@ -312,12 +341,12 @@

Source: src/js/views/metadata/EMLMethodsView.js

break; } - if(!textType) return; + if (!textType) return; //Create a new EMLText model var newTextModel = new EMLText({ type: textType, - parentModel: this.model + parentModel: this.model, }); //Update the model with the textarea value @@ -325,9 +354,7 @@

Source: src/js/views/metadata/EMLMethodsView.js

//Set the EMLText model on the EMLMethods model this.model.set(changedAttr, newTextModel); - } - } //Show the remove button @@ -337,7 +364,7 @@

Source: src/js/views/metadata/EMLMethodsView.js

/** * Renders a new empty method step input. Does not update the model at all. */ - renderNewMethodStep: function(){ + renderNewMethodStep: function () { // Add new textareas as needed this.$(".method-step.new").removeClass("new"); @@ -350,14 +377,13 @@

Source: src/js/views/metadata/EMLMethodsView.js

* Remove this method step * @param {Event} e */ - removeMethodStep: function(e){ - - try{ + removeMethodStep: function (e) { + try { //Get the EMLMethodStep var step = $(e.target).data("methodStepModel"); //Exit if there is no EMLMethodStep - if( !step ){ + if (!step) { return; } @@ -366,74 +392,76 @@

Source: src/js/views/metadata/EMLMethodsView.js

//Remove the step elements from the page let view = this; - $(e.target).parent(".step-container").slideUp("fast", function(){ - this.remove(); + $(e.target) + .parent(".step-container") + .slideUp("fast", function () { + this.remove(); //Bump down all the step numbers view.updateMethodStepNums(); - }); - } - catch(e){ + }); + } catch (e) { console.error("Failed to remove the EML Method Step: ", e); } - }, /** - * Updates the step number in the view for each step - * @since 2.19.0 - */ - updateMethodStepNums: function(){ + * Updates the step number in the view for each step + * @since 2.19.0 + */ + updateMethodStepNums: function () { //Update all the step numbers this.$(".step-num").each((i, numEl) => { - numEl.textContent = i+1; - }) + numEl.textContent = i + 1; + }); }, /** - * Shows validation errors that need to be fixed by the user - */ - showValidation: function(){ - - try{ - - if( Object.keys(this.model.validationError).length ){ - if( this.model.validationError.methodSteps ){ - + * Shows validation errors that need to be fixed by the user + */ + showValidation: function () { + try { + if (Object.keys(this.model.validationError).length) { + if (this.model.validationError.methodSteps) { //A general error about all method steps will just be a string. //Apply the error styling to all the elements for the method steps - if( typeof this.model.validationError.methodSteps == "string" ){ + if (typeof this.model.validationError.methodSteps == "string") { this.$('.notification[data-attribute="methodStepDescription"]') - .text(this.model.validationError.methodSteps) - .addClass("error"); - this.$('[data-attribute="methodStepDescription"]:not([data-custom-method-id])').addClass("error"); + .text(this.model.validationError.methodSteps) + .addClass("error"); + this.$( + '[data-attribute="methodStepDescription"]:not([data-custom-method-id])', + ).addClass("error"); } //Validation errors that aren't strings are errors about specific // Custom EML Method Steps. - else{ - _.mapObject(this.model.validationError.methodSteps, (errors, customMethodID) => { - this.$(`.notification[data-category="${customMethodID}"]`) + else { + _.mapObject( + this.model.validationError.methodSteps, + (errors, customMethodID) => { + this.$(`.notification[data-category="${customMethodID}"]`) .text(errors.description) .addClass("error"); - this.$(`[data-custom-method-id="${customMethodID}"]`).addClass("error"); - }); + this.$( + `[data-custom-method-id="${customMethodID}"]`, + ).addClass("error"); + }, + ); } } } - - } - catch(e){ + } catch (e) { console.warn("Failed to show Methods validation: ", e); } - }, - previewRemove: function(e){ + previewRemove: function (e) { $(e.target).parents(".step-container").toggleClass("remove-preview"); - } - }); + }, + }, + ); - return EMLMethodsView; + return EMLMethodsView; });
diff --git a/docs/docs/src_js_views_metadata_EMLOtherEntityView.js.html b/docs/docs/src_js_views_metadata_EMLOtherEntityView.js.html index 7428ca02d..b3316cf16 100644 --- a/docs/docs/src_js_views_metadata_EMLOtherEntityView.js.html +++ b/docs/docs/src_js_views_metadata_EMLOtherEntityView.js.html @@ -44,61 +44,63 @@

Source: src/js/views/metadata/EMLOtherEntityView.js

-
/* global define */
-define(['underscore', 'jquery', 'backbone',
-        'models/DataONEObject', 'models/metadata/eml211/EMLOtherEntity',
-        'views/metadata/EMLEntityView',
-        'text!templates/metadata/eml-other-entity.html'],
-    function(_, $, Backbone, DataONEObject, EMLOtherEntity,
-    		EMLEntityView,
-    		EMLOtherEntityTemplate){
-
-        /**
-         * @class EMLOtherEntityView
-         * @classdesc An EMLOtherEntityView expands on the EMLEntityView to show attributes of the EML specific to the otherEntity
-         * @classcategory Views/Metadata
-         * @extends EMLEntityView
-        */
-        var EMLOtherEntityView = EMLEntityView.extend(
-          /** @lends EMLOtherEntityView.prototype */{
-
-            tagName: "div",
-
-            className: "",
-
-            id: null,
-
-            template: EMLOtherEntityTemplate(),
-
-            /* Events this view listens to */
-            events: {
-
-            },
-
-            initialize: function(options){
-            	if(!options)
-            		var options = {};
-
-            	this.model = options.model || new EMLOtherEntity();
-            	this.DataONEObject = options.DataONEObject;
-            },
-
-            render: function(){
-
-            	this.renderEntityTemplate();
-
-            	var overviewContainer = this.$(".overview-container");
-            	overviewContainer.append( this.template( this.model.toJSON() ) );
-
-            	this.renderPreview();
-
-            	this.renderAttributes();
-
-            }
-
-        });
-
-        return EMLOtherEntityView;
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/DataONEObject",
+  "models/metadata/eml211/EMLOtherEntity",
+  "views/metadata/EMLEntityView",
+  "text!templates/metadata/eml-other-entity.html",
+], function (
+  _,
+  $,
+  Backbone,
+  DataONEObject,
+  EMLOtherEntity,
+  EMLEntityView,
+  EMLOtherEntityTemplate,
+) {
+  /**
+   * @class EMLOtherEntityView
+   * @classdesc An EMLOtherEntityView expands on the EMLEntityView to show attributes of the EML specific to the otherEntity
+   * @classcategory Views/Metadata
+   * @extends EMLEntityView
+   */
+  var EMLOtherEntityView = EMLEntityView.extend(
+    /** @lends EMLOtherEntityView.prototype */ {
+      tagName: "div",
+
+      className: "",
+
+      id: null,
+
+      template: EMLOtherEntityTemplate(),
+
+      /* Events this view listens to */
+      events: {},
+
+      initialize: function (options) {
+        if (!options) var options = {};
+
+        this.model = options.model || new EMLOtherEntity();
+        this.DataONEObject = options.DataONEObject;
+      },
+
+      render: function () {
+        this.renderEntityTemplate();
+
+        var overviewContainer = this.$(".overview-container");
+        overviewContainer.append(this.template(this.model.toJSON()));
+
+        this.renderPreview();
+
+        this.renderAttributes();
+      },
+    },
+  );
+
+  return EMLOtherEntityView;
 });
 
diff --git a/docs/docs/src_js_views_metadata_EMLPartyView.js.html b/docs/docs/src_js_views_metadata_EMLPartyView.js.html index 633de2984..6f5c1d29a 100644 --- a/docs/docs/src_js_views_metadata_EMLPartyView.js.html +++ b/docs/docs/src_js_views_metadata_EMLPartyView.js.html @@ -44,453 +44,492 @@

Source: src/js/views/metadata/EMLPartyView.js

-
/* global define */
-define(['underscore', 'jquery', 'backbone', 'models/metadata/eml211/EMLParty',
-        'text!templates/metadata/EMLParty.html'],
-    function(_, $, Backbone, EMLParty, EMLPartyTemplate){
-
-        /**
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/metadata/eml211/EMLParty",
+  "text!templates/metadata/EMLParty.html",
+], function (_, $, Backbone, EMLParty, EMLPartyTemplate) {
+  /**
           @class EMLPartyView
           @classdesc  The EMLParty renders the content of an EMLParty model
           @classcategory Views/Metadata
           @extends Backbone.View
         */
-        var EMLPartyView = Backbone.View.extend(
-          /** @lends EMLPartyView.prototype */{
-
-          type: "EMLPartyView",
-
-          tagName: "div",
-
-          className: "row-fluid eml-party",
-
-          editTemplate: _.template(EMLPartyTemplate),
-
-          initialize: function(options){
-            if(!options)
-              var options = {};
-
-            this.isNew = options.isNew || (options.model? false : true);
-            this.model = options.model || new EMLParty();
-            this.edit  = options.edit  || false;
-
-            this.$el.data({ model: this.model });
-
-          },
-
-          events: {
-            "change"             : "updateModel",
-            "focusout"          : "showValidation",
-            "keyup .phone"      : "formatPhone",
-            "mouseover .remove" : "previewRemove",
-            "mouseout .remove"  : "previewRemove"
-          },
-
-          render: function(){
-
-            //Format the given names
-            var name = this.model.get("individualName") || {},
-              fullGivenName = "";
-
-            //Take multiple given names and combine into one given name.
-            //TODO: Support multiple given names as an array
-            if (Array.isArray(name.givenName)) {
-          fullGivenName = _.map(name.givenName, function(name) {
-              if(typeof name != "undefined" && name)
-                return name.trim();
-              else
-                return "";
-            }).join(' ');
+  var EMLPartyView = Backbone.View.extend(
+    /** @lends EMLPartyView.prototype */ {
+      type: "EMLPartyView",
+
+      tagName: "div",
+
+      className: "row-fluid eml-party",
+
+      editTemplate: _.template(EMLPartyTemplate),
+
+      initialize: function (options) {
+        if (!options) var options = {};
+
+        this.isNew = options.isNew || (options.model ? false : true);
+        this.model = options.model || new EMLParty();
+        this.edit = options.edit || false;
+
+        this.$el.data({ model: this.model });
+      },
+
+      events: {
+        change: "updateModel",
+        focusout: "showValidation",
+        "keyup .phone": "formatPhone",
+        "mouseover .remove": "previewRemove",
+        "mouseout .remove": "previewRemove",
+      },
+
+      render: function () {
+        //Format the given names
+        var name = this.model.get("individualName") || {},
+          fullGivenName = "";
+
+        //Take multiple given names and combine into one given name.
+        //TODO: Support multiple given names as an array
+        if (Array.isArray(name.givenName)) {
+          fullGivenName = _.map(name.givenName, function (name) {
+            if (typeof name != "undefined" && name) return name.trim();
+            else return "";
+          }).join(" ");
+        } else fullGivenName = name.givenName;
+
+        //Get the address object
+        var address = Array.isArray(this.model.get("address"))
+          ? this.model.get("address")[0] || {}
+          : this.model.get("address") || {};
+
+        //Use the template with the editing elements if this view has the "edit" flag on
+        if (this.edit) {
+          //Send all the EMLParty info to the template
+          this.$el.html(
+            this.editTemplate({
+              uniqueId: this.model.cid,
+            }),
+          );
+
+          //Populate the form with all the EMLParty values
+          this.$("#" + this.model.cid + "-givenName").val(fullGivenName || "");
+          this.$("#" + this.model.cid + "-surName").val(name.surName || "");
+          this.$("#" + this.model.cid + "-position").val(
+            this.model.get("positionName") || "",
+          );
+          this.$("#" + this.model.cid + "-organizationName").val(
+            this.model.get("organizationName") || "",
+          );
+          this.$("#" + this.model.cid + "-email").val(
+            this.model.get("email").length ? this.model.get("email")[0] : "",
+          );
+          this.$("#" + this.model.cid + "-website").val(
+            this.model.get("onlineUrl").length
+              ? this.model.get("onlineUrl")[0]
+              : "",
+          );
+          this.$("#" + this.model.cid + "-phone").val(
+            this.model.get("phone").length ? this.model.get("phone")[0] : "",
+          );
+          this.$("#" + this.model.cid + "-fax").val(
+            this.model.get("fax").length ? this.model.get("fax")[0] : "",
+          );
+          this.$("#" + this.model.cid + "-orcid").val(
+            Array.isArray(this.model.get("userId"))
+              ? this.model.get("userId")[0]
+              : this.model.get("userId") || "",
+          );
+          this.$("#" + this.model.cid + "-address").val(
+            address.deliveryPoint && address.deliveryPoint.length
+              ? address.deliveryPoint[0]
+              : "",
+          );
+          this.$("#" + this.model.cid + "-address2").val(
+            address.deliveryPoint && address.deliveryPoint.length > 1
+              ? address.deliveryPoint[1]
+              : "",
+          );
+          this.$("#" + this.model.cid + "-city").val(address.city || "");
+          this.$("#" + this.model.cid + "-state").val(
+            address.administrativeArea || "",
+          );
+          this.$("#" + this.model.cid + "-zip").val(address.postalCode || "");
+          this.$("#" + this.model.cid + "-country").val(address.country || "");
         }
-            else
-              fullGivenName = name.givenName;
-
-            //Get the address object
-            var address = Array.isArray(this.model.get("address"))?
-                    (this.model.get("address")[0] || {}) : (this.model.get("address") || {});
-
-            //Use the template with the editing elements if this view has the "edit" flag on
-            if(this.edit){
-
-              //Send all the EMLParty info to the template
-              this.$el.html(this.editTemplate({
-                uniqueId   : this.model.cid
-              }));
-
-              //Populate the form with all the EMLParty values
-              this.$("#" + this.model.cid + "-givenName").val(fullGivenName || "");
-              this.$("#" + this.model.cid + "-surName").val(name.surName || "");
-              this.$("#" + this.model.cid + "-position").val(this.model.get("positionName") || "");
-              this.$("#" + this.model.cid + "-organizationName").val(this.model.get("organizationName") || "");
-              this.$("#" + this.model.cid + "-email").val(this.model.get("email").length? this.model.get("email")[0] : "");
-              this.$("#" + this.model.cid + "-website").val(this.model.get("onlineUrl").length? this.model.get("onlineUrl")[0] : "");
-              this.$("#" + this.model.cid + "-phone").val(this.model.get("phone").length? this.model.get("phone")[0] : "");
-              this.$("#" + this.model.cid + "-fax").val(this.model.get("fax").length? this.model.get("fax")[0] : "");
-              this.$("#" + this.model.cid + "-orcid").val(Array.isArray(this.model.get("userId"))? this.model.get("userId")[0] : this.model.get("userId") || "");
-              this.$("#" + this.model.cid + "-address").val(address.deliveryPoint && address.deliveryPoint.length? address.deliveryPoint[0] : "");
-              this.$("#" + this.model.cid + "-address2").val(address.deliveryPoint && address.deliveryPoint.length > 1? address.deliveryPoint[1] : "");
-              this.$("#" + this.model.cid + "-city").val(address.city || "");
-              this.$("#" + this.model.cid + "-state").val(address.administrativeArea || "");
-              this.$("#" + this.model.cid + "-zip").val(address.postalCode || "");
-              this.$("#" + this.model.cid + "-country").val(address.country || "");
-            }
-
-            //If this EML Party is new/empty, then add the new class
-            if(this.isNew){
-              this.$el.addClass("new");
-            }
-
-            //Save the view and model on the element
-            this.$el.data({
-              model: this.model,
-              view: this
-            });
-
-            this.$el.attr("data-category", this.model.get("type"));
-
-            return this;
-          },
-
-          updateModel: function(e){
-            if(!e) return false;
-
-            //Get the attribute that was changed
-            var changedAttr = $(e.target).attr("data-attribute");
-            if(!changedAttr) return false;
-
-            //Get the current value
-            var currentValue = this.model.get(changedAttr);
-
-            //Addresses and Names have special rules for updating
-            switch(changedAttr){
-              case "deliveryPoint":
-                this.updateAddress(e);
-                return;
-              case "city":
-                this.updateAddress(e);
-                return;
-              case "administrativeArea":
-                this.updateAddress(e);
-                return;
-              case "country":
-                this.updateAddress(e);
-                return;
-              case "postalCode":
-                this.updateAddress(e);
-                return;
-              case "surName":
-                this.updateName(e);
-                return;
-              case "givenName":
-                this.updateName(e);
-                return;
-              case "salutation":
-                this.updateName(e);
-                return;
-            }
-
-            //Update the EMLParty model with the new value
-            if(Array.isArray(currentValue)){
-              //Get the position that this new value should go in
-              var position = this.$("[data-attribute='" + changedAttr + "']").index(e.target);
 
-              if( $(e.target).val() == "" ){
-                //Remove the current value from the array if there is no value in the input field
-                currentValue.splice(position, 1);
-              }
-              else{
-
-                var emlModel = this.model.getParentEML(),
-                    value = $(e.target).val();
-
-                if( emlModel ){
-                  value = emlModel.cleanXMLText(value);
-                }
-
-                //Put the new value in the array at the correct position
-                currentValue[position] = value;
-              }
-
-              this.model.set(changedAttr, currentValue);
-
-              this.model.trigger("change:" + changedAttr);
-              this.model.trigger("change");
-            }
-            else{
-              //If the value of the input field is nothing, then reset the field
-              if( $(e.target).val() == "" ){
-                this.model.set(changedAttr, this.model.defaults()[changedAttr]);
-              }
-              else{
-
-                var emlModel = this.model.getParentEML(),
-                    value = $(e.target).val();
-
-                if( emlModel ){
-                  value = emlModel.cleanXMLText(value);
-                }
-
-                this.model.set(changedAttr, value);
-              }
-            }
-
-            //If this is a new EML Party, add it to the parent EML211 model
-            if(this.isNew){
-              var mergeSuccess = this.model.mergeIntoParent();
-
-              //If the merge was sucessfull, mark this as not new
-              if( mergeSuccess  )
-                 this.notNew();
-            }
-
-            //If this EMLParty model has been removed from the parent EML model,
-            //then add it back
-            if( this.model.get("removed") ){
-              var position = this.$el.parent().children(".eml-party").index(this.$el);
-              this.model.get("parentModel").addParty(this.model);
-              this.model.set("removed", false);
-            }
-
-            this.model.trickleUpChange();
-
-          },
-
-          updateAddress: function(e){
-            if(!e) return false;
+        //If this EML Party is new/empty, then add the new class
+        if (this.isNew) {
+          this.$el.addClass("new");
+        }
 
-            //Get the address part that was changed
-            var changedAttr = $(e.target).attr("data-attribute");
-            if(!changedAttr) return false;
+        //Save the view and model on the element
+        this.$el.data({
+          model: this.model,
+          view: this,
+        });
 
-            //TODO: Allow multiple addresses - right now we only support editing the first address
-            var address = this.model.get("address")[0] || {},
-              currentValue = address[changedAttr];
+        this.$el.attr("data-category", this.model.get("type"));
+
+        return this;
+      },
+
+      updateModel: function (e) {
+        if (!e) return false;
+
+        //Get the attribute that was changed
+        var changedAttr = $(e.target).attr("data-attribute");
+        if (!changedAttr) return false;
+
+        //Get the current value
+        var currentValue = this.model.get(changedAttr);
+
+        //Addresses and Names have special rules for updating
+        switch (changedAttr) {
+          case "deliveryPoint":
+            this.updateAddress(e);
+            return;
+          case "city":
+            this.updateAddress(e);
+            return;
+          case "administrativeArea":
+            this.updateAddress(e);
+            return;
+          case "country":
+            this.updateAddress(e);
+            return;
+          case "postalCode":
+            this.updateAddress(e);
+            return;
+          case "surName":
+            this.updateName(e);
+            return;
+          case "givenName":
+            this.updateName(e);
+            return;
+          case "salutation":
+            this.updateName(e);
+            return;
+        }
 
-            //Get the parent EML model and the value from the input element
+        //Update the EMLParty model with the new value
+        if (Array.isArray(currentValue)) {
+          //Get the position that this new value should go in
+          var position = this.$("[data-attribute='" + changedAttr + "']").index(
+            e.target,
+          );
+
+          if ($(e.target).val() == "") {
+            //Remove the current value from the array if there is no value in the input field
+            currentValue.splice(position, 1);
+          } else {
             var emlModel = this.model.getParentEML(),
-                value = $(e.target).val();
+              value = $(e.target).val();
 
-            //If there is a parent EML model, clean up the text for XML
-            if( emlModel ){
+            if (emlModel) {
               value = emlModel.cleanXMLText(value);
             }
 
-            //Update the address
-            if(Array.isArray(currentValue)){
-              //Get the position that this new value should go in
-              var position = this.$("[data-attribute='" + changedAttr + "']").index(e.target);
-
-              //Put the new value in the array at the correct position
-              currentValue[position] = value;
-            }
-            //Make sure delivery points are saved as arrays
-            else if(changedAttr == "deliveryPoint"){
-              address[changedAttr] = [value];
-            }
-            else
-              address[changedAttr] = value;
-
-            //Update the model
-          var allAddresses = this.model.get("address");
-          allAddresses[0] = address;
-          this.model.set("address", allAddresses);
-
-          //If this is a new EML Party, add it to the parent EML211 model
-          if(this.isNew){
-            var mergeSuccess = this.model.mergeIntoParent();
-
-            //If the merge was sucessfull, mark this as not new
-            if( mergeSuccess  )
-               this.notNew();
-          }
-
-          //If this EMLParty model has been removed from the parent EML model,
-          //then add it back
-          if( this.model.get("removed") ){
-            var position = this.$el.parent().children(".eml-party").index(this.$el);
-            this.model.get("parentModel").addParty(this.model);
-            this.model.set("removed", false);
+            //Put the new value in the array at the correct position
+            currentValue[position] = value;
           }
 
-          //Manually trigger the change event since it's an object
-            this.model.trigger("change:address");
-            this.model.trigger("change");
-
-            this.model.trickleUpChange();
-          },
+          this.model.set(changedAttr, currentValue);
 
-          updateName: function(e){
-            if(!e) return false;
-
-            //Get the address part that was changed
-            var changedAttr = $(e.target).attr("data-attribute");
-            if(!changedAttr) return false;
-
-            //TODO: Allow multiple given names - right now we only support editing the first given name
-            var name = this.model.get("individualName") || {},
-            currentValue = String.prototype.trim(name[changedAttr]);
-
-            //Get the parent EML model and the value from the input element
+          this.model.trigger("change:" + changedAttr);
+          this.model.trigger("change");
+        } else {
+          //If the value of the input field is nothing, then reset the field
+          if ($(e.target).val() == "") {
+            this.model.set(changedAttr, this.model.defaults()[changedAttr]);
+          } else {
             var emlModel = this.model.getParentEML(),
-                value = $(e.target).val().trim();
+              value = $(e.target).val();
 
-            //If there is a parent EML model, clean up the text for XML
-            if( emlModel ){
+            if (emlModel) {
               value = emlModel.cleanXMLText(value);
             }
 
-            //Update the name
-            if(Array.isArray(currentValue)){
+            this.model.set(changedAttr, value);
+          }
+        }
 
-              //Get the position that this new value should go in
-              var position = this.$("[data-attribute='" + changedAttr + "']").index(e.target);
+        //If this is a new EML Party, add it to the parent EML211 model
+        if (this.isNew) {
+          var mergeSuccess = this.model.mergeIntoParent();
 
-              //Put the new value in the array at the correct position
-              currentValue[position] = value;
+          //If the merge was sucessfull, mark this as not new
+          if (mergeSuccess) this.notNew();
+        }
 
-            }
-            else if(changedAttr == "givenName"){
-              name.givenName = value;
-            }
-            else
-              name[changedAttr] = value;
+        //If this EMLParty model has been removed from the parent EML model,
+        //then add it back
+        if (this.model.get("removed")) {
+          var position = this.$el
+            .parent()
+            .children(".eml-party")
+            .index(this.$el);
+          this.model.get("parentModel").addParty(this.model);
+          this.model.set("removed", false);
+        }
 
-            //Update the value on the model
-            this.model.set("individualName", name);
+        this.model.trickleUpChange();
+      },
 
-            //If this is a new EML Party, add it to the parent EML211 model
-            if(this.isNew){
-              var mergeSuccess = this.model.mergeIntoParent();
+      updateAddress: function (e) {
+        if (!e) return false;
 
-              //If the merge was sucessfull, mark this as not new
-              if( mergeSuccess  )
-                 this.notNew();
-            }
+        //Get the address part that was changed
+        var changedAttr = $(e.target).attr("data-attribute");
+        if (!changedAttr) return false;
 
-            //If this EMLParty model has been removed from the parent EML model,
-            //then add it back
-            if( this.model.get("removed") ){
-              var position = this.$el.parent().children(".eml-party").index(this.$el);
-              this.model.get("parentModel").addParty(this.model);
-              this.model.set("removed", false);
-            }
+        //TODO: Allow multiple addresses - right now we only support editing the first address
+        var address = this.model.get("address")[0] || {},
+          currentValue = address[changedAttr];
 
-            //Manually trigger a change on the name attribute
-            this.model.trigger("change:individualName");
-            this.model.trigger("change");
+        //Get the parent EML model and the value from the input element
+        var emlModel = this.model.getParentEML(),
+          value = $(e.target).val();
 
-            this.model.trickleUpChange();
-          },
+        //If there is a parent EML model, clean up the text for XML
+        if (emlModel) {
+          value = emlModel.cleanXMLText(value);
+        }
 
-            /**
-             * Validates and displays error messages for the persons' name, position
-             * and organization name.
-             *
-             */
-          showValidation: function() {
+        //Update the address
+        if (Array.isArray(currentValue)) {
+          //Get the position that this new value should go in
+          var position = this.$("[data-attribute='" + changedAttr + "']").index(
+            e.target,
+          );
 
-            //Remove the error styling
-            this.$(".notification").empty();
-                this.$(".error").removeClass("error");
+          //Put the new value in the array at the correct position
+          currentValue[position] = value;
+        }
+        //Make sure delivery points are saved as arrays
+        else if (changedAttr == "deliveryPoint") {
+          address[changedAttr] = [value];
+        } else address[changedAttr] = value;
+
+        //Update the model
+        var allAddresses = this.model.get("address");
+        allAddresses[0] = address;
+        this.model.set("address", allAddresses);
+
+        //If this is a new EML Party, add it to the parent EML211 model
+        if (this.isNew) {
+          var mergeSuccess = this.model.mergeIntoParent();
+
+          //If the merge was sucessfull, mark this as not new
+          if (mergeSuccess) this.notNew();
+        }
 
-                // Check if there are values to validate
-                if( this.isEmpty() ) {
+        //If this EMLParty model has been removed from the parent EML model,
+        //then add it back
+        if (this.model.get("removed")) {
+          var position = this.$el
+            .parent()
+            .children(".eml-party")
+            .index(this.$el);
+          this.model.get("parentModel").addParty(this.model);
+          this.model.set("removed", false);
+        }
+
+        //Manually trigger the change event since it's an object
+        this.model.trigger("change:address");
+        this.model.trigger("change");
 
-                    //Remove this EMLParty model from it's parent model instead
-                    //of showing a validation error, since it's completely empty
-                    this.model.removeFromParent();
+        this.model.trickleUpChange();
+      },
 
-                    return;
+      updateName: function (e) {
+        if (!e) return false;
 
-                }
-                //If the model is valid, exit
-                else if (this.model.isValid()) {
-                    return;
-                }
-                else{
-                  //Start the full error message string for all the EMLParty errors
-                  var errorMessages = "";
+        //Get the address part that was changed
+        var changedAttr = $(e.target).attr("data-attribute");
+        if (!changedAttr) return false;
 
-                  //Iterate over each field that has a validation error
-                  _.mapObject( this.model.validationError, function(errorMsg, attribute){
+        //TODO: Allow multiple given names - right now we only support editing the first given name
+        var name = this.model.get("individualName") || {},
+          currentValue = String.prototype.trim(name[changedAttr]);
 
-                    //Find the input element for this attribute and add the error styling
-                    this.$("[data-attribute='" + attribute + "']").addClass("error");
+        //Get the parent EML model and the value from the input element
+        var emlModel = this.model.getParentEML(),
+          value = $(e.target).val().trim();
 
-                    //Add this error message to the full error messages string
-                    errorMessages += errorMsg + " ";
+        //If there is a parent EML model, clean up the text for XML
+        if (emlModel) {
+          value = emlModel.cleanXMLText(value);
+        }
 
-                  }, this);
+        //Update the name
+        if (Array.isArray(currentValue)) {
+          //Get the position that this new value should go in
+          var position = this.$("[data-attribute='" + changedAttr + "']").index(
+            e.target,
+          );
 
-                //Add the full error message text to the notification area and add the error styling
-                this.$(".notification").text(errorMessages).addClass("error");
+          //Put the new value in the array at the correct position
+          currentValue[position] = value;
+        } else if (changedAttr == "givenName") {
+          name.givenName = value;
+        } else name[changedAttr] = value;
 
-               }
-            },
+        //Update the value on the model
+        this.model.set("individualName", name);
 
-            /**
-             * Checks if the user has entered any data in the fields.
-             *
-             * @return {bool} True if the user hasn't entered any party info, otherwise returns false
-             */
-            isEmpty: function() {
+        //If this is a new EML Party, add it to the parent EML211 model
+        if (this.isNew) {
+          var mergeSuccess = this.model.mergeIntoParent();
 
-                // If we add any new fields, be sure to add the data-attribute here.
-                var attributes = ["country", "city", "administrativeArea", "postalCode", "deliveryPoint","userId",
-                                  "fax", "phone", "onlineUrl", "email", "givenName", "surName", "positionName", "organizationName"];
+          //If the merge was sucessfull, mark this as not new
+          if (mergeSuccess) this.notNew();
+        }
 
-                 for(var i in attributes) {
-                    var attribute = "[data-attribute='"+attributes[i]+"']";
-                    if(this.$(attribute).val() != "")
-                        return false;
-                 }
+        //If this EMLParty model has been removed from the parent EML model,
+        //then add it back
+        if (this.model.get("removed")) {
+          var position = this.$el
+            .parent()
+            .children(".eml-party")
+            .index(this.$el);
+          this.model.get("parentModel").addParty(this.model);
+          this.model.set("removed", false);
+        }
 
-                 return true;
+        //Manually trigger a change on the name attribute
+        this.model.trigger("change:individualName");
+        this.model.trigger("change");
+
+        this.model.trickleUpChange();
+      },
+
+      /**
+       * Validates and displays error messages for the persons' name, position
+       * and organization name.
+       *
+       */
+      showValidation: function () {
+        //Remove the error styling
+        this.$(".notification").empty();
+        this.$(".error").removeClass("error");
+
+        // Check if there are values to validate
+        if (this.isEmpty()) {
+          //Remove this EMLParty model from it's parent model instead
+          //of showing a validation error, since it's completely empty
+          this.model.removeFromParent();
+
+          return;
+        }
+        //If the model is valid, exit
+        else if (this.model.isValid()) {
+          return;
+        } else {
+          //Start the full error message string for all the EMLParty errors
+          var errorMessages = "";
+
+          //Iterate over each field that has a validation error
+          _.mapObject(
+            this.model.validationError,
+            function (errorMsg, attribute) {
+              //Find the input element for this attribute and add the error styling
+              this.$("[data-attribute='" + attribute + "']").addClass("error");
+
+              //Add this error message to the full error messages string
+              errorMessages += errorMsg + " ";
             },
+            this,
+          );
 
-          // A function to format text to look like a phone number
-          formatPhone: function(e){
-                  // Strip all characters from the input except digits
-                  var input = $(e.target).val().replace(/\D/g,'');
-
-                  // Trim the remaining input to ten characters, to preserve phone number format
-                  input = input.substring(0,10);
-
-                  // Based upon the length of the string, we add formatting as necessary
-                  var size = input.length;
-                  if(size == 0){
-                          input = input;
-                  }else if(size < 4){
-                          input = '('+input;
-                  }else if(size < 7){
-                          input = '('+input.substring(0,3)+') '+input.substring(3,6);
-                  }else{
-                          input = '('+input.substring(0,3)+') '+input.substring(3,6)+' - '+input.substring(6,10);
-                  }
-
-                  $(e.target).val(input);
-          },
-
-          previewRemove: function(){
-            this.$("input, img, label").toggleClass("remove-preview");
-          },
-
-          /**
-           * Changes this view and its model from -new- to -not new-
-           * "New" means this EMLParty model is not referenced or stored on a
-           * parent model, and this view is being displayed to the user so they can
-           * add a new party to their EML (versus edit an existing one).
-           */
-          notNew: function(){
-            this.isNew = false;
-
-            this.$el.removeClass("new");
-            this.$el.find(".new").removeClass("new");
-          }
-        });
+          //Add the full error message text to the notification area and add the error styling
+          this.$(".notification").text(errorMessages).addClass("error");
+        }
+      },
+
+      /**
+       * Checks if the user has entered any data in the fields.
+       *
+       * @return {bool} True if the user hasn't entered any party info, otherwise returns false
+       */
+      isEmpty: function () {
+        // If we add any new fields, be sure to add the data-attribute here.
+        var attributes = [
+          "country",
+          "city",
+          "administrativeArea",
+          "postalCode",
+          "deliveryPoint",
+          "userId",
+          "fax",
+          "phone",
+          "onlineUrl",
+          "email",
+          "givenName",
+          "surName",
+          "positionName",
+          "organizationName",
+        ];
+
+        for (var i in attributes) {
+          var attribute = "[data-attribute='" + attributes[i] + "']";
+          if (this.$(attribute).val() != "") return false;
+        }
+
+        return true;
+      },
+
+      // A function to format text to look like a phone number
+      formatPhone: function (e) {
+        // Strip all characters from the input except digits
+        var input = $(e.target).val().replace(/\D/g, "");
+
+        // Trim the remaining input to ten characters, to preserve phone number format
+        input = input.substring(0, 10);
+
+        // Based upon the length of the string, we add formatting as necessary
+        var size = input.length;
+        if (size == 0) {
+          input = input;
+        } else if (size < 4) {
+          input = "(" + input;
+        } else if (size < 7) {
+          input = "(" + input.substring(0, 3) + ") " + input.substring(3, 6);
+        } else {
+          input =
+            "(" +
+            input.substring(0, 3) +
+            ") " +
+            input.substring(3, 6) +
+            " - " +
+            input.substring(6, 10);
+        }
 
-        return EMLPartyView;
-    });
+        $(e.target).val(input);
+      },
+
+      previewRemove: function () {
+        this.$("input, img, label").toggleClass("remove-preview");
+      },
+
+      /**
+       * Changes this view and its model from -new- to -not new-
+       * "New" means this EMLParty model is not referenced or stored on a
+       * parent model, and this view is being displayed to the user so they can
+       * add a new party to their EML (versus edit an existing one).
+       */
+      notNew: function () {
+        this.isNew = false;
+
+        this.$el.removeClass("new");
+        this.$el.find(".new").removeClass("new");
+      },
+    },
+  );
+
+  return EMLPartyView;
+});
 
diff --git a/docs/docs/src_js_views_metadata_EMLTempCoverageView.js.html b/docs/docs/src_js_views_metadata_EMLTempCoverageView.js.html index d4b7b6656..8909abdb1 100644 --- a/docs/docs/src_js_views_metadata_EMLTempCoverageView.js.html +++ b/docs/docs/src_js_views_metadata_EMLTempCoverageView.js.html @@ -44,159 +44,165 @@

Source: src/js/views/metadata/EMLTempCoverageView.js

-
/* global define */
-define(['underscore', 'jquery', 'backbone',
-        'models/metadata/eml211/EMLTemporalCoverage',
-        'text!templates/metadata/dates.html'],
-    function(_, $, Backbone, EMLTemporalCoverage, DatesTemplate){
-
-        /**
-        * @class EMLTempCoverageView
-        * @classdesc The EMLTempCoverage renders the content of an EMLTemporalCoverage model
-        * @classcategory Views/Metadata
-		* @extends Backbone.View
-        */
-        var EMLTempCoverageView = Backbone.View.extend(
-          /** @lends EMLTempCoverageView.prototype */{
-
-        	type: "EMLTempCoverageView",
-
-        	tagName: "div",
-
-        	className: "row-fluid eml-temporal-coverage",
-
-        	attributes: {
-        		"data-category": "temporalCoverage"
-        	},
-
-        	template: _.template(DatesTemplate),
-
-        	initialize: function(options){
-        		if(!options)
-        			var options = {};
-
-        		this.isNew = options.isNew || (options.model? false : true);
-        		this.model = options.model || new EMLTemporalCoverage();
-        		this.edit  = options.edit  || false;
-
-        	},
-
-        	events: {
-        		"change"   : "updateModel",
-        		"focusout" : "showValidation",
-        		"keyup input.error"    : "updateError",
-        		"mouseover .remove"    : "toggleRemoveClass",
-        		"mouseout  .remove"    : "toggleRemoveClass"
-        	},
-
-        	render: function(e) {
-        		//Save the view and model on the element
-        		this.$el.data({
-        			model: this.model,
-        			view: this
-        		});
-
-        		this.$el.append(this.template(this.model.toJSON()));
-
-        		if(this.isNew){
-        			this.$el.addClass("new");
-        		}
-
-        		return this;
-        	},
-
-        	/**
-        	 * Updates the model
-        	 */
-        	updateModel: function(e){
-        		if(!e) return false;
-
-        		e.preventDefault();
-
-        		//Get the attribute and value
-        		var element   = $(e.target),
-        			value     = element.val(),
-        			attribute = element.attr("data-category");
-
-            // Get the parent EML model
-            var emlModel = this.model.getParentEML();
-            //If a parent EML model was found, clean up the text for XML
-            if( emlModel ){
-              value = emlModel.cleanXMLText(value);
-            }
-
-        		//Get the attribute that was changed
-        		if(!attribute) return false;
-
-        		this.model.set(attribute, value);
-
-        		if(this.model.isValid() && this.model.get("parentModel") && this.model.get("parentModel").type == "EML"){
-
-        			this.notNew();
-
-        			this.model.mergeIntoParent();
-        			this.model.trickleUpChange();
-        		}
-
-
-        	},
-
-        	/**
-        	 * If the model isn't valid, show verification messages
-        	 */
-        	showValidation: function(e, options){
-
-
-    			this.$el.find(".notification").empty();
-    			this.$el.find(".error").removeClass("error");
-
-    			//Validate the temporal coverage model
-				if( !this.model.isValid() ){
-					var errors = this.model.validationError;
-
-					_.mapObject(errors, function(errorMsg, category){
-						this.$el.find(".notification").addClass("error").append(errorMsg + " ");
-						this.$el.find("[data-category='" + category + "']").addClass("error");
-					}, this);
-				}
-
-        	},
-
-        	/**
-        	 * When the user is typing in an input with an error, check if they've fixed the error
-        	 */
-        	updateError : function(e){
-        		var input = $(e.target);
-
-        		if(input.val()){
-        			input.removeClass("error");
-
-        			//If there are no more errors, remove the error class from the view
-        			if(!this.$(".error").length){
-            			this.$(".notification.error").text("");
-        				this.$el.removeClass("error");
-        			}
-        		}
-        	},
-
-        	/**
-        	 * Highlight what will be removed when the remove icon is hovered over
-        	 */
-        	toggleRemoveClass: function(){
-        		this.$el.toggleClass("remove-preview");
-        	},
-
-        	/**
-        	 * Unmarks this view as new
-        	 */
-        	notNew: function(){
-        		this.$el.removeClass("new");
-        		this.isNew = false;
-        	}
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/metadata/eml211/EMLTemporalCoverage",
+  "text!templates/metadata/dates.html",
+], function (_, $, Backbone, EMLTemporalCoverage, DatesTemplate) {
+  /**
+   * @class EMLTempCoverageView
+   * @classdesc The EMLTempCoverage renders the content of an EMLTemporalCoverage model
+   * @classcategory Views/Metadata
+   * @extends Backbone.View
+   */
+  var EMLTempCoverageView = Backbone.View.extend(
+    /** @lends EMLTempCoverageView.prototype */ {
+      type: "EMLTempCoverageView",
+
+      tagName: "div",
+
+      className: "row-fluid eml-temporal-coverage",
+
+      attributes: {
+        "data-category": "temporalCoverage",
+      },
+
+      template: _.template(DatesTemplate),
+
+      initialize: function (options) {
+        if (!options) var options = {};
+
+        this.isNew = options.isNew || (options.model ? false : true);
+        this.model = options.model || new EMLTemporalCoverage();
+        this.edit = options.edit || false;
+      },
+
+      events: {
+        change: "updateModel",
+        focusout: "showValidation",
+        "keyup input.error": "updateError",
+        "mouseover .remove": "toggleRemoveClass",
+        "mouseout  .remove": "toggleRemoveClass",
+      },
+
+      render: function (e) {
+        //Save the view and model on the element
+        this.$el.data({
+          model: this.model,
+          view: this,
         });
 
-        return EMLTempCoverageView;
-    });
+        this.$el.append(this.template(this.model.toJSON()));
+
+        if (this.isNew) {
+          this.$el.addClass("new");
+        }
+
+        return this;
+      },
+
+      /**
+       * Updates the model
+       */
+      updateModel: function (e) {
+        if (!e) return false;
+
+        e.preventDefault();
+
+        //Get the attribute and value
+        var element = $(e.target),
+          value = element.val(),
+          attribute = element.attr("data-category");
+
+        // Get the parent EML model
+        var emlModel = this.model.getParentEML();
+        //If a parent EML model was found, clean up the text for XML
+        if (emlModel) {
+          value = emlModel.cleanXMLText(value);
+        }
+
+        //Get the attribute that was changed
+        if (!attribute) return false;
+
+        this.model.set(attribute, value);
+
+        if (
+          this.model.isValid() &&
+          this.model.get("parentModel") &&
+          this.model.get("parentModel").type == "EML"
+        ) {
+          this.notNew();
+
+          this.model.mergeIntoParent();
+          this.model.trickleUpChange();
+        }
+      },
+
+      /**
+       * If the model isn't valid, show verification messages
+       */
+      showValidation: function (e, options) {
+        this.$el.find(".notification").empty();
+        this.$el.find(".error").removeClass("error");
+
+        //Validate the temporal coverage model
+        if (!this.model.isValid()) {
+          var errors = this.model.validationError;
+
+          _.mapObject(
+            errors,
+            function (errorMsg, category) {
+              this.$el
+                .find(".notification")
+                .addClass("error")
+                .append(errorMsg + " ");
+              this.$el
+                .find("[data-category='" + category + "']")
+                .addClass("error");
+            },
+            this,
+          );
+        }
+      },
+
+      /**
+       * When the user is typing in an input with an error, check if they've fixed the error
+       */
+      updateError: function (e) {
+        var input = $(e.target);
+
+        if (input.val()) {
+          input.removeClass("error");
+
+          //If there are no more errors, remove the error class from the view
+          if (!this.$(".error").length) {
+            this.$(".notification.error").text("");
+            this.$el.removeClass("error");
+          }
+        }
+      },
+
+      /**
+       * Highlight what will be removed when the remove icon is hovered over
+       */
+      toggleRemoveClass: function () {
+        this.$el.toggleClass("remove-preview");
+      },
+
+      /**
+       * Unmarks this view as new
+       */
+      notNew: function () {
+        this.$el.removeClass("new");
+        this.isNew = false;
+      },
+    },
+  );
+
+  return EMLTempCoverageView;
+});
 
diff --git a/docs/docs/src_js_views_metadata_ScienceMetadataView.js.html b/docs/docs/src_js_views_metadata_ScienceMetadataView.js.html index d53c03184..bf44934d0 100644 --- a/docs/docs/src_js_views_metadata_ScienceMetadataView.js.html +++ b/docs/docs/src_js_views_metadata_ScienceMetadataView.js.html @@ -44,95 +44,86 @@

Source: src/js/views/metadata/ScienceMetadataView.js

-
/*global define */
-define(['jquery',
-        'jqueryui',
-		'underscore',
-		'backbone'
-		],
-	function($, $ui, _, Backbone) {
-	'use strict';
+            
define(["jquery", "jqueryui", "underscore", "backbone"], function (
+  $,
+  $ui,
+  _,
+  Backbone,
+) {
+  "use strict";
 
   /**
-  * @class ScienceMetadataView
-  * @classdesc The ScienceMetadataView renders the content of a ScienceMetadata model
-  * @classcategory Views/Metadata
-  * @extends Backbone.View
-  */
-	var ScienceMetadataView = Backbone.View.extend(
-    /** @lends ScienceMetadataView.prototype */{
-
-    /**
-    * The ScienceMetadata model to render
-    * @type {ScienceMetadata}
-    */
-		type: "ScienceMetadata",
-
-		initialize: function(){
-
-		},
-
-		render: function(){
-
-		},
-
-		/**
-		 * Takes the text object from a metadata model and returns it as HTML formatted with paragraph elements
-		 */
-		formatParagraphs: function(text, edit){
-			//Get the abstract text
-	    	var paragraphs = [],
-	    		formattedText = "";
-
-	    	//Get the text from the content attribute is it exists
-	    	if(text) text = text;
-
-	    	//Put the abstract in an array format to seperate out paragraphs
-	    	if(typeof text.para == "string")
-	    		paragraphs.push(text.para);
-	    	else if(typeof text == "string")
-	    		paragraphs.push(text || text);
-	    	else if(Array.isArray(text.para)){
-	    		paragraphs = text.para;
-	    	}
-
-	    	//For each paragraph, insert a new line
-	    	_.each(paragraphs, function(p){
-	    		if(edit)
-	    			formattedText += p + "\n";
-	    		else
-	    			formattedText += "<p>" + p + "</p>";
-	    	});
-
-	    	return formattedText;
-		},
-
-		unformatParagraphs: function(htmlText){
-			var paragraphs = htmlText.trim().split("\n"),
-				paragraphsJSON = [];
-
-			_.each(paragraphs, function(p){
-				paragraphsJSON.push(p);
-			});
-
-			return paragraphsJSON;
-		},
-
-	    /**
-	     * When a text element is changed, update the attribute in the model
-	     */
-	    updateText: function(e){
-	    	var textEl = e.target;
-
-	    	//Get the new abstract text
-	    	var newAttr = this.unformatParagraphs($(textEl).val());
-
-	    	//Update the model
-	    	this.model.set($(textEl).attr("data-category"), newAttr);
-	    }
-	});
-
-	return ScienceMetadataView;
+   * @class ScienceMetadataView
+   * @classdesc The ScienceMetadataView renders the content of a ScienceMetadata model
+   * @classcategory Views/Metadata
+   * @extends Backbone.View
+   */
+  var ScienceMetadataView = Backbone.View.extend(
+    /** @lends ScienceMetadataView.prototype */ {
+      /**
+       * The ScienceMetadata model to render
+       * @type {ScienceMetadata}
+       */
+      type: "ScienceMetadata",
+
+      initialize: function () {},
+
+      render: function () {},
+
+      /**
+       * Takes the text object from a metadata model and returns it as HTML formatted with paragraph elements
+       */
+      formatParagraphs: function (text, edit) {
+        //Get the abstract text
+        var paragraphs = [],
+          formattedText = "";
+
+        //Get the text from the content attribute is it exists
+        if (text) text = text;
+
+        //Put the abstract in an array format to seperate out paragraphs
+        if (typeof text.para == "string") paragraphs.push(text.para);
+        else if (typeof text == "string") paragraphs.push(text || text);
+        else if (Array.isArray(text.para)) {
+          paragraphs = text.para;
+        }
+
+        //For each paragraph, insert a new line
+        _.each(paragraphs, function (p) {
+          if (edit) formattedText += p + "\n";
+          else formattedText += "<p>" + p + "</p>";
+        });
+
+        return formattedText;
+      },
+
+      unformatParagraphs: function (htmlText) {
+        var paragraphs = htmlText.trim().split("\n"),
+          paragraphsJSON = [];
+
+        _.each(paragraphs, function (p) {
+          paragraphsJSON.push(p);
+        });
+
+        return paragraphsJSON;
+      },
+
+      /**
+       * When a text element is changed, update the attribute in the model
+       */
+      updateText: function (e) {
+        var textEl = e.target;
+
+        //Get the new abstract text
+        var newAttr = this.unformatParagraphs($(textEl).val());
+
+        //Update the model
+        this.model.set($(textEl).attr("data-category"), newAttr);
+      },
+    },
+  );
+
+  return ScienceMetadataView;
 });
 
diff --git a/docs/docs/src_js_views_portals_PortalDataView.js.html b/docs/docs/src_js_views_portals_PortalDataView.js.html index 37d15c395..72b8ad46e 100644 --- a/docs/docs/src_js_views_portals_PortalDataView.js.html +++ b/docs/docs/src_js_views_portals_PortalDataView.js.html @@ -44,117 +44,126 @@

Source: src/js/views/portals/PortalDataView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "collections/Filters",
-    "views/portals/PortalSectionView",
-    "views/DataCatalogViewWithFilters",
-    "views/filters/FilterGroupsView"],
-    function($, _, Backbone, Filters, PortalSectionView, DataCatalogView, FilterGroupsView){
-
-    /**
-    * @class PortalDataView
-    * @classdesc The PortalDataView is a view to render the
-     * portal data tab (within PortalView) to display all the datasets related to this portal.
-     * @classcategory Views/Portals
-     * @extends PortalSectionView
-     * @constructor
-     */
-      var PortalDataView = PortalSectionView.extend(
-        /** @lends PortalDataView.prototype */{
-
-        tagName: "div",
-
-        /**
-        * The Portal associated with this view
-        * @type {PortalModel}
-        */
-        model: null,
-
-        /**
-        * An array of subviews in this view
-        * @type {Backbone.View[]}
-         */
-        subviews: [],
-
-        /**
-        * The display name for this Section
-        * @type {string}
-        */
-        uniqueSectionLabel: "Data",
-
-        render: function(){
-
-          if( this.id ){
-            this.$el.attr("id", this.id);
-          }
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/Filters",
+  "views/portals/PortalSectionView",
+  "views/DataCatalogViewWithFilters",
+  "views/filters/FilterGroupsView",
+], function (
+  $,
+  _,
+  Backbone,
+  Filters,
+  PortalSectionView,
+  DataCatalogView,
+  FilterGroupsView,
+) {
+  /**
+   * @class PortalDataView
+   * @classdesc The PortalDataView is a view to render the
+   * portal data tab (within PortalView) to display all the datasets related to this portal.
+   * @classcategory Views/Portals
+   * @extends PortalSectionView
+   * @constructor
+   */
+  var PortalDataView = PortalSectionView.extend(
+    /** @lends PortalDataView.prototype */ {
+      tagName: "div",
+
+      /**
+       * The Portal associated with this view
+       * @type {PortalModel}
+       */
+      model: null,
+
+      /**
+       * An array of subviews in this view
+       * @type {Backbone.View[]}
+       */
+      subviews: [],
+
+      /**
+       * The display name for this Section
+       * @type {string}
+       */
+      uniqueSectionLabel: "Data",
+
+      render: function () {
+        if (this.id) {
+          this.$el.attr("id", this.id);
+        }
 
-          var searchResults;
-          var searchModel = this.model.get("searchModel");
+        var searchResults;
+        var searchModel = this.model.get("searchModel");
 
-          //Set some options on the searchResults
-          searchResults = this.model.get("searchResults");
+        //Set some options on the searchResults
+        searchResults = this.model.get("searchResults");
 
-          //If Solr joins are disabled, get the documents and id facets for the PortalMetricsView
-          if( !MetacatUI.appModel.get("enableSolrJoins") ){
-            //Get the documents values as a facet so we can get all the data object IDs
-            searchResults.facet = ["documents", "id"];
-          }
+        //If Solr joins are disabled, get the documents and id facets for the PortalMetricsView
+        if (!MetacatUI.appModel.get("enableSolrJoins")) {
+          //Get the documents values as a facet so we can get all the data object IDs
+          searchResults.facet = ["documents", "id"];
+        }
+
+        //Retrieve only 5 result rows
+        searchResults.rows = 25;
 
-          //Retrieve only 5 result rows
-          searchResults.rows = 25;
-
-          //Hide the Filters that are part of the Collection definition.
-          var searchFilters = this.model.get("searchModel").get("filters");
-          searchFilters.each(function(searchFilter){
-            //Check if this Filter model is also part of the definition filters collection
-            if( this.model.get("definitionFilters").contains(searchFilter) ){
-              searchFilter.set("isInvisible", true);
-            }
-          }, this);
-
-          //Render the filters
-          var filterGroupsView = new FilterGroupsView({
-            filterGroups: this.model.get("filterGroups"),
-            filters: this.model.get("searchModel").get("filters")
-          });
-
-          this.$el.append(filterGroupsView.el);
-          filterGroupsView.render();
-          this.subviews.push(filterGroupsView);
-
-          //Create a DataCatalogView
-          var dataCatalogView = new DataCatalogView({
-            mode: "map",
-            searchModel: this.model.get("searchModel"),
-            searchResults: searchResults,
-            mapModel: this.model.get("mapModel"),
-            isSubView: true,
-            filters: false,
-            fixedHeight: true,
-            filterGroupsView: filterGroupsView
-          });
-
-          this.dataCatalogView = dataCatalogView;
-          var view = this;
-
-          this.$el.append(dataCatalogView.el);
-          this.$el.data("view", this);
-
-          dataCatalogView.render();
-
-          // Listener to handle the semantic annotation search
-          this.listenTo( filterGroupsView, "updateDataCatalogView", function(event, item){
+        //Hide the Filters that are part of the Collection definition.
+        var searchFilters = this.model.get("searchModel").get("filters");
+        searchFilters.each(function (searchFilter) {
+          //Check if this Filter model is also part of the definition filters collection
+          if (this.model.get("definitionFilters").contains(searchFilter)) {
+            searchFilter.set("isInvisible", true);
+          }
+        }, this);
+
+        //Render the filters
+        var filterGroupsView = new FilterGroupsView({
+          filterGroups: this.model.get("filterGroups"),
+          filters: this.model.get("searchModel").get("filters"),
+        });
+
+        this.$el.append(filterGroupsView.el);
+        filterGroupsView.render();
+        this.subviews.push(filterGroupsView);
+
+        //Create a DataCatalogView
+        var dataCatalogView = new DataCatalogView({
+          mode: "map",
+          searchModel: this.model.get("searchModel"),
+          searchResults: searchResults,
+          mapModel: this.model.get("mapModel"),
+          isSubView: true,
+          filters: false,
+          fixedHeight: true,
+          filterGroupsView: filterGroupsView,
+        });
+
+        this.dataCatalogView = dataCatalogView;
+        var view = this;
+
+        this.$el.append(dataCatalogView.el);
+        this.$el.data("view", this);
+
+        dataCatalogView.render();
+
+        // Listener to handle the semantic annotation search
+        this.listenTo(
+          filterGroupsView,
+          "updateDataCatalogView",
+          function (event, item) {
             view.dataCatalogView.updateTextFilters(event, item);
             view.dataCatalogView.triggerSearch();
-          });
-
-        }
-
-     });
+          },
+        );
+      },
+    },
+  );
 
-     return PortalDataView;
+  return PortalDataView;
 });
 
diff --git a/docs/docs/src_js_views_portals_PortalHeaderView.js.html b/docs/docs/src_js_views_portals_PortalHeaderView.js.html index 9f15d51b7..2763b37bc 100644 --- a/docs/docs/src_js_views_portals_PortalHeaderView.js.html +++ b/docs/docs/src_js_views_portals_PortalHeaderView.js.html @@ -44,108 +44,124 @@

Source: src/js/views/portals/PortalHeaderView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "text!templates/portals/portalHeader.html"], function($, _, Backbone, PortalHeaderTemplate){
-
-    /**
-    * @class PortalHeaderView
-    * @classdesc The PortalHeaderView is the view at the top of portal pages
-     * that shows the portal's title, synopsis, and logo
-     * @classcategory Views/Portals
-     * @extends Backbone.View
-     */
-     var PortalHeaderView = Backbone.View.extend(
-       /** @lends PortalHeaderView.prototype */{
-
-        /* The Portal Header Element */
-        el: "#portal-header-container",
-
-        type: "PortalHeader",
-
-        /* Renders the compiled template into HTML */
-        template: _.template(PortalHeaderTemplate),
-
-        initialize: function(options) {
-          this.model = options.model ? options.model : undefined;
-          this.nodeView = options.nodeView ? options.nodeView : undefined;
-        },
-
-        /* Render the view */
-        render: function() {
-
-          var templateInfo = {
-            label: this.model.get("label"),
-            description: this.model.get("description"),
-            name: this.model.get("name"),
-            viewType: "portalView",
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/portals/portalHeader.html",
+], function ($, _, Backbone, PortalHeaderTemplate) {
+  /**
+   * @class PortalHeaderView
+   * @classdesc The PortalHeaderView is the view at the top of portal pages
+   * that shows the portal's title, synopsis, and logo
+   * @classcategory Views/Portals
+   * @extends Backbone.View
+   */
+  var PortalHeaderView = Backbone.View.extend(
+    /** @lends PortalHeaderView.prototype */ {
+      /* The Portal Header Element */
+      el: "#portal-header-container",
+
+      type: "PortalHeader",
+
+      /* Renders the compiled template into HTML */
+      template: _.template(PortalHeaderTemplate),
+
+      initialize: function (options) {
+        this.model = options.model ? options.model : undefined;
+        this.nodeView = options.nodeView ? options.nodeView : undefined;
+      },
+
+      /* Render the view */
+      render: function () {
+        var templateInfo = {
+          label: this.model.get("label"),
+          description: this.model.get("description"),
+          name: this.model.get("name"),
+          viewType: "portalView",
+        };
+
+        if (this.nodeView) {
+          templateInfo.imageURL = this.model.get("logo");
+
+          templateInfo.viewType = "nodeView";
+
+          // Re-inserting the default header
+          $(".PortalView #Navbar").addClass("RepositoryPortalView");
+        } else {
+          if (this.model.get("logo")) {
+            templateInfo.imageURL = this.model.get("logo").get("imageURL");
+          } else {
+            templateInfo.imageURL = "";
           }
+        }
 
-          if ( this.nodeView ) {
-            templateInfo.imageURL = this.model.get("logo");
-
-            templateInfo.viewType = "nodeView"
-
-            // Re-inserting the default header
-            $(".PortalView #Navbar").addClass("RepositoryPortalView");
-          }
-          else {
-            if( this.model.get("logo") ){
-              templateInfo.imageURL = this.model.get("logo").get("imageURL");
-            }
-            else{
-              templateInfo.imageURL = "";
-            }
-          }
-
-
-          this.$el.append(this.template(templateInfo));
-
-          // Insert the member since stat
-          if (this.nodeView) {
-            this.insertMemberSinceStat();
-          }
-
-          return this;
-        },
-
-
-        /*
-        * Insert the member since stat for this node
-        */
-        insertMemberSinceStat: function(){
-
-          //Get the member node object
-          var view = this;
-          var node = _.find(MetacatUI.nodeModel.get("members"), function(nodeModel) {
-						return nodeModel.identifier.toLowerCase() == "urn:node:" + (view.model.get("label")).toLowerCase();
-            });
-
-          //If there is no memberSince date, then hide this statistic and exit
-          if( !node.memberSince ){
-            this.$("#first-upload-container").hide();
-            return;
-          }
-          else{
-            var firstUpload = node.memberSince? new Date(node.memberSince.substring(0, node.memberSince.indexOf("T"))) : new Date();
-          }
-
-
-          // Construct the first upload date sentence
-          var	monthNames = [ "January", "February", "March", "April", "May", "June",
-                            "July", "August", "September", "October", "November", "December" ],
-            m = monthNames[firstUpload.getUTCMonth()],
-            y = firstUpload.getUTCFullYear(),
-            d = firstUpload.getUTCDate();
+        this.$el.append(this.template(templateInfo));
 
-          //For Member Nodes, start all dates at July 2012, the beginning of DataONE
-          this.$("#first-upload-container").text("DataONE Member Repository since " + y);
+        // Insert the member since stat
+        if (this.nodeView) {
+          this.insertMemberSinceStat();
         }
 
-     });
+        return this;
+      },
+
+      /*
+       * Insert the member since stat for this node
+       */
+      insertMemberSinceStat: function () {
+        //Get the member node object
+        var view = this;
+        var node = _.find(
+          MetacatUI.nodeModel.get("members"),
+          function (nodeModel) {
+            return (
+              nodeModel.identifier.toLowerCase() ==
+              "urn:node:" + view.model.get("label").toLowerCase()
+            );
+          },
+        );
+
+        //If there is no memberSince date, then hide this statistic and exit
+        if (!node.memberSince) {
+          this.$("#first-upload-container").hide();
+          return;
+        } else {
+          var firstUpload = node.memberSince
+            ? new Date(
+                node.memberSince.substring(0, node.memberSince.indexOf("T")),
+              )
+            : new Date();
+        }
 
-     return PortalHeaderView;
+        // Construct the first upload date sentence
+        var monthNames = [
+            "January",
+            "February",
+            "March",
+            "April",
+            "May",
+            "June",
+            "July",
+            "August",
+            "September",
+            "October",
+            "November",
+            "December",
+          ],
+          m = monthNames[firstUpload.getUTCMonth()],
+          y = firstUpload.getUTCFullYear(),
+          d = firstUpload.getUTCDate();
+
+        //For Member Nodes, start all dates at July 2012, the beginning of DataONE
+        this.$("#first-upload-container").text(
+          "DataONE Member Repository since " + y,
+        );
+      },
+    },
+  );
+
+  return PortalHeaderView;
 });
 
diff --git a/docs/docs/src_js_views_portals_PortalListView.js.html b/docs/docs/src_js_views_portals_PortalListView.js.html index 71085c5a8..51bb7758b 100644 --- a/docs/docs/src_js_views_portals_PortalListView.js.html +++ b/docs/docs/src_js_views_portals_PortalListView.js.html @@ -44,542 +44,609 @@

Source: src/js/views/portals/PortalListView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "collections/Filters",
-    "collections/SolrResults",
-    "views/PagerView",
-    "text!templates/portals/portalList.html"],
-    function($, _, Backbone, Filters, SearchResults, PagerView, Template){
-
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/Filters",
+  "collections/SolrResults",
+  "views/PagerView",
+  "text!templates/portals/portalList.html",
+], function ($, _, Backbone, Filters, SearchResults, PagerView, Template) {
+  /**
+   * @class PortalListView
+   * @classdesc A view that shows a list of Portals
+   * @classcategory Views/Portals
+   * @extends Backbone.View
+   * @screenshot views/portals/PortalListView.png
+   * @constructor
+   */
+  return Backbone.View.extend(
+    /** @lends PortalListView.prototype */ {
       /**
-      * @class PortalListView
-      * @classdesc A view that shows a list of Portals
-      * @classcategory Views/Portals
-      * @extends Backbone.View
-      * @screenshot views/portals/PortalListView.png
-      * @constructor
-      */
-      return Backbone.View.extend(
-        /** @lends PortalListView.prototype */{
-
-        /**
-        * An array of Filter models  or Filter model JSON to use in the query.
-        * If not provided, a default query will be used.
-        * @type {Filter[]}
-        */
-        filters: null,
-
-        /**
-        * A SolrResults collection that contains the results of the search for the portals
-        * @type {SolrResults}
-        */
-        searchResults: new SearchResults(),
-
-        /**
-        * A comma-separated list of Solr index fields to retrieve when searching for portals
-        * @type {string}
-        * @default "id,seriesId,title,formatId,label,logo"
-        */
-        searchFields: "id,seriesId,title,formatId,label,logo,datasource,writePermission,changePermission,rightsHolder,abstract",
-
-        /**
-        * The number of portals to dispaly per page
-        * @default 10
-        * @type {number}
-        */
-        numPortalsPerPage: 10,
-
-        /**
-        * The number of portals to retrieve and render in this view
-        * @default 100
-        * @type {number}
-        */
-        numPortals: 100,
-
-        /**
-        * An array of additional SolrResult models for portals that will be displayed
-        * in this view in addition to the SolrResults found as a result of the search.
-        * These could be portals that wouldn't otherwise be found by a search but should be displayed anyway.
-        * @type {SolrResult[]}
-        */
-        additionalPortalsToDisplay: [],
-
-        /**
-        * The message to display when there are no portals in this list
-        * @type {string}
-        */
-        noResultsMessage: "You haven't created or have access to any " + MetacatUI.appModel.get("portalTermPlural") + " yet.",
-
-        /**
-        * A jQuery selector for the element that the list should be inserted into
-        * @type {string}
-        */
-        listContainer: ".portal-list-container",
-        /**
-        * A jQuery selector for the element that the Create Portal should be inserted into
-        * @type {string}
-        */
-        createBtnContainer: ".create-btn-container",
-
-        /**
-        * References to templates for this view. HTML files are converted to Underscore.js templates
-        */
-        template: _.template(Template),
-
-        /**
-        * Initializes a new view
-        */
-        initialize: function(){
-          //Create a new SearchResults collection
-          this.searchResults = new SearchResults();
-        },
-
-        /**
-        * Renders the list of portals
-        */
-        render: function(){
-
-          try{
-
-            //If the "my portals" feature is disabled, exit now
-            if(MetacatUI.appModel.get("showMyPortals") === false){
-              return;
-            }
-
-            //Insert the template
-            this.$el.html( this.template() );
+       * An array of Filter models  or Filter model JSON to use in the query.
+       * If not provided, a default query will be used.
+       * @type {Filter[]}
+       */
+      filters: null,
 
-            //If there are no given filters, create default ones
-            if( !this.filters ){
-              //Create search filters for finding the portals
-              var filters = new Filters();
+      /**
+       * A SolrResults collection that contains the results of the search for the portals
+       * @type {SolrResults}
+       */
+      searchResults: new SearchResults(),
 
-              //Filter datasets that the user has ownership of
-              filters.addWritePermissionFilter();
+      /**
+       * A comma-separated list of Solr index fields to retrieve when searching for portals
+       * @type {string}
+       * @default "id,seriesId,title,formatId,label,logo"
+       */
+      searchFields:
+        "id,seriesId,title,formatId,label,logo,datasource,writePermission,changePermission,rightsHolder,abstract",
 
-              this.filters = filters;
-            }
-            //If the filters set on this view is an array of JSON, add it to a Filters collection
-            else if( this.filters.length && !Filters.prototype.isPrototypeOf(this.filters) ){
-              //Create search filters for finding the portals
-              var filters = new Filters();
+      /**
+       * The number of portals to dispaly per page
+       * @default 10
+       * @type {number}
+       */
+      numPortalsPerPage: 10,
 
-              filters.add( this.filters );
+      /**
+       * The number of portals to retrieve and render in this view
+       * @default 100
+       * @type {number}
+       */
+      numPortals: 100,
 
-              this.filters = filters;
-            }
-            //If there is an empty array, create a new Filters collection
-            else if( !this.filters.length ){
-              this.filters = new Filters();
-            }
+      /**
+       * An array of additional SolrResult models for portals that will be displayed
+       * in this view in addition to the SolrResults found as a result of the search.
+       * These could be portals that wouldn't otherwise be found by a search but should be displayed anyway.
+       * @type {SolrResult[]}
+       */
+      additionalPortalsToDisplay: [],
 
-            //Get the search results and render them
-            this.getSearchResults();
+      /**
+       * The message to display when there are no portals in this list
+       * @type {string}
+       */
+      noResultsMessage:
+        "You haven't created or have access to any " +
+        MetacatUI.appModel.get("portalTermPlural") +
+        " yet.",
 
-            //Display any additional portals in the list that have been passed to
-            // the view directly.
-            _.each(this.additionalPortalsToDisplay, function(searchResult){
-              //Get the list container element
-              var listContainer = this.$(this.listContainer);
+      /**
+       * A jQuery selector for the element that the list should be inserted into
+       * @type {string}
+       */
+      listContainer: ".portal-list-container",
+      /**
+       * A jQuery selector for the element that the Create Portal should be inserted into
+       * @type {string}
+       */
+      createBtnContainer: ".create-btn-container",
 
-              //Remove any 'loading' elements before adding items to the list
-              listContainer.find(".loading").remove();
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      template: _.template(Template),
 
-              //Create a list item element and add the search result element
-              // to the list container
-              listContainer.append(this.createListItem(searchResult));
-            }, this);
-
-            if( this.additionalPortalsToDisplay.length ){
-              //While the search is being sent for the other portals in this list,
-              // show a loading sign underneath the additional portals we just displayed.
-              var loadingListItem = this.createListItem();
-              loadingListItem.html("<td class='loading subtle' colspan='4'>Loading more " +
-                                     MetacatUI.appModel.get("portalTermPlural") + "...</td>");
-              this.$(this.listContainer).append(loadingListItem);
-            }
+      /**
+       * Initializes a new view
+       */
+      initialize: function () {
+        //Create a new SearchResults collection
+        this.searchResults = new SearchResults();
+      },
 
-          }
-          catch(e){
-            console.error(e);
+      /**
+       * Renders the list of portals
+       */
+      render: function () {
+        try {
+          //If the "my portals" feature is disabled, exit now
+          if (MetacatUI.appModel.get("showMyPortals") === false) {
+            return;
           }
 
-        },
+          //Insert the template
+          this.$el.html(this.template());
 
-        /**
-        * Queries for the portal objects using the SearchResults collection
-        */
-        getSearchResults: function(){
+          //If there are no given filters, create default ones
+          if (!this.filters) {
+            //Create search filters for finding the portals
+            var filters = new Filters();
 
-          try{
+            //Filter datasets that the user has ownership of
+            filters.addWritePermissionFilter();
 
-            //Filter by the portal format ID
-            this.filters.add({
-              fields: ["formatId"],
-              values: ["dataone.org/portals"],
-              matchSubstring: true,
-              exclude: false
-            });
-
-            //Filter datasets by their ownership
-            this.filters.add({
-              fields: ["obsoletedBy"],
-              values: ["*"],
-              matchSubstring: false,
-              exclude: true
-            });
-
-            //Get 100 rows
-            this.searchResults.rows = this.numPortals;
-
-            //The fields to return
-            this.searchResults.fields = this.searchFields;
-
-            //Set the query service URL
-            try{
-              if( MetacatUI.appModel.get("defaultAlternateRepositoryId") ){
-                var mnToQuery = _.findWhere( MetacatUI.appModel.get("alternateRepositories"), { identifier: MetacatUI.appModel.get("defaultAlternateRepositoryId") } );
-                if( mnToQuery ){
-                  this.searchResults.queryServiceUrl = mnToQuery.queryServiceUrl;
-                }
-              }
-            }
-            catch(e){
-              console.error("Could not get active alt repo. ", e);
-            }
-
-            //Set the query on the SearchResults
-            this.searchResults.setQuery( this.filters.getQuery() );
+            this.filters = filters;
+          }
+          //If the filters set on this view is an array of JSON, add it to a Filters collection
+          else if (
+            this.filters.length &&
+            !Filters.prototype.isPrototypeOf(this.filters)
+          ) {
+            //Create search filters for finding the portals
+            var filters = new Filters();
 
-            //Listen to the search results collection and render the results when the search is complete
-            this.listenToOnce( this.searchResults, "reset", this.renderList );
-            //Listen to the search results collection for errors
-            this.listenToOnce( this.searchResults, "error", this.showError );
+            filters.add(this.filters);
 
-            //Get the first page of results
-            this.searchResults.toPage(0);
+            this.filters = filters;
           }
-          catch(e){
-            this.showError();
-            console.error("Failed to fetch the SearchResults for the PortalsList: ", e);
+          //If there is an empty array, create a new Filters collection
+          else if (!this.filters.length) {
+            this.filters = new Filters();
           }
-        },
-
-        /**
-        * Renders each search result from the SolrResults collection
-        */
-        renderList: function(){
-
-          try{
 
-            //Get the list container element
-            var listContainer = this.$(this.listContainer);
+          //Get the search results and render them
+          this.getSearchResults();
 
-            //If no search results were found, display a message
-            if( (!this.searchResults || !this.searchResults.length) && !this.additionalPortalsToDisplay.length){
-              var row = this.createListItem();
-              row.html("<div class='no-results'>" + this.noResultsMessage + "</div>");
-              listContainer.html(row);
-
-              //Add a "Create" button to create a new portal
-              this.renderCreateButton();
-
-              return;
-            }
-
-            //Remove any 'loading' elements before adding items to the list
-            listContainer.find(".loading").remove();
+          //Display any additional portals in the list that have been passed to
+          // the view directly.
+          _.each(
+            this.additionalPortalsToDisplay,
+            function (searchResult) {
+              //Get the list container element
+              var listContainer = this.$(this.listContainer);
 
-            //Iterate over each search result and render it
-            this.searchResults.each(function(searchResult){
+              //Remove any 'loading' elements before adding items to the list
+              listContainer.find(".loading").remove();
 
               //Create a list item element and add the search result element
               // to the list container
               listContainer.append(this.createListItem(searchResult));
-
-            }, this);
-
-            //Add a "Create" button to create a new portal
-            this.renderCreateButton();
-
-            // Create a pager for this list if there are many portals
-            if( this.$(".portals-list-entry").length > this.numPortalsPerPage ){
-              var pager = new PagerView({
-                  pages: this.$(".portals-list-entry"),
-                  itemsPerPage: this.numPortalsPerPage
-              });
-
-              this.$el.append(pager.render().el);
-            }
-
+            },
+            this,
+          );
+
+          if (this.additionalPortalsToDisplay.length) {
+            //While the search is being sent for the other portals in this list,
+            // show a loading sign underneath the additional portals we just displayed.
+            var loadingListItem = this.createListItem();
+            loadingListItem.html(
+              "<td class='loading subtle' colspan='4'>Loading more " +
+                MetacatUI.appModel.get("portalTermPlural") +
+                "...</td>",
+            );
+            this.$(this.listContainer).append(loadingListItem);
           }
-          catch(e){
-            console.error(e);
-
-            this.showError();
+        } catch (e) {
+          console.error(e);
+        }
+      },
 
+      /**
+       * Queries for the portal objects using the SearchResults collection
+       */
+      getSearchResults: function () {
+        try {
+          //Filter by the portal format ID
+          this.filters.add({
+            fields: ["formatId"],
+            values: ["dataone.org/portals"],
+            matchSubstring: true,
+            exclude: false,
+          });
+
+          //Filter datasets by their ownership
+          this.filters.add({
+            fields: ["obsoletedBy"],
+            values: ["*"],
+            matchSubstring: false,
+            exclude: true,
+          });
+
+          //Get 100 rows
+          this.searchResults.rows = this.numPortals;
+
+          //The fields to return
+          this.searchResults.fields = this.searchFields;
+
+          //Set the query service URL
+          try {
+            if (MetacatUI.appModel.get("defaultAlternateRepositoryId")) {
+              var mnToQuery = _.findWhere(
+                MetacatUI.appModel.get("alternateRepositories"),
+                {
+                  identifier: MetacatUI.appModel.get(
+                    "defaultAlternateRepositoryId",
+                  ),
+                },
+              );
+              if (mnToQuery) {
+                this.searchResults.queryServiceUrl = mnToQuery.queryServiceUrl;
+              }
+            }
+          } catch (e) {
+            console.error("Could not get active alt repo. ", e);
           }
 
-        },
+          //Set the query on the SearchResults
+          this.searchResults.setQuery(this.filters.getQuery());
+
+          //Listen to the search results collection and render the results when the search is complete
+          this.listenToOnce(this.searchResults, "reset", this.renderList);
+          //Listen to the search results collection for errors
+          this.listenToOnce(this.searchResults, "error", this.showError);
+
+          //Get the first page of results
+          this.searchResults.toPage(0);
+        } catch (e) {
+          this.showError();
+          console.error(
+            "Failed to fetch the SearchResults for the PortalsList: ",
+            e,
+          );
+        }
+      },
 
-        /**
-        * Creates a table row for the given portal SolrResult model
-        * @param {SolrResult} - The SolrResult model that represent the portal
-        * @return {Element}
-        */
-        createListItem: function(searchResult){
+      /**
+       * Renders each search result from the SolrResults collection
+       */
+      renderList: function () {
+        try {
+          //Get the list container element
+          var listContainer = this.$(this.listContainer);
+
+          //If no search results were found, display a message
+          if (
+            (!this.searchResults || !this.searchResults.length) &&
+            !this.additionalPortalsToDisplay.length
+          ) {
+            var row = this.createListItem();
+            row.html(
+              "<div class='no-results'>" + this.noResultsMessage + "</div>",
+            );
+            listContainer.html(row);
 
-          try{
-            var listItem = $(document.createElement("div")).addClass("portals-list-entry");
+            //Add a "Create" button to create a new portal
+            this.renderCreateButton();
 
-            if( searchResult && typeof searchResult.get == "function" ){
+            return;
+          }
 
-              //Don't render a list item for a portal that is already there
-              if( this.$("tr[data-seriesId='" + searchResult.get("seriesId") + "']").length ){
-                return listItem;
-              }
+          //Remove any 'loading' elements before adding items to the list
+          listContainer.find(".loading").remove();
 
-              //Add an id to the list element
-              listItem.attr("data-seriesId", searchResult.get("seriesId"));
+          //Iterate over each search result and render it
+          this.searchResults.each(function (searchResult) {
+            //Create a list item element and add the search result element
+            // to the list container
+            listContainer.append(this.createListItem(searchResult));
+          }, this);
 
-              //Create a logo image
-              var logoImg = "";
-              var logoDiv = "";
+          //Add a "Create" button to create a new portal
+          this.renderCreateButton();
 
-              // Add link to the portal to the list item
-              var link = $(document.createElement("a"))
-                          .attr("href", MetacatUI.root + "/" + MetacatUI.appModel.get("portalTermPlural")
-                          + "/" + encodeURIComponent((searchResult.get("label") || searchResult.get("seriesId") || searchResult.get("id"))) );
+          // Create a pager for this list if there are many portals
+          if (this.$(".portals-list-entry").length > this.numPortalsPerPage) {
+            var pager = new PagerView({
+              pages: this.$(".portals-list-entry"),
+              itemsPerPage: this.numPortalsPerPage,
+            });
 
-              if( searchResult.get("logo")) {
-                if( !searchResult.get("logo").startsWith("http") ){
+            this.$el.append(pager.render().el);
+          }
+        } catch (e) {
+          console.error(e);
 
-                  var urlBase = "";
+          this.showError();
+        }
+      },
 
-                  //If there are alt repos configured, use the datasource obbject service URL
-                  if( MetacatUI.appModel.get("alternateRepositories").length && searchResult.get("datasource") ){
-                    var sourceMN = _.findWhere(MetacatUI.appModel.get("alternateRepositories"), { identifier: searchResult.get("datasource") });
-                    if( sourceMN ){
-                      urlBase = sourceMN.objectServiceUrl;
-                    }
-                  }
+      /**
+       * Creates a table row for the given portal SolrResult model
+       * @param {SolrResult} - The SolrResult model that represent the portal
+       * @return {Element}
+       */
+      createListItem: function (searchResult) {
+        try {
+          var listItem = $(document.createElement("div")).addClass(
+            "portals-list-entry",
+          );
+
+          if (searchResult && typeof searchResult.get == "function") {
+            //Don't render a list item for a portal that is already there
+            if (
+              this.$("tr[data-seriesId='" + searchResult.get("seriesId") + "']")
+                .length
+            ) {
+              return listItem;
+            }
 
-                  if( !urlBase ){
-                    // use the resolve service if there is no object service url
-                    // (e.g. in DataONE theme)
-                    urlBase = MetacatUI.appModel.get("objectServiceUrl") ||
-                              MetacatUI.appModel.get("resolveServiceUrl");
+            //Add an id to the list element
+            listItem.attr("data-seriesId", searchResult.get("seriesId"));
+
+            //Create a logo image
+            var logoImg = "";
+            var logoDiv = "";
+
+            // Add link to the portal to the list item
+            var link = $(document.createElement("a")).attr(
+              "href",
+              MetacatUI.root +
+                "/" +
+                MetacatUI.appModel.get("portalTermPlural") +
+                "/" +
+                encodeURIComponent(
+                  searchResult.get("label") ||
+                    searchResult.get("seriesId") ||
+                    searchResult.get("id"),
+                ),
+            );
+
+            if (searchResult.get("logo")) {
+              if (!searchResult.get("logo").startsWith("http")) {
+                var urlBase = "";
+
+                //If there are alt repos configured, use the datasource obbject service URL
+                if (
+                  MetacatUI.appModel.get("alternateRepositories").length &&
+                  searchResult.get("datasource")
+                ) {
+                  var sourceMN = _.findWhere(
+                    MetacatUI.appModel.get("alternateRepositories"),
+                    { identifier: searchResult.get("datasource") },
+                  );
+                  if (sourceMN) {
+                    urlBase = sourceMN.objectServiceUrl;
                   }
-
-                  searchResult.set("logo", urlBase + searchResult.get("logo") );
                 }
 
-                var logoImg = $(document.createElement("img"))
-                          .attr("src", searchResult.get("logo"))
-                          .attr("alt", searchResult.get("title") + " logo");
-                var logoLink = link.clone().append(logoImg);
-
-                logoDiv = $(document.createElement("div"))
-                          .addClass("portal-logo")
-                          .append(logoLink);
-
-              } else {
-                // Create an empty <div>, as no portal image is available.
-                logoDiv = $(document.createElement("div"))
-                          .addClass("portal-logo");
-              }
-
-              var portalTitle = $(document.createElement("h5"))
-                                .addClass("portal-title")
-                                .text(searchResult.get("title"));
-              var titleLink = link.clone().append(portalTitle);
-
-              var descriptionText = searchResult.get("abstract") || "",
-                  maxLength = window.innerWidth < 800 ? 150 : 300;
-              if( descriptionText.length > maxLength ){
-                descriptionText = descriptionText.substr(0, maxLength);
-                descriptionText = descriptionText.substr(0, Math.min(descriptionText.length, descriptionText.lastIndexOf(" ")));
-                descriptionText += "...";
-              }
-              var description = $(document.createElement("div"))
-                                .addClass("portal-description")
-                                .append( $(document.createElement("p"))
-                                          .text(descriptionText) );
-
-              var portalInfo = $(document.createElement("div"))
-                                 .addClass("portal-info")
-                                 .append(titleLink, description);
-
-              var editDiv = $(document.createElement("div"))
-                            .addClass("portal-edit-link")
-                            .addClass("controls");
-
-              //Add all the elements to the row
-              listItem.append(logoDiv, portalInfo, editDiv);
-
-              //Construct an array of ownership subjects
-              var wPermission = searchResult.get("writePermission"),
-                  cPermission = searchResult.get("changePermission"),
-                  rightsHolder = searchResult.get("rightsHolder");
-              var owners = [];
-
-              [wPermission, cPermission, rightsHolder].forEach( subjects => {
-                if( typeof subjects == "string" ){
-                  owners.push(subjects);
+                if (!urlBase) {
+                  // use the resolve service if there is no object service url
+                  // (e.g. in DataONE theme)
+                  urlBase =
+                    MetacatUI.appModel.get("objectServiceUrl") ||
+                    MetacatUI.appModel.get("resolveServiceUrl");
                 }
-                else if( Array.isArray(subjects) ){
-                  owners = owners.concat(subjects);
-                }
-              });
 
-              //Render an Edit button
-              if ( MetacatUI.appUserModel.hasIdentityOverlap(owners) ){
-                  //Create an Edit buttton
-                  var editButton = $(document.createElement("a")).attr("href",
-                               MetacatUI.root + "/edit/"+ MetacatUI.appModel.get("portalTermPlural") +"/" + encodeURIComponent((searchResult.get("label") || searchResult.get("seriesId") || searchResult.get("id"))) )
-                               .text("Edit")
-                               .addClass("btn edit");
-                  editDiv.append(editButton);
+                searchResult.set("logo", urlBase + searchResult.get("logo"));
               }
 
+              var logoImg = $(document.createElement("img"))
+                .attr("src", searchResult.get("logo"))
+                .attr("alt", searchResult.get("title") + " logo");
+              var logoLink = link.clone().append(logoImg);
+
+              logoDiv = $(document.createElement("div"))
+                .addClass("portal-logo")
+                .append(logoLink);
+            } else {
+              // Create an empty <div>, as no portal image is available.
+              logoDiv = $(document.createElement("div")).addClass(
+                "portal-logo",
+              );
             }
 
-            //Return the list item
-            return listItem;
-          }
-          catch(e){
-            console.error(e);
-            return "";
-          }
-        },
-
-        /**
-        * Renders a "Create" button for the user to create a new portal
-        */
-        renderCreateButton: function(){
-          try{
-
-            //If the authorization hasn't been checked yet
-            if( MetacatUI.appUserModel.get("isAuthorizedCreatePortal") !== true &&
-                MetacatUI.appUserModel.get("isAuthorizedCreatePortal") !== false ){
-              //Check is this user is authorized to create a new portal
-              this.listenToOnce( MetacatUI.appUserModel, "change:isAuthorizedCreatePortal", this.renderCreateButton);
-              MetacatUI.appUserModel.isAuthorizedCreatePortal();
+            var portalTitle = $(document.createElement("h5"))
+              .addClass("portal-title")
+              .text(searchResult.get("title"));
+            var titleLink = link.clone().append(portalTitle);
+
+            var descriptionText = searchResult.get("abstract") || "",
+              maxLength = window.innerWidth < 800 ? 150 : 300;
+            if (descriptionText.length > maxLength) {
+              descriptionText = descriptionText.substr(0, maxLength);
+              descriptionText = descriptionText.substr(
+                0,
+                Math.min(
+                  descriptionText.length,
+                  descriptionText.lastIndexOf(" "),
+                ),
+              );
+              descriptionText += "...";
             }
-            else{
-
-              //Create a New portal buttton
-              var createButton = $(document.createElement("a"))
-                                 .addClass("btn btn-primary")
-                                 .append( $(document.createElement("i")).addClass("icon icon-plus icon-on-left"),
-                                   "New " + MetacatUI.appModel.get('portalTermSingular'));
-
-              var isNotAuthorizedNoBookkeeper   = !MetacatUI.appModel.get("enableBookkeeperServices") &&
-                                                   MetacatUI.appUserModel.get("isAuthorizedCreatePortal") === false,
-                  reachedLimitWithBookkeeper    = MetacatUI.appModel.get("enableBookkeeperServices") &&
-                                                  MetacatUI.appUserModel.get("isAuthorizedCreatePortal") === false,
-                  reachedLimitWithoutBookkeeper = !MetacatUI.appModel.get("enableBookkeeperServices") &&
-                                                   MetacatUI.appModel.get("portalLimit") <= this.searchResults.length;
-
-              //If creating portals is disabled in the entire app, or is only limited to certain groups,
-              // then don't show the Create button.
-              if( isNotAuthorizedNoBookkeeper ){
-                return;
-              }
-              //If creating portals is enabled, but this person is unauthorized because of Bookkeeper info,
-              // then show the Create button as disabled.
-              else if( reachedLimitWithBookkeeper || reachedLimitWithoutBookkeeper ){
-
-                 //Disable the button
-                 createButton.addClass("disabled");
-
-                 //Add the create button to the view
-                 this.$(this.createBtnContainer).html(createButton);
-
-                 var message = "You've already reached the " + MetacatUI.appModel.get("portalTermSingular") +
-                               " limit for your ";
-
-                 if( MetacatUI.appModel.get("enableBookkeeperServices") ){
-                   message += MetacatUI.appModel.get("dataonePlusName");
-
-                   if( MetacatUI.appModel.get("dataonePlusPreviewMode") ){
-                     message += " free preview. ";
-                   }
-                   else{
-                     message += " subscription. ";
-                   }
-
-                   var portalQuotas = MetacatUI.appUserModel.getQuotas("portal");
-                   if( portalQuotas.length ){
-                     message += "(" + portalQuotas[0].get("softLimit") + " " +
-                                ((portalQuotas[0].get("softLimit") > 1)? MetacatUI.appModel.get("portalTermPlural") : MetacatUI.appModel.get("portalTermSingular")) + ")";
-                   }
-
-                   message += " Contact us to upgrade your subscription.";
-
-                 }
-                 else{
-                   message += " account. ";
-
-                   var portalLimit = MetacatUI.appModel.get("portalLimit");
-                   if( portalLimit > 0 ){
-                     message += "(" + portalLimit + " " +
-                                ((portalLimit > 1)? MetacatUI.appModel.get("portalTermPlural") : MetacatUI.appModel.get("portalTermSingular")) +
-                                ")"
-                   }
-                 }
-
-                 //Add the tooltip to the button
-                 createButton.tooltip({
-                   placement: "top",
-                   trigger: "hover click focus",
-                   delay: {
-                     show: 500
-                   },
-                   title: message
-                 });
+            var description = $(document.createElement("div"))
+              .addClass("portal-description")
+              .append($(document.createElement("p")).text(descriptionText));
+
+            var portalInfo = $(document.createElement("div"))
+              .addClass("portal-info")
+              .append(titleLink, description);
+
+            var editDiv = $(document.createElement("div"))
+              .addClass("portal-edit-link")
+              .addClass("controls");
+
+            //Add all the elements to the row
+            listItem.append(logoDiv, portalInfo, editDiv);
+
+            //Construct an array of ownership subjects
+            var wPermission = searchResult.get("writePermission"),
+              cPermission = searchResult.get("changePermission"),
+              rightsHolder = searchResult.get("rightsHolder");
+            var owners = [];
+
+            [wPermission, cPermission, rightsHolder].forEach((subjects) => {
+              if (typeof subjects == "string") {
+                owners.push(subjects);
+              } else if (Array.isArray(subjects)) {
+                owners = owners.concat(subjects);
               }
-              else{
+            });
 
-                //Add the link URL to the button
-                createButton.attr("href", MetacatUI.root + "/edit/" + MetacatUI.appModel.get("portalTermPlural"))
+            //Render an Edit button
+            if (MetacatUI.appUserModel.hasIdentityOverlap(owners)) {
+              //Create an Edit buttton
+              var editButton = $(document.createElement("a"))
+                .attr(
+                  "href",
+                  MetacatUI.root +
+                    "/edit/" +
+                    MetacatUI.appModel.get("portalTermPlural") +
+                    "/" +
+                    encodeURIComponent(
+                      searchResult.get("label") ||
+                        searchResult.get("seriesId") ||
+                        searchResult.get("id"),
+                    ),
+                )
+                .text("Edit")
+                .addClass("btn edit");
+              editDiv.append(editButton);
+            }
+          }
 
-                //Add the create button to the view
-                this.$(this.createBtnContainer).html(createButton);
-              }
+          //Return the list item
+          return listItem;
+        } catch (e) {
+          console.error(e);
+          return "";
+        }
+      },
 
-              //Reset the isAuthorizedCreatePortal attribute
-              MetacatUI.appUserModel.set("isAuthorizedCreatePortal", null);
+      /**
+       * Renders a "Create" button for the user to create a new portal
+       */
+      renderCreateButton: function () {
+        try {
+          //If the authorization hasn't been checked yet
+          if (
+            MetacatUI.appUserModel.get("isAuthorizedCreatePortal") !== true &&
+            MetacatUI.appUserModel.get("isAuthorizedCreatePortal") !== false
+          ) {
+            //Check is this user is authorized to create a new portal
+            this.listenToOnce(
+              MetacatUI.appUserModel,
+              "change:isAuthorizedCreatePortal",
+              this.renderCreateButton,
+            );
+            MetacatUI.appUserModel.isAuthorizedCreatePortal();
+          } else {
+            //Create a New portal buttton
+            var createButton = $(document.createElement("a"))
+              .addClass("btn btn-primary")
+              .append(
+                $(document.createElement("i")).addClass(
+                  "icon icon-plus icon-on-left",
+                ),
+                "New " + MetacatUI.appModel.get("portalTermSingular"),
+              );
+
+            var isNotAuthorizedNoBookkeeper =
+                !MetacatUI.appModel.get("enableBookkeeperServices") &&
+                MetacatUI.appUserModel.get("isAuthorizedCreatePortal") ===
+                  false,
+              reachedLimitWithBookkeeper =
+                MetacatUI.appModel.get("enableBookkeeperServices") &&
+                MetacatUI.appUserModel.get("isAuthorizedCreatePortal") ===
+                  false,
+              reachedLimitWithoutBookkeeper =
+                !MetacatUI.appModel.get("enableBookkeeperServices") &&
+                MetacatUI.appModel.get("portalLimit") <=
+                  this.searchResults.length;
+
+            //If creating portals is disabled in the entire app, or is only limited to certain groups,
+            // then don't show the Create button.
+            if (isNotAuthorizedNoBookkeeper) {
+              return;
             }
-          }
-          catch(e){
-            console.error(e);
-          }
-        },
+            //If creating portals is enabled, but this person is unauthorized because of Bookkeeper info,
+            // then show the Create button as disabled.
+            else if (
+              reachedLimitWithBookkeeper ||
+              reachedLimitWithoutBookkeeper
+            ) {
+              //Disable the button
+              createButton.addClass("disabled");
+
+              //Add the create button to the view
+              this.$(this.createBtnContainer).html(createButton);
+
+              var message =
+                "You've already reached the " +
+                MetacatUI.appModel.get("portalTermSingular") +
+                " limit for your ";
+
+              if (MetacatUI.appModel.get("enableBookkeeperServices")) {
+                message += MetacatUI.appModel.get("dataonePlusName");
+
+                if (MetacatUI.appModel.get("dataonePlusPreviewMode")) {
+                  message += " free preview. ";
+                } else {
+                  message += " subscription. ";
+                }
 
-        /**
-        * Displays an error message when rendering this view has failed.
-        */
-        showError: function(){
+                var portalQuotas = MetacatUI.appUserModel.getQuotas("portal");
+                if (portalQuotas.length) {
+                  message +=
+                    "(" +
+                    portalQuotas[0].get("softLimit") +
+                    " " +
+                    (portalQuotas[0].get("softLimit") > 1
+                      ? MetacatUI.appModel.get("portalTermPlural")
+                      : MetacatUI.appModel.get("portalTermSingular")) +
+                    ")";
+                }
 
-          //Remove the loading elements
-          this.$(this.listContainer).find(".loading").remove();
+                message += " Contact us to upgrade your subscription.";
+              } else {
+                message += " account. ";
+
+                var portalLimit = MetacatUI.appModel.get("portalLimit");
+                if (portalLimit > 0) {
+                  message +=
+                    "(" +
+                    portalLimit +
+                    " " +
+                    (portalLimit > 1
+                      ? MetacatUI.appModel.get("portalTermPlural")
+                      : MetacatUI.appModel.get("portalTermSingular")) +
+                    ")";
+                }
+              }
 
-          if( this.$(this.listContainer).children("tr").length == 0 ){
+              //Add the tooltip to the button
+              createButton.tooltip({
+                placement: "top",
+                trigger: "hover click focus",
+                delay: {
+                  show: 500,
+                },
+                title: message,
+              });
+            } else {
+              //Add the link URL to the button
+              createButton.attr(
+                "href",
+                MetacatUI.root +
+                  "/edit/" +
+                  MetacatUI.appModel.get("portalTermPlural"),
+              );
+
+              //Add the create button to the view
+              this.$(this.createBtnContainer).html(createButton);
+            }
 
-            //Show an error message
-            MetacatUI.appView.showAlert(
-              "Something went wrong while getting this list of portals.",
-              "alert-error",
-              this.$(this.listContainer));
+            //Reset the isAuthorizedCreatePortal attribute
+            MetacatUI.appUserModel.set("isAuthorizedCreatePortal", null);
           }
+        } catch (e) {
+          console.error(e);
         }
+      },
 
-      });
-
-    });
+      /**
+       * Displays an error message when rendering this view has failed.
+       */
+      showError: function () {
+        //Remove the loading elements
+        this.$(this.listContainer).find(".loading").remove();
+
+        if (this.$(this.listContainer).children("tr").length == 0) {
+          //Show an error message
+          MetacatUI.appView.showAlert(
+            "Something went wrong while getting this list of portals.",
+            "alert-error",
+            this.$(this.listContainer),
+          );
+        }
+      },
+    },
+  );
+});
 
diff --git a/docs/docs/src_js_views_portals_PortalLogosView.js.html b/docs/docs/src_js_views_portals_PortalLogosView.js.html index b51bfdd83..8c7d2c92a 100644 --- a/docs/docs/src_js_views_portals_PortalLogosView.js.html +++ b/docs/docs/src_js_views_portals_PortalLogosView.js.html @@ -44,101 +44,101 @@

Source: src/js/views/portals/PortalLogosView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "text!templates/portals/portalLogo.html"],
-    function($, _, Backbone, PortalLogoTemplate){
-
-    /**
-     * @class PortalLogosView
-     * @classdesc The PortalLogosView is the area where the the logos of the organizations
-     * associated with each portal will be displayed.
-     * @classcategory Views/Portals
-     * @extends Backbone.View
-     * @screenshot views/portals/PortalLogosView.png
-     */
-    var PortalLogosView = Backbone.View.extend(
-      /** @lends PortalLogosView.prototype */{
-
-        /**
-         * The HTML element type for this view
-         * @type {string}
-         */
-        tagName: "div",
-        /**
-         * The HTML classes for this view
-         * @type {string}
-         */
-        className: "portal-logos-view",
-        /**
-         * The name of this View type
-         * @type {string}
-         */
-        type: "PortalLogos",
-
-        /**
-        * An array of PortalImages to display in this view
-        * @type {PortalImage[]}
-        */
-        logos: [],
-
-        /**
-        * Renders the compiled template into HTML
-        * @type {UnderscoreTemplate}
-        */
-        template: _.template(PortalLogoTemplate),
-
-        /**
-        * Renders the view
-        */
-        render: function() {
-            var spanX = "span";
-
-            // Determine the correct bootstrap fluid row span width to use
-            if (this.logos.length < 5) {
-                spanN = 12 / this.logos.length;
-                spanX = spanX + spanN;
-            } else {
-                // If there are more than 4 logos, use span3 and multiple
-                // rows.
-                spanX = "span3";
-            }
-
-            var row;
-
-            //Remove any logos that don't have a URL
-            var logos = _.reject(this.logos, function(logo){
-              return !logo || !logo.get("imageURL");
-            });
-
-            _.each(logos, function(logo, i) {
-
-                if (i % 4 == 0) {
-                    // create a row for each multiple of 4
-                    row = $(document.createElement("div")).addClass("logo-row row-fluid");
-                    this.$el.append(row);
-                }
-
-                var templateVars = logo.toJSON();
-                templateVars.spanX = spanX;
-
-                row.append(this.template(templateVars));
-
-            }, this);
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/portals/portalLogo.html",
+], function ($, _, Backbone, PortalLogoTemplate) {
+  /**
+   * @class PortalLogosView
+   * @classdesc The PortalLogosView is the area where the the logos of the organizations
+   * associated with each portal will be displayed.
+   * @classcategory Views/Portals
+   * @extends Backbone.View
+   * @screenshot views/portals/PortalLogosView.png
+   */
+  var PortalLogosView = Backbone.View.extend(
+    /** @lends PortalLogosView.prototype */ {
+      /**
+       * The HTML element type for this view
+       * @type {string}
+       */
+      tagName: "div",
+      /**
+       * The HTML classes for this view
+       * @type {string}
+       */
+      className: "portal-logos-view",
+      /**
+       * The name of this View type
+       * @type {string}
+       */
+      type: "PortalLogos",
+
+      /**
+       * An array of PortalImages to display in this view
+       * @type {PortalImage[]}
+       */
+      logos: [],
+
+      /**
+       * Renders the compiled template into HTML
+       * @type {UnderscoreTemplate}
+       */
+      template: _.template(PortalLogoTemplate),
+
+      /**
+       * Renders the view
+       */
+      render: function () {
+        var spanX = "span";
+
+        // Determine the correct bootstrap fluid row span width to use
+        if (this.logos.length < 5) {
+          spanN = 12 / this.logos.length;
+          spanX = spanX + spanN;
+        } else {
+          // If there are more than 4 logos, use span3 and multiple
+          // rows.
+          spanX = "span3";
+        }
 
-        },
+        var row;
+
+        //Remove any logos that don't have a URL
+        var logos = _.reject(this.logos, function (logo) {
+          return !logo || !logo.get("imageURL");
+        });
+
+        _.each(
+          logos,
+          function (logo, i) {
+            if (i % 4 == 0) {
+              // create a row for each multiple of 4
+              row = $(document.createElement("div")).addClass(
+                "logo-row row-fluid",
+              );
+              this.$el.append(row);
+            }
 
-        /**
-         * Close and destroy the view
-         */
-        onClose: function() {
+            var templateVars = logo.toJSON();
+            templateVars.spanX = spanX;
 
-        }
+            row.append(this.template(templateVars));
+          },
+          this,
+        );
+      },
 
-    });
+      /**
+       * Close and destroy the view
+       */
+      onClose: function () {},
+    },
+  );
 
-    return PortalLogosView;
+  return PortalLogosView;
 });
 
diff --git a/docs/docs/src_js_views_portals_PortalMembersView.js.html b/docs/docs/src_js_views_portals_PortalMembersView.js.html index 04f5b3646..6311e1cb4 100644 --- a/docs/docs/src_js_views_portals_PortalMembersView.js.html +++ b/docs/docs/src_js_views_portals_PortalMembersView.js.html @@ -44,42 +44,50 @@

Source: src/js/views/portals/PortalMembersView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "text!templates/metadata/EMLPartyDisplay.html",
-    "views/portals/PortalSectionView",
-    "views/portals/PortalLogosView",
-    "text!templates/portals/portalAcknowledgements.html",
-    "text!templates/portals/portalAwards.html"],
-    function($, _, Backbone, EMLPartyDisplayTemplate, PortalSectionView,
-        PortalLogosView, AcknowledgementsTemplate, AwardsTemplate){
-
-    /**
-    * @class PortalMembersView
-    * @classdesc The PortalMembersView is a view to render the
-     * portal members tab (within PortalSectionView)
-     * @classcategory Views/Portals
-     * @extends PortalSectionView
-     * @constructor
-     */
-     var PortalMembersView = PortalSectionView.extend(
-        /** @lends PortalMembersView.prototype */{
-        type: "PortalMembers",
-
-        /**
-        * The display name for this Section
-        * @type {string}
-        */
-        uniqueSectionLabel: "Members",
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/metadata/EMLPartyDisplay.html",
+  "views/portals/PortalSectionView",
+  "views/portals/PortalLogosView",
+  "text!templates/portals/portalAcknowledgements.html",
+  "text!templates/portals/portalAwards.html",
+], function (
+  $,
+  _,
+  Backbone,
+  EMLPartyDisplayTemplate,
+  PortalSectionView,
+  PortalLogosView,
+  AcknowledgementsTemplate,
+  AwardsTemplate,
+) {
+  /**
+   * @class PortalMembersView
+   * @classdesc The PortalMembersView is a view to render the
+   * portal members tab (within PortalSectionView)
+   * @classcategory Views/Portals
+   * @extends PortalSectionView
+   * @constructor
+   */
+  var PortalMembersView = PortalSectionView.extend(
+    /** @lends PortalMembersView.prototype */ {
+      type: "PortalMembers",
+
+      /**
+       * The display name for this Section
+       * @type {string}
+       */
+      uniqueSectionLabel: "Members",
 
       //   /* The list of subview instances contained in this view*/
       //   subviews: [], // Could be a literal object {}
 
       //   /* Renders the compiled template into HTML */
-        partyTemplate: _.template(EMLPartyDisplayTemplate),
-        acknowledgementsTemplate: _.template(AcknowledgementsTemplate),
-        awardsTemplate: _.template(AwardsTemplate),
+      partyTemplate: _.template(EMLPartyDisplayTemplate),
+      acknowledgementsTemplate: _.template(AcknowledgementsTemplate),
+      awardsTemplate: _.template(AwardsTemplate),
 
       //   /* The events that this view listens to*/
       //   events: {
@@ -92,94 +100,96 @@ 

Source: src/js/views/portals/PortalMembersView.js

// }, // /* Render the view */ - render: function() { - - if( this.id ){ - this.$el.attr("id", this.id); - } - - var parties = this.model.get("associatedParties"); - var thisview = this; - // Group parties into sets of 2 to do 2 per row - var row_groups = _.groupBy(parties, function(parties, index) { - return Math.floor(index / 2); + render: function () { + if (this.id) { + this.$el.attr("id", this.id); + } + + var parties = this.model.get("associatedParties"); + var thisview = this; + // Group parties into sets of 2 to do 2 per row + var row_groups = _.groupBy(parties, function (parties, index) { + return Math.floor(index / 2); + }); + + _.each(row_groups, function (row_group) { + // Create a new bootstrap row for each set of 2 parties + var newdiv = $('<div class="row-fluid"></div>'); + // Put the empty row into the portal members container + thisview.$el.append(newdiv); + // iterate for the 2 parties in this row + _.each(row_group, function (party) { + //Get the party info in JSON form + var partyInfo = party.toJSON(); + + // Create html links from the urls + var regex = /(.+)/gi; + var urlLink = []; + _.each(party.get("onlineUrl"), function (url) { + urlLink.push(url.replace(regex, '<a href="$&">$&</a>')); }); + partyInfo.urlLink = urlLink; + + //Set the ORCIDs as a blank string + partyInfo.orcids = ""; + + //Get the UserIds so we can display ORCIDs + if (Array.isArray(partyInfo.userId) && partyInfo.userId.length) { + //FInd the user ids that are ORCIDs + _.each(partyInfo.userId, function (userId) { + //If this user id is an ORCID, + if (party.isOrcid(userId)) { + //Display it with the icon and as a link + partyInfo.orcids += + '<img src="' + + MetacatUI.root + + '/img/orcid_64x64.png" ' + + 'class="icon orcid-logo icon-on-left" />' + + '<a href="' + + userId + + '" target="_blank">' + + userId + + "</a>"; + } + }); + } - _.each(row_groups, function(row_group){ - // Create a new bootstrap row for each set of 2 parties - var newdiv = $('<div class="row-fluid"></div>'); - // Put the empty row into the portal members container - thisview.$el.append(newdiv); - // iterate for the 2 parties in this row - _.each(row_group, function(party) { - - //Get the party info in JSON form - var partyInfo = party.toJSON(); - - // Create html links from the urls - var regex = /(.+)/gi; - var urlLink = []; - _.each(party.get("onlineUrl"), function(url){ - urlLink.push(url.replace(regex, '<a href="$&">$&</a>')); - }); - partyInfo.urlLink = urlLink; - - //Set the ORCIDs as a blank string - partyInfo.orcids = ""; - - //Get the UserIds so we can display ORCIDs - if( Array.isArray(partyInfo.userId) && partyInfo.userId.length ){ - - //FInd the user ids that are ORCIDs - _.each( partyInfo.userId, function(userId){ - //If this user id is an ORCID, - if( party.isOrcid(userId) ){ - //Display it with the icon and as a link - partyInfo.orcids += "<img src=\"" + MetacatUI.root + "/img/orcid_64x64.png\" " + - "class=\"icon orcid-logo icon-on-left\" />" + - "<a href=\"" + userId + "\" target=\"_blank\">" + userId + "</a>"; - } - }); - } - - // render party into its row - newdiv.append(thisview.partyTemplate(partyInfo)); - }); - }); - - var acknowledgements = this.model.get("acknowledgments") || ""; - var awards = this.model.get("awards") || ""; - - //Add a container element - if(acknowledgements || awards.length){ - var ack_div = $('<div class="well awards-info"></div>'); - this.$el.append(ack_div); - - //Add the awards - if( awards.length ) { - - ack_div.append(this.awardsTemplate({awards: awards})); - } + // render party into its row + newdiv.append(thisview.partyTemplate(partyInfo)); + }); + }); - //Add the acknowledgments - if( acknowledgements ) { + var acknowledgements = this.model.get("acknowledgments") || ""; + var awards = this.model.get("awards") || ""; - ack_div.append(this.acknowledgementsTemplate(acknowledgements.toJSON())); + //Add a container element + if (acknowledgements || awards.length) { + var ack_div = $('<div class="well awards-info"></div>'); + this.$el.append(ack_div); - } - } + //Add the awards + if (awards.length) { + ack_div.append(this.awardsTemplate({ awards: awards })); + } - this.$el.data("view", this); + //Add the acknowledgments + if (acknowledgements) { + ack_div.append( + this.acknowledgementsTemplate(acknowledgements.toJSON()), + ); + } + } - }, + this.$el.data("view", this); + }, // onClose: function() { // } + }, + ); - }); - - return PortalMembersView; + return PortalMembersView; });
diff --git a/docs/docs/src_js_views_portals_PortalMetricsView.js.html b/docs/docs/src_js_views_portals_PortalMetricsView.js.html index 43d8b35f4..ad58701d2 100644 --- a/docs/docs/src_js_views_portals_PortalMetricsView.js.html +++ b/docs/docs/src_js_views_portals_PortalMetricsView.js.html @@ -44,303 +44,324 @@

Source: src/js/views/portals/PortalMetricsView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "models/Search",
-    "models/MetricsModel",
-    "models/Stats",
-    "views/portals/PortalSectionView",
-    "views/StatsView",
-    "text!templates/loading.html"],
-    function($, _, Backbone, SearchModel, MetricsModel, StatsModel, PortalSectionView, StatsView,
-      LoadingTemplate){
-
-    /**
-     * @class PortalMetricsView
-     * @classdec The PortalMetricsView is a view to render the
-     * portal metrics tab (within PortalSectionView)
-     * @classcategory Views/Portals
-     * @extends PortalSectionView
-     * @constructor
-     */
-     var PortalMetricsView = PortalSectionView.extend(
-       /** @lends PortalMetricsView.prototype */{
-        type: "PortalMetrics",
-
-        /**
-        * A unique name for this Section
-        * @type {string}
-        */
-        uniqueSectionLabel: "Metrics",
-
-        /**
-        * The display name for this Section
-        * @type {string}
-        */
-        sectionName: "Metrics",
-
-        /**
-        * The Portal Model this Metrics section is part of
-        * @type {Portal}
-        */
-        model: undefined,
-
-        /**
-        * Aggregated Quality Metrics flag
-        * @type {boolean}
-        */
-        hideMetadataAssessment: MetacatUI.appModel.get("hideSummaryMetadataAssessment"),
-
-
-        /**
-        * Aggregated Citation Metrics flag
-        * @type {boolean}
-        */
-        hideCitationsChart: MetacatUI.appModel.get("hideSummaryCitationsChart"),
-
-
-        /**
-        * Aggregated Download Metrics flag
-        * @type {boolean}
-        */
-        hideDownloadsChart: MetacatUI.appModel.get("hideSummaryDownloadsChart"),
-
-
-        /**
-        * Aggregated View Metrics flag
-        * @type {boolean}
-        */
-        hideViewsChart: MetacatUI.appModel.get("hideSummaryViewsChart"),
-
-        /**
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/Search",
+  "models/MetricsModel",
+  "models/Stats",
+  "views/portals/PortalSectionView",
+  "views/StatsView",
+  "text!templates/loading.html",
+], function (
+  $,
+  _,
+  Backbone,
+  SearchModel,
+  MetricsModel,
+  StatsModel,
+  PortalSectionView,
+  StatsView,
+  LoadingTemplate,
+) {
+  /**
+   * @class PortalMetricsView
+   * @classdec The PortalMetricsView is a view to render the
+   * portal metrics tab (within PortalSectionView)
+   * @classcategory Views/Portals
+   * @extends PortalSectionView
+   * @constructor
+   */
+  var PortalMetricsView = PortalSectionView.extend(
+    /** @lends PortalMetricsView.prototype */ {
+      type: "PortalMetrics",
+
+      /**
+       * A unique name for this Section
+       * @type {string}
+       */
+      uniqueSectionLabel: "Metrics",
+
+      /**
+       * The display name for this Section
+       * @type {string}
+       */
+      sectionName: "Metrics",
+
+      /**
+       * The Portal Model this Metrics section is part of
+       * @type {Portal}
+       */
+      model: undefined,
+
+      /**
+       * Aggregated Quality Metrics flag
+       * @type {boolean}
+       */
+      hideMetadataAssessment: MetacatUI.appModel.get(
+        "hideSummaryMetadataAssessment",
+      ),
+
+      /**
+       * Aggregated Citation Metrics flag
+       * @type {boolean}
+       */
+      hideCitationsChart: MetacatUI.appModel.get("hideSummaryCitationsChart"),
+
+      /**
+       * Aggregated Download Metrics flag
+       * @type {boolean}
+       */
+      hideDownloadsChart: MetacatUI.appModel.get("hideSummaryDownloadsChart"),
+
+      /**
+       * Aggregated View Metrics flag
+       * @type {boolean}
+       */
+      hideViewsChart: MetacatUI.appModel.get("hideSummaryViewsChart"),
+
+      /**
         A template for displaying a loading message
         * @type {Underscore.Template}
         */
-        loadingTemplate: _.template(LoadingTemplate),
-
-        /* Render the view */
-        render: function() {
-
-            if( this.model && this.model.get("metricsLabel") ){
-              this.uniqueSectionLabel = this.model.get("metricsLabel");
-              this.sectionName = this.model.get("metricsLabel");
-            }
-
-            this.$el.data("view", this);
+      loadingTemplate: _.template(LoadingTemplate),
 
-            //Add a loading message to the metrics tab since it can take a while for the metrics query to be sent
-            this.$el.html(this.loadingTemplate({
-              msg: "Getting " + this.model.get("metricsLabel").toLowerCase() + "..."
-            }));
-
-        },
-
-        /**
-         * Render the metrics inside this view
-         */
-        renderMetrics: function() {
-
-          try{
-
-            if( this.model.get("hideMetrics") == true ) {
-              return;
-            }
-
-            // If the search results haven't been fetched yet, wait.
-            if( !MetacatUI.appModel.get("enableSolrJoins") && !this.model.get("searchResults").header ){
-              this.listenToOnce( this.model.get("searchResults"), "sync", this.renderMetrics );
-              return;
-            }
+      /* Render the view */
+      render: function () {
+        if (this.model && this.model.get("metricsLabel")) {
+          this.uniqueSectionLabel = this.model.get("metricsLabel");
+          this.sectionName = this.model.get("metricsLabel");
+        }
 
-            //Create a Stats Model for retrieving and storing all of the statistics
-            var statsModel = new StatsModel();
+        this.$el.data("view", this);
+
+        //Add a loading message to the metrics tab since it can take a while for the metrics query to be sent
+        this.$el.html(
+          this.loadingTemplate({
+            msg:
+              "Getting " + this.model.get("metricsLabel").toLowerCase() + "...",
+          }),
+        );
+      },
+
+      /**
+       * Render the metrics inside this view
+       */
+      renderMetrics: function () {
+        try {
+          if (this.model.get("hideMetrics") == true) {
+            return;
+          }
 
-            //If Solr Joins are enabled, set the query on the StatsModel using the Portal Filters
-            if( MetacatUI.appModel.get("enableSolrJoins") && this.model.get("definitionFilters") ){
+          // If the search results haven't been fetched yet, wait.
+          if (
+            !MetacatUI.appModel.get("enableSolrJoins") &&
+            !this.model.get("searchResults").header
+          ) {
+            this.listenToOnce(
+              this.model.get("searchResults"),
+              "sync",
+              this.renderMetrics,
+            );
+            return;
+          }
 
-              statsModel.set("query", this.model.getQuery());
+          //Create a Stats Model for retrieving and storing all of the statistics
+          var statsModel = new StatsModel();
 
-            }
-            //Otherwise, construct a query using a Search model and all of the ID facet counts
-            else{
-
-              // Get all the facet counts from the search results collection
-              var facetCounts = this.model.get("allSearchResults").facetCounts,
-                  //Get the id facet counts
-                  idFacets = facetCounts? facetCounts.id : [],
-                  //Get the documents facet counts
-                  documentsFacets = facetCounts? facetCounts.documents : [],
-                  //Start an array to hold all the ids
-                  allIDs = [];
-
-              //If there are resource map facet counts, get all the ids
-              if( idFacets && idFacets.length ){
-
-                //Merge the id and documents arrays
-                var allFacets = idFacets.concat(documentsFacets);
-
-                //Get all the ids, which should be every other element in the
-                // facets array
-                for( var i=0; i < allFacets.length; i+=2 ){
-                  allIDs.push( allFacets[i] );
-                }
+          //If Solr Joins are enabled, set the query on the StatsModel using the Portal Filters
+          if (
+            MetacatUI.appModel.get("enableSolrJoins") &&
+            this.model.get("definitionFilters")
+          ) {
+            statsModel.set("query", this.model.getQuery());
+          }
+          //Otherwise, construct a query using a Search model and all of the ID facet counts
+          else {
+            // Get all the facet counts from the search results collection
+            var facetCounts = this.model.get("allSearchResults").facetCounts,
+              //Get the id facet counts
+              idFacets = facetCounts ? facetCounts.id : [],
+              //Get the documents facet counts
+              documentsFacets = facetCounts ? facetCounts.documents : [],
+              //Start an array to hold all the ids
+              allIDs = [];
+
+            //If there are resource map facet counts, get all the ids
+            if (idFacets && idFacets.length) {
+              //Merge the id and documents arrays
+              var allFacets = idFacets.concat(documentsFacets);
+
+              //Get all the ids, which should be every other element in the
+              // facets array
+              for (var i = 0; i < allFacets.length; i += 2) {
+                allIDs.push(allFacets[i]);
               }
-
-              // Create a search model that filters by all the data object Ids
-              var statsSearchModel = new SearchModel({
-                idOnly: allIDs,
-                formatType: [],
-                exclude: []
-              });
-
-              //Sett the query using the query constructing by the Search Model
-              statsModel.set("query", statsSearchModel.getQuery());
-              //Save a reference to the Search Model on the Stats model
-              statsModel.set("searchModel", statsSearchModel);
             }
 
-            var userType = "portal";
-
-            var label_list = [];
-            label_list.push(this.model.get("label"));
+            // Create a search model that filters by all the data object Ids
+            var statsSearchModel = new SearchModel({
+              idOnly: allIDs,
+              formatType: [],
+              exclude: [],
+            });
 
-            var metricsModel = new MetricsModel();
-            this.metricsModel = metricsModel;
+            //Sett the query using the query constructing by the Search Model
+            statsModel.set("query", statsSearchModel.getQuery());
+            //Save a reference to the Search Model on the Stats model
+            statsModel.set("searchModel", statsSearchModel);
+          }
 
-            if (this.nodeView) {
+          var userType = "portal";
 
-              userType = "repository";
+          var label_list = [];
+          label_list.push(this.model.get("label"));
 
-              // TODO: replace the following logic with dataone bookkeeper service
-              // check if the repository is a dataone member
-              var dataoneHostedRepos = MetacatUI.appModel.get("dataoneHostedRepos");
+          var metricsModel = new MetricsModel();
+          this.metricsModel = metricsModel;
 
-              if ((typeof dataoneHostedRepos !== 'undefined') && Array.isArray(dataoneHostedRepos) &&
-                  dataoneHostedRepos.includes(this.model.get("seriesId"))){
+          if (this.nodeView) {
+            userType = "repository";
 
-                if( MetacatUI.appModel.get("hideSummaryMetadataAssessment") !== true )
-                  this.hideMetadataAssessment = false;
+            // TODO: replace the following logic with dataone bookkeeper service
+            // check if the repository is a dataone member
+            var dataoneHostedRepos =
+              MetacatUI.appModel.get("dataoneHostedRepos");
 
-                if( MetacatUI.appModel.get("hideSummaryCitationsChart") !== true )
-                  this.hideCitationsChart = false;
+            if (
+              typeof dataoneHostedRepos !== "undefined" &&
+              Array.isArray(dataoneHostedRepos) &&
+              dataoneHostedRepos.includes(this.model.get("seriesId"))
+            ) {
+              if (
+                MetacatUI.appModel.get("hideSummaryMetadataAssessment") !== true
+              )
+                this.hideMetadataAssessment = false;
 
-                if( MetacatUI.appModel.get("hideSummaryDownloadsChart") !== true )
-                  this.hideDownloadsChart = false;
+              if (MetacatUI.appModel.get("hideSummaryCitationsChart") !== true)
+                this.hideCitationsChart = false;
 
-                if( MetacatUI.appModel.get("hideSummaryViewsChart") !== true )
-                  this.hideViewsChart = false;
-              }
-              //Hide all of the metrics charts
-              else{
-                this.hideMetadataAssessment = true;
-                this.hideCitationsChart = true;
-                this.hideDownloadsChart = true;
-                this.hideViewsChart     = true;
-              }
+              if (MetacatUI.appModel.get("hideSummaryDownloadsChart") !== true)
+                this.hideDownloadsChart = false;
 
-              // set the statsModel
-              statsModel = MetacatUI.statsModel;
-
-              if (!this.hideCitationsChart || !this.hideDownloadsChart || !this.hideViewsChart) {
-                // create a metrics query for repository object
-                var pid_list = new Array();
-                pid_list.push(this.model.get("seriesId"));
-                this.metricsModel.set("pid_list", pid_list);
-                this.metricsModel.set("filterType", "repository");
-              }
-              else{
-                this.metricsModel.set("pid_list", []);
-                this.metricsModel.set("filterType", "");
-              }
+              if (MetacatUI.appModel.get("hideSummaryViewsChart") !== true)
+                this.hideViewsChart = false;
             }
+            //Hide all of the metrics charts
             else {
-              // create a metrics query for portal object
-              this.metricsModel.set("pid_list", label_list);
-              this.metricsModel.set("filterType", "portal");
-
-              // creating additional filters for portal Metrics
-              var portalQueryFilter = {};
-              var portalCollectionQuery = statsModel.get("query");
-              portalQueryFilter["filterType"] = "query";
-              portalQueryFilter["values"] = [portalCollectionQuery];
-              portalQueryFilter["interpretAs"] = "list";
-              this.metricsModel.set("filterQueryObject", portalQueryFilter);
+              this.hideMetadataAssessment = true;
+              this.hideCitationsChart = true;
+              this.hideDownloadsChart = true;
+              this.hideViewsChart = true;
             }
 
-            this.metricsModel.fetch();
-
-            // Add a stats view
-            this.statsView = new StatsView({
-                title: null,
-                description: null,
-                metricsModel: this.metricsModel,
-                el: document.createElement("div"),
-                model: statsModel,
-                userType: userType,
-                userId: this.model.get("seriesId"),
-                userLabel: this.model.get("label"),
-                hideMetadataAssessment: this.hideMetadataAssessment,
-                // Rendering metrics on the portal
-                hideCitationsChart: this.hideCitationsChart,
-                hideDownloadsChart: this.hideDownloadsChart,
-                hideViewsChart: this.hideViewsChart,
-            });
-
-            //Insert the StatsView into this view
-            this.$el.html(this.statsView.el);
-
-            //Render the StatsView
-            this.statsView.render();
-
-          }
-          catch(e){
-            this.handlePortalMetricsError(e);
-          }
-
-        },
-
-        /**
-         * Handles error display if something went wrong while displaying metrics
-        */
-       handlePortalMetricsError: function(error, errorDisplayMessage){
-
-          if(!errorDisplayMessage) {
-            var errorDisplayMessage = "<p>Sorry, we couldn't retrieve metrics for the \"" + (this.model.get("label") || this.model.get("portalId")) +
-                "\" portal at this time.</p>"
+            // set the statsModel
+            statsModel = MetacatUI.statsModel;
+
+            if (
+              !this.hideCitationsChart ||
+              !this.hideDownloadsChart ||
+              !this.hideViewsChart
+            ) {
+              // create a metrics query for repository object
+              var pid_list = new Array();
+              pid_list.push(this.model.get("seriesId"));
+              this.metricsModel.set("pid_list", pid_list);
+              this.metricsModel.set("filterType", "repository");
+            } else {
+              this.metricsModel.set("pid_list", []);
+              this.metricsModel.set("filterType", "");
+            }
+          } else {
+            // create a metrics query for portal object
+            this.metricsModel.set("pid_list", label_list);
+            this.metricsModel.set("filterType", "portal");
+
+            // creating additional filters for portal Metrics
+            var portalQueryFilter = {};
+            var portalCollectionQuery = statsModel.get("query");
+            portalQueryFilter["filterType"] = "query";
+            portalQueryFilter["values"] = [portalCollectionQuery];
+            portalQueryFilter["interpretAs"] = "list";
+            this.metricsModel.set("filterQueryObject", portalQueryFilter);
           }
 
-          // Track the error
-          const model = this.model;
-          const portalID = model.get("portalId") || model.get("label");
-          MetacatUI.analytics?.trackException(
-            "Failed to render the Metrics view for a portal", portalID, false
-          );
-
-          //Show a warning message about the metrics error
-          MetacatUI.appView.showAlert(
-            errorDisplayMessage,
-            "alert-warning",
-            this.$el
-          );
-          this.$(".loading").remove();
-
-          console.log("Failed to render the metrics view. Error message: " + error);
-       },
-
-        /**
-         * Functionality to execute after the view has been created and rendered initially
-         */
-        postRender: function(){
-          //If there is no StatsView rendered yet, then render it
-          if( !this.statsView ){
-            this.renderMetrics();
-          }
+          this.metricsModel.fetch();
+
+          // Add a stats view
+          this.statsView = new StatsView({
+            title: null,
+            description: null,
+            metricsModel: this.metricsModel,
+            el: document.createElement("div"),
+            model: statsModel,
+            userType: userType,
+            userId: this.model.get("seriesId"),
+            userLabel: this.model.get("label"),
+            hideMetadataAssessment: this.hideMetadataAssessment,
+            // Rendering metrics on the portal
+            hideCitationsChart: this.hideCitationsChart,
+            hideDownloadsChart: this.hideDownloadsChart,
+            hideViewsChart: this.hideViewsChart,
+          });
+
+          //Insert the StatsView into this view
+          this.$el.html(this.statsView.el);
+
+          //Render the StatsView
+          this.statsView.render();
+        } catch (e) {
+          this.handlePortalMetricsError(e);
+        }
+      },
+
+      /**
+       * Handles error display if something went wrong while displaying metrics
+       */
+      handlePortalMetricsError: function (error, errorDisplayMessage) {
+        if (!errorDisplayMessage) {
+          var errorDisplayMessage =
+            "<p>Sorry, we couldn't retrieve metrics for the \"" +
+            (this.model.get("label") || this.model.get("portalId")) +
+            '" portal at this time.</p>';
         }
 
-     });
+        // Track the error
+        const model = this.model;
+        const portalID = model.get("portalId") || model.get("label");
+        MetacatUI.analytics?.trackException(
+          "Failed to render the Metrics view for a portal",
+          portalID,
+          false,
+        );
+
+        //Show a warning message about the metrics error
+        MetacatUI.appView.showAlert(
+          errorDisplayMessage,
+          "alert-warning",
+          this.$el,
+        );
+        this.$(".loading").remove();
+
+        console.log(
+          "Failed to render the metrics view. Error message: " + error,
+        );
+      },
+
+      /**
+       * Functionality to execute after the view has been created and rendered initially
+       */
+      postRender: function () {
+        //If there is no StatsView rendered yet, then render it
+        if (!this.statsView) {
+          this.renderMetrics();
+        }
+      },
+    },
+  );
 
-     return PortalMetricsView;
+  return PortalMetricsView;
 });
 
diff --git a/docs/docs/src_js_views_portals_PortalSectionView.js.html b/docs/docs/src_js_views_portals_PortalSectionView.js.html index 9f240f2c9..8b144f6fc 100644 --- a/docs/docs/src_js_views_portals_PortalSectionView.js.html +++ b/docs/docs/src_js_views_portals_PortalSectionView.js.html @@ -44,212 +44,207 @@

Source: src/js/views/portals/PortalSectionView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    'models/portals/PortalSectionModel',
-    "views/MarkdownView",
-    "text!templates/portals/portalSection.html"],
-    function($, _, Backbone, PortalSectionModel, MarkdownView, Template){
-
-    /**
-     * @class PortalSectionView
-     * @classdesc The PortalSectionView is a generic view to render
-     * portal sections, with a default rendering of a
-     * MarkdownView
-     * @classcategory Views/Portals
-     * @module views/PortalSectionView
-     * @name PortalSectionView
-     * @extends Backbone.View
-     * @constructor
-     */
-     var PortalSectionView = Backbone.View.extend(
-       /** @lends PortalSectionView.prototype */{
-
-       /**
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "models/portals/PortalSectionModel",
+  "views/MarkdownView",
+  "text!templates/portals/portalSection.html",
+], function ($, _, Backbone, PortalSectionModel, MarkdownView, Template) {
+  /**
+   * @class PortalSectionView
+   * @classdesc The PortalSectionView is a generic view to render
+   * portal sections, with a default rendering of a
+   * MarkdownView
+   * @classcategory Views/Portals
+   * @module views/PortalSectionView
+   * @name PortalSectionView
+   * @extends Backbone.View
+   * @constructor
+   */
+  var PortalSectionView = Backbone.View.extend(
+    /** @lends PortalSectionView.prototype */ {
+      /**
        * The type of View this is
        * @type {string}
        * @readonly
        */
-        type: "PortalSection",
+      type: "PortalSection",
 
-        /**
-        * The display name for this Section
-        * @type {string}
-        */
-        sectionName: "",
-
-        /**
-        * The unique label for this Section. It most likely matches the label on the model, but
-        * may include a number after if more than one section has the same name.
-        * @type {string}
-        */
-        uniqueSectionLabel: "",
+      /**
+       * The display name for this Section
+       * @type {string}
+       */
+      sectionName: "",
 
-        /**
-        * The HTML tag name for this view's element
-        * @type {string}
-        */
-        tagName: "div",
+      /**
+       * The unique label for this Section. It most likely matches the label on the model, but
+       * may include a number after if more than one section has the same name.
+       * @type {string}
+       */
+      uniqueSectionLabel: "",
 
-        /**
-        * The HTML classes to use for this view's element
-        * @type {string}
-        */
-        className: "tab-pane portal-section-view",
+      /**
+       * The HTML tag name for this view's element
+       * @type {string}
+       */
+      tagName: "div",
 
-        /**
-        * Specifies if this section is active or not
-        * @type {boolean}
-        */
-        active: false,
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "tab-pane portal-section-view",
 
-        /**
-        * The PortalSectionModel that is being edited
-        * @type {PortalSection}
-        */
-        model: undefined,
+      /**
+       * Specifies if this section is active or not
+       * @type {boolean}
+       */
+      active: false,
 
-        /**
-        * @type {UnderscoreTemplate}
-        */
-        template: _.template(Template),
+      /**
+       * The PortalSectionModel that is being edited
+       * @type {PortalSection}
+       */
+      model: undefined,
 
-        /**
-        * @param {Object} options - A literal object with options to pass to the view
-        * @property {PortalSection} options.model - The PortalSection rendered in this view
-        * @property {string} options.sectionName - The name of the portal section
-        */
-        initialize: function(options){
-
-          // Get all the options and apply them to this view
-          if( typeof options == "object" ) {
-              var optionKeys = Object.keys(options);
-              _.each(optionKeys, function(key, i) {
-                  this[key] = options[key];
-              }, this);
-          }
+      /**
+       * @type {UnderscoreTemplate}
+       */
+      template: _.template(Template),
 
-        },
+      /**
+       * @param {Object} options - A literal object with options to pass to the view
+       * @property {PortalSection} options.model - The PortalSection rendered in this view
+       * @property {string} options.sectionName - The name of the portal section
+       */
+      initialize: function (options) {
+        // Get all the options and apply them to this view
+        if (typeof options == "object") {
+          var optionKeys = Object.keys(options);
+          _.each(
+            optionKeys,
+            function (key, i) {
+              this[key] = options[key];
+            },
+            this,
+          );
+        }
+      },
 
-        /**
+      /**
          Renders the view
         */
-        render: function() {
-
-          //Add the active class to the element
-          if( this.active ){
-            this.$el.addClass("active");
-          }
+      render: function () {
+        //Add the active class to the element
+        if (this.active) {
+          this.$el.addClass("active");
+        }
 
-          //Add an id to this element so links work
-          this.$el.attr("id", this.getName({ linkFriendly: true }));
+        //Add an id to this element so links work
+        this.$el.attr("id", this.getName({ linkFriendly: true }));
 
-          this.$el.html(this.template({
-            imageURL: this.model.get("image")? this.model.get("image").get("imageURL") : "",
+        this.$el.html(
+          this.template({
+            imageURL: this.model.get("image")
+              ? this.model.get("image").get("imageURL")
+              : "",
             title: this.model.get("title"),
-            introduction: this.model.get("introduction")
-          }));
-
-          this.$el.data("view", this);
-
-          //If there is Markdown, render it
-          if( this.model.get("content").get("markdown") ){
-
-            //Create a MarkdownView
-            this.markdownView = new MarkdownView({
-              markdown: this.model.get("content").get("markdown"),
-              citations: this.model.get("literatureCited"),
-              showTOC: true
-            });
-
-            // Listen to the markdown view and when it is rendered, format the rendered markdown
-            this.listenTo(this.markdownView, "mdRendered", this.postMarkdownRender);
-
-            // Render the view
-            this.markdownView.render();
-
-            // Add the markdown view element to this view
-            this.$(".portal-section-content").html(this.markdownView.el);
-
-          }
-
-        },
-
-
-
-        /**
-        * This funciton is called after this view has fully rendered and is
-        * visible on the webpage
-        */
-        postRender: function(){
-
-          _.each(this.subviews, function(subview){
-              if(subview.postRender){
-                subview.postRender();
-              }
+            introduction: this.model.get("introduction"),
+          }),
+        );
+
+        this.$el.data("view", this);
+
+        //If there is Markdown, render it
+        if (this.model.get("content").get("markdown")) {
+          //Create a MarkdownView
+          this.markdownView = new MarkdownView({
+            markdown: this.model.get("content").get("markdown"),
+            citations: this.model.get("literatureCited"),
+            showTOC: true,
           });
 
-          document.body.style.removeProperty("height");
-
-          this.markdownView?.tocView?.setAffix();
-        },
-
-        /**
-        * When the portal section markdown is rendered in a MarkdownView, format the
-        * resulting HTML as needed for this view
-        */
-        postMarkdownRender: function(){
-
+          // Listen to the markdown view and when it is rendered, format the rendered markdown
+          this.listenTo(
+            this.markdownView,
+            "mdRendered",
+            this.postMarkdownRender,
+          );
 
+          // Render the view
+          this.markdownView.render();
 
+          // Add the markdown view element to this view
+          this.$(".portal-section-content").html(this.markdownView.el);
+        }
+      },
 
-          // If the window location has a hash, scroll to it
-          if( window.location.hash && this.$(window.location.hash).length ){
-            var view = this;
-            // Wait 0.5 seconds to allow images time to load before we scroll down the page
-            setTimeout(function(){
-              // Scroll to the element specified in the hash
-              MetacatUI.appView.scrollTo( view.$(window.location.hash) );
-            }, 500);
+      /**
+       * This funciton is called after this view has fully rendered and is
+       * visible on the webpage
+       */
+      postRender: function () {
+        _.each(this.subviews, function (subview) {
+          if (subview.postRender) {
+            subview.postRender();
           }
+        });
 
-        },
-
-        /**
-        * Gets the name of this section and returns it
-        * @param {Object} [options] - Optional options for the name that is returned
-        * @property {Boolean} options.linkFriendly - If true, the name will be stripped of special characters
-        * @return {string} The name for this section
-        */
-        getName: function(options){
+        document.body.style.removeProperty("height");
 
-          var name = "";
+        this.markdownView?.tocView?.setAffix();
+      },
 
-          //If a section name is set on the view, use that
-          if( this.sectionName ){
-            name = this.sectionName;
-          }
-          //If the model is a PortalSectionModel, use the label from the model
-          else if( PortalSectionModel.prototype.isPrototypeOf(this.model) ){
-            name = this.model.get("label");
-          }
-          else{
-            name = "Untitled";
-          }
+      /**
+       * When the portal section markdown is rendered in a MarkdownView, format the
+       * resulting HTML as needed for this view
+       */
+      postMarkdownRender: function () {
+        // If the window location has a hash, scroll to it
+        if (window.location.hash && this.$(window.location.hash).length) {
+          var view = this;
+          // Wait 0.5 seconds to allow images time to load before we scroll down the page
+          setTimeout(function () {
+            // Scroll to the element specified in the hash
+            MetacatUI.appView.scrollTo(view.$(window.location.hash));
+          }, 500);
+        }
+      },
 
-          if( typeof options == "object" ){
-            if( options.linkFriendly ){
-              name = name.replace(/[^a-zA-Z0-9 ]/g, "").replace(/ /g, "-");
-            }
-          }
+      /**
+       * Gets the name of this section and returns it
+       * @param {Object} [options] - Optional options for the name that is returned
+       * @property {Boolean} options.linkFriendly - If true, the name will be stripped of special characters
+       * @return {string} The name for this section
+       */
+      getName: function (options) {
+        var name = "";
 
-          return name;
+        //If a section name is set on the view, use that
+        if (this.sectionName) {
+          name = this.sectionName;
+        }
+        //If the model is a PortalSectionModel, use the label from the model
+        else if (PortalSectionModel.prototype.isPrototypeOf(this.model)) {
+          name = this.model.get("label");
+        } else {
+          name = "Untitled";
+        }
 
+        if (typeof options == "object") {
+          if (options.linkFriendly) {
+            name = name.replace(/[^a-zA-Z0-9 ]/g, "").replace(/ /g, "-");
+          }
         }
-     });
 
-     return PortalSectionView;
+        return name;
+      },
+    },
+  );
+
+  return PortalSectionView;
 });
 
diff --git a/docs/docs/src_js_views_portals_PortalUsagesView.js.html b/docs/docs/src_js_views_portals_PortalUsagesView.js.html index d520f675a..98f640b1a 100644 --- a/docs/docs/src_js_views_portals_PortalUsagesView.js.html +++ b/docs/docs/src_js_views_portals_PortalUsagesView.js.html @@ -44,65 +44,69 @@

Source: src/js/views/portals/PortalUsagesView.js

-
define(["jquery",
+            
define([
+  "jquery",
   "underscore",
   "backbone",
   "collections/SolrResults",
   "collections/Filters",
   "collections/bookkeeper/Usages",
   "views/portals/PortalListView",
-  "text!templates/portals/portalList.html"],
-  function($, _, Backbone, SearchResults, Filters, Usages, PortalListView, Template){
-
-    /**
-    * @class PortalUsagesView
-    * @classdesc A view that shows a list of Portal Usages
-    * @classcategory Views/Portals
-    * @extends PortalListView
-    * @since 2.14.0
-    * @constructor
-    */
-    return PortalListView.extend(
-      /** @lends PortalUsagesView.prototype */{
-
+  "text!templates/portals/portalList.html",
+], function (
+  $,
+  _,
+  Backbone,
+  SearchResults,
+  Filters,
+  Usages,
+  PortalListView,
+  Template,
+) {
+  /**
+   * @class PortalUsagesView
+   * @classdesc A view that shows a list of Portal Usages
+   * @classcategory Views/Portals
+   * @extends PortalListView
+   * @since 2.14.0
+   * @constructor
+   */
+  return PortalListView.extend(
+    /** @lends PortalUsagesView.prototype */ {
       /**
-      * A reference to the Usages collection that is rendered in this view
-      * @type {Usages}
-      */
+       * A reference to the Usages collection that is rendered in this view
+       * @type {Usages}
+       */
       usagesCollection: null,
 
       /**
-      * Renders this view
-      */
-      render: function(){
-
-        try{
-
+       * Renders this view
+       */
+      render: function () {
+        try {
           //If the "my portals" feature is disabled, exit now
-          if(MetacatUI.appModel.get("showMyPortals") === false){
+          if (MetacatUI.appModel.get("showMyPortals") === false) {
             return;
           }
 
           //Insert the template
           this.$el.html(this.template());
 
-          if( !this.usagesCollection ){
+          if (!this.usagesCollection) {
             this.usagesCollection = new Usages();
           }
 
           //When in DataONE Plus Preview mode, search for portals in Solr first,
           // then create Usage models for each portal in Solr.
-          if( MetacatUI.appModel.get("dataonePlusPreviewMode") ){
-
-            this.listenTo(this.searchResults, "sync", function(){
-
+          if (MetacatUI.appModel.get("dataonePlusPreviewMode")) {
+            this.listenTo(this.searchResults, "sync", function () {
               //Create a Usage for each portal found in Solr
-              this.searchResults.each(function(searchResult){
+              this.searchResults.each(function (searchResult) {
                 this.usagesCollection.add({
                   instanceId: searchResult.get("seriesId"),
                   status: "active",
                   quantity: 1,
-                  nodeId: searchResult.get("datasource")
+                  nodeId: searchResult.get("datasource"),
                 });
               }, this);
 
@@ -113,94 +117,95 @@ 

Source: src/js/views/portals/PortalUsagesView.js

this.showUsageInfo(); }); - if( MetacatUI.appModel.get("dataonePlusPreviewPortals").length ){ - + if (MetacatUI.appModel.get("dataonePlusPreviewPortals").length) { this.altReposChecked = 0; this.altReposToCheck = []; this.additionalPortalsToDisplay = []; - _.each( MetacatUI.appModel.get("alternateRepositories"), function(altRepo){ - - var portalsInThisRepo = _.where(MetacatUI.appModel.get("dataonePlusPreviewPortals"), - { datasource: altRepo.identifier }); - - if( portalsInThisRepo.length ){ - - var searchResults = new SearchResults(); - this.listenToOnce(searchResults, "reset", function(){ - - if( searchResults.length ){ - this.additionalPortalsToDisplay = this.additionalPortalsToDisplay.concat( searchResults.models ); - } - - if(typeof this.altReposChecked == "number" ){ - this.altReposChecked++; - if( this.altReposChecked == this.altReposToCheck ){ - //Call the PortalListView render function - PortalListView.prototype.render.call(this); + _.each( + MetacatUI.appModel.get("alternateRepositories"), + function (altRepo) { + var portalsInThisRepo = _.where( + MetacatUI.appModel.get("dataonePlusPreviewPortals"), + { datasource: altRepo.identifier }, + ); + + if (portalsInThisRepo.length) { + var searchResults = new SearchResults(); + this.listenToOnce(searchResults, "reset", function () { + if (searchResults.length) { + this.additionalPortalsToDisplay = + this.additionalPortalsToDisplay.concat( + searchResults.models, + ); } - } - - //Create a Usage for each portal found in Solr - searchResults.each(function(searchResult){ - this.usagesCollection.add({ - instanceId: searchResult.get("seriesId"), - status: "active", - quantity: 1, - nodeId: searchResult.get("datasource") - }); - }, this); - - //Merge the Usages and Search Results - this.mergeSearchResults(searchResults); - - //Update the view with info about the corresponding Usage model - this.showUsageInfo(); - - }); - - //Create a Filters() collection - var portalFilters = new Filters(); - portalFilters.mustMatchIds = true; - portalFilters.addWritePermissionFilter(); - portalFilters.add({ - fields: ["obsoletedBy"], - values: ["*"], - matchSubstring: false, - exclude: true - }); - portalFilters.add({ - fields: ["datasource"], - values: [altRepo.identifier], - matchSubstring: false, - exclude: false - }); - var portalIds = _.pluck(portalsInThisRepo, "seriesId"); - portalFilters.add({ - fields: ["seriesId"], - values: portalIds, - operator: "OR", - matchSubstring: false - }); - - searchResults.rows = portalIds.length; - searchResults.fields = this.searchFields; - - searchResults.queryServiceUrl = altRepo.queryServiceUrl; - - searchResults.setQuery( portalFilters.getQuery() ); - - this.altReposToCheck++; - - //Get the first page of results - searchResults.toPage(0); - } - }, this); + if (typeof this.altReposChecked == "number") { + this.altReposChecked++; + if (this.altReposChecked == this.altReposToCheck) { + //Call the PortalListView render function + PortalListView.prototype.render.call(this); + } + } + //Create a Usage for each portal found in Solr + searchResults.each(function (searchResult) { + this.usagesCollection.add({ + instanceId: searchResult.get("seriesId"), + status: "active", + quantity: 1, + nodeId: searchResult.get("datasource"), + }); + }, this); + + //Merge the Usages and Search Results + this.mergeSearchResults(searchResults); + + //Update the view with info about the corresponding Usage model + this.showUsageInfo(); + }); + + //Create a Filters() collection + var portalFilters = new Filters(); + portalFilters.mustMatchIds = true; + portalFilters.addWritePermissionFilter(); + portalFilters.add({ + fields: ["obsoletedBy"], + values: ["*"], + matchSubstring: false, + exclude: true, + }); + portalFilters.add({ + fields: ["datasource"], + values: [altRepo.identifier], + matchSubstring: false, + exclude: false, + }); + var portalIds = _.pluck(portalsInThisRepo, "seriesId"); + portalFilters.add({ + fields: ["seriesId"], + values: portalIds, + operator: "OR", + matchSubstring: false, + }); + + searchResults.rows = portalIds.length; + searchResults.fields = this.searchFields; + + searchResults.queryServiceUrl = altRepo.queryServiceUrl; + + searchResults.setQuery(portalFilters.getQuery()); + + this.altReposToCheck++; + + //Get the first page of results + searchResults.toPage(0); + } + }, + this, + ); return; - } //Call the PortalListView render function @@ -211,13 +216,17 @@

Source: src/js/views/portals/PortalUsagesView.js

} //When the collection has been fetched, redner the Usage list - this.listenToOnce(this.usagesCollection, "sync", this.getSearchResultsForUsages); + this.listenToOnce( + this.usagesCollection, + "sync", + this.getSearchResultsForUsages, + ); //Listen to the collection for errors this.listenToOnce(this.usagesCollection, "error", this.showError); //When the SearchResults are retrieved, merge them with the Usages collection - this.listenToOnce(this.searchResults, "sync", function(){ + this.listenToOnce(this.searchResults, "sync", function () { this.mergeSearchResults(); //Update the view with info about the corresponding Usage model @@ -227,32 +236,27 @@

Source: src/js/views/portals/PortalUsagesView.js

//Fetch the collection this.usagesCollection.fetch({ quotaType: "portal", - subject: MetacatUI.appUserModel.get("username") + subject: MetacatUI.appUserModel.get("username"), }); - - } - catch(e){ + } catch (e) { console.error("Failed to render the PortalUsagesView: ", e); } - }, /** - * Using the Usages collection, this function creates Filters that search for - * the portal objects for those Usages - * @param {Usages} usages The Usages collection to get search results for - */ - getSearchResultsForUsages: function(usages){ - - try{ - + * Using the Usages collection, this function creates Filters that search for + * the portal objects for those Usages + * @param {Usages} usages The Usages collection to get search results for + */ + getSearchResultsForUsages: function (usages) { + try { //Set the number of portals to the number of usages found this.numPortals = this.usagesCollection.length; var portalIds = this.usagesCollection.pluck("instanceId"); //If there are no given filters, create a Filter for the seriesId of each portal Usage - if( !this.filters && portalIds.length ){ + if (!this.filters && portalIds.length) { this.filters = new Filters(); this.filters.mustMatchIds = true; @@ -261,101 +265,104 @@

Source: src/js/views/portals/PortalUsagesView.js

values: portalIds, operator: "OR", matchSubstring: false, - exclude: false + exclude: false, }); //Only get Portals that the user is an owner of this.filters.addWritePermissionFilter(); } //If the filters set on this view is an array of JSON, add it to a Filters collection - else if( this.filters.length && !Filters.prototype.isPrototypeOf(this.filters) ){ + else if ( + this.filters.length && + !Filters.prototype.isPrototypeOf(this.filters) + ) { //Create search filters for finding the portals var filters = new Filters(); - filters.add( this.filters ); + filters.add(this.filters); this.filters = filters; - } - else{ + } else { this.filters = new Filters(); } this.getSearchResults(); - - } - catch(e){ + } catch (e) { this.showError(); - console.error("Failed to create search results for the portal list: ", e); + console.error( + "Failed to create search results for the portal list: ", + e, + ); } - }, /** - * Merges the SearchResults collection with the Usages collection - */ - mergeSearchResults: function(searchResults){ - - if(typeof searchResults == "undefined"){ + * Merges the SearchResults collection with the Usages collection + */ + mergeSearchResults: function (searchResults) { + if (typeof searchResults == "undefined") { var searchResults = this.searchResults; } this.usagesCollection.mergeCollections(searchResults); //If in DataONE Plus Preview mode, total the portal count from Solr and use that as the portal totalUsage - if( MetacatUI.appModel.get("dataonePlusPreviewMode") ){ - + if (MetacatUI.appModel.get("dataonePlusPreviewMode")) { var portalQuotas = MetacatUI.appUserModel.getQuotas("portal"); - if( portalQuotas.length ){ + if (portalQuotas.length) { portalQuotas[0].set("totalUsage", this.usagesCollection.length); } - } }, /** - * Shows the Usage info for each Portal in this view - */ - showUsageInfo: function(){ - - this.usagesCollection.each(function(usage){ - + * Shows the Usage info for each Portal in this view + */ + showUsageInfo: function () { + this.usagesCollection.each(function (usage) { //Find the list item HTML element for this Usage - var listItem = this.$("[data-seriesId='" + usage.get("instanceId") + "']"); + var listItem = this.$( + "[data-seriesId='" + usage.get("instanceId") + "']", + ); //If a list item is found, update it - if( listItem.length ){ - + if (listItem.length) { //Disable the Edit button if the Usage status is "inactive" - if( usage.get("status") == "inactive" ){ - listItem.find(".edit.btn") - .attr("disabled", "disabled") - .popover({ - trigger: "hover focus click", - placement: "top", - delay: { - show: 800 - }, - html: true, - content: "To edit this " + MetacatUI.appModel.get("portalTermSingular") + ", contact us at " + - "<a href='mailto:" + MetacatUI.appModel.get("emailContact") + "'>" + - MetacatUI.appModel.get("emailContact") + "</a>" + - " to activate it. It may be deactivated because your " + - MetacatUI.appModel.get("dataonePlusName") + " membership has ended." - }); + if (usage.get("status") == "inactive") { + listItem + .find(".edit.btn") + .attr("disabled", "disabled") + .popover({ + trigger: "hover focus click", + placement: "top", + delay: { + show: 800, + }, + html: true, + content: + "To edit this " + + MetacatUI.appModel.get("portalTermSingular") + + ", contact us at " + + "<a href='mailto:" + + MetacatUI.appModel.get("emailContact") + + "'>" + + MetacatUI.appModel.get("emailContact") + + "</a>" + + " to activate it. It may be deactivated because your " + + MetacatUI.appModel.get("dataonePlusName") + + " membership has ended.", + }); } - } - }, this); //Add a "Create" button to create a new portal, since we know the total Usage and // remaining Quota now. this.renderCreateButton(); - - } - - }); + }, + }, + ); });
diff --git a/docs/docs/src_js_views_portals_PortalView.js.html b/docs/docs/src_js_views_portals_PortalView.js.html index bc6f051ef..34ac5f316 100644 --- a/docs/docs/src_js_views_portals_PortalView.js.html +++ b/docs/docs/src_js_views_portals_PortalView.js.html @@ -44,7 +44,8 @@

Source: src/js/views/portals/PortalView.js

-
define(["jquery",
+            
define([
+  "jquery",
   "underscore",
   "backbone",
   "models/portals/PortalModel",
@@ -59,846 +60,878 @@ 

Source: src/js/views/portals/PortalView.js

"views/portals/PortalMetricsView", "views/portals/PortalMembersView", "views/portals/PortalLogosView", - "views/portals/PortalVisualizationsView" -], - function ($, _, Backbone, Portal, User, AlertTemplate, LoadingTemplate, PortalTemplate, EditPortalsTemplate, PortalHeaderView, - PortalDataView, PortalSectionView, PortalMetricsView, PortalMembersView, PortalLogosView, PortalVisualizationsView) { - "use_strict"; - - /** - * @class PortalView - * @classdesc The PortalView is a generic view to render - * portals, it will hold portal sections - * @classcategory Views/Portals - * @extends Backbone.View - * @constructor - */ - var PortalView = Backbone.View.extend( - /** @lends PortalView.prototype */{ - - /** - * The Portal element - * @type {string} - */ - el: "#Content", - - /** - * The type of View this is - * @type {string} - */ - type: "Portal", - - /** - * The currently active section view - * @type {PortalSectionView} - */ - activeSection: undefined, - - /** - * The currently active section label. e.g. Data, Metrics, Settings, etc. - * @type {string} - */ - activeSectionLabel: "", - - /** - * The names of all sections in this portal editor - * @type {Array} - */ - sectionNames: [], - - /** - * The seriesId of the portal document - * @type {string} - */ - portalId: "", - - /** - * The unique short name of the portal - * @type {string} - */ - label: "", - - /** - * Flag to add section name to URL. Enabled by default. - * @type {boolean} - */ - displaySectionInUrl: true, - - /** - * The subviews contained within this view to be removed with onClose - * @type {Array} - */ - subviews: new Array(), // Could be a literal object {} */ - - /** - * A reference to the Portal Logos View that displays the logos of this portal. - * @type PortalLogosView - */ - logosView: null, - - /** - * A Portal Model is associated with this view and gets created during render() - * @type {Portal} - */ - model: null, - - /** - * A User Model is associated with this view for rendering node/user views - * @type {User} - */ - userModel: null, - - /* Renders the compiled template into HTML */ - template: _.template(PortalTemplate), - //A template to display a notification message - alertTemplate: _.template(AlertTemplate), - //A template for displaying a loading message - loadingTemplate: _.template(LoadingTemplate), - // Template for the 'edit portal' button - editPortalsTemplate: _.template(EditPortalsTemplate), - - /** - * A jQuery selector for the element that a single section link will be inserted into - * @type {string} - */ - sectionLinkContainer: ".section-link-container", - /** - * A jQuery selector for the elements that are links to the individual sections - * @type {string} - */ - sectionLinks: ".portal-section-link", - /** - * A jQuery selector for the section elements - * @type {string} - */ - sectionEls: ".portal-section-view", - /** - * A jQuery selection for the element that will contain the Edit button. - * @type {string} - * @since 2.14.0 - */ - editButtonContainer: ".edit-portal-link-container", - /** - * The events this view will listen to and the associated function to call. - * @type {Object} - */ - events: { - "click .portal-section-link": "handleSwitchSection", - "click .section-links-container": "toggleSectionLinks" - }, - - /** - * Is executed when a new PortalView is created - */ - initialize: function (options) { - // Set the current PortalView properties - this.portalId = options.portalId ? options.portalId : undefined; - this.model = options.model ? options.model : undefined; - this.nodeView = options.nodeView ? options.nodeView : undefined; - this.label = options.label ? options.label : undefined; - this.activeSection = options.activeSection ? options.activeSection : undefined; - this.activeSectionLabel = options.activeSectionLabel ? options.activeSectionLabel : undefined; - }, - - /** - * Initial render of the PortalView - * - * @return {PortalView} Returns itself for easy function stacking in the app - */ - render: function () { - - var view = this; - - //Make sure the subviews array is reset - this.subviews = new Array(); - - // Add the overall class immediately so the navbar is styled correctly right away - $("body").addClass("PortalView"); - - this.$el.html(this.loadingTemplate({ - msg: "Loading..." - })); - - //Perform specific label checks - if (!MetacatUI.nodeModel.get("checked")) { - this.listenToOnce(MetacatUI.nodeModel, "change:checked", function () { - // perform node checks - if (view.isNode(view.label)) { - view.nodeView = true; - view.renderAsNode(); - } - else { - view.nodeView = false; - view.renderAsPortal(); - } - }); + "views/portals/PortalVisualizationsView", +], function ( + $, + _, + Backbone, + Portal, + User, + AlertTemplate, + LoadingTemplate, + PortalTemplate, + EditPortalsTemplate, + PortalHeaderView, + PortalDataView, + PortalSectionView, + PortalMetricsView, + PortalMembersView, + PortalLogosView, + PortalVisualizationsView, +) { + "use_strict"; + + /** + * @class PortalView + * @classdesc The PortalView is a generic view to render + * portals, it will hold portal sections + * @classcategory Views/Portals + * @extends Backbone.View + * @constructor + */ + var PortalView = Backbone.View.extend( + /** @lends PortalView.prototype */ { + /** + * The Portal element + * @type {string} + */ + el: "#Content", + + /** + * The type of View this is + * @type {string} + */ + type: "Portal", + + /** + * The currently active section view + * @type {PortalSectionView} + */ + activeSection: undefined, + + /** + * The currently active section label. e.g. Data, Metrics, Settings, etc. + * @type {string} + */ + activeSectionLabel: "", + + /** + * The names of all sections in this portal editor + * @type {Array} + */ + sectionNames: [], + + /** + * The seriesId of the portal document + * @type {string} + */ + portalId: "", + + /** + * The unique short name of the portal + * @type {string} + */ + label: "", + + /** + * Flag to add section name to URL. Enabled by default. + * @type {boolean} + */ + displaySectionInUrl: true, + + /** + * The subviews contained within this view to be removed with onClose + * @type {Array} + */ + subviews: new Array(), // Could be a literal object {} */ + + /** + * A reference to the Portal Logos View that displays the logos of this portal. + * @type PortalLogosView + */ + logosView: null, + + /** + * A Portal Model is associated with this view and gets created during render() + * @type {Portal} + */ + model: null, + + /** + * A User Model is associated with this view for rendering node/user views + * @type {User} + */ + userModel: null, + + /* Renders the compiled template into HTML */ + template: _.template(PortalTemplate), + //A template to display a notification message + alertTemplate: _.template(AlertTemplate), + //A template for displaying a loading message + loadingTemplate: _.template(LoadingTemplate), + // Template for the 'edit portal' button + editPortalsTemplate: _.template(EditPortalsTemplate), + + /** + * A jQuery selector for the element that a single section link will be inserted into + * @type {string} + */ + sectionLinkContainer: ".section-link-container", + /** + * A jQuery selector for the elements that are links to the individual sections + * @type {string} + */ + sectionLinks: ".portal-section-link", + /** + * A jQuery selector for the section elements + * @type {string} + */ + sectionEls: ".portal-section-view", + /** + * A jQuery selection for the element that will contain the Edit button. + * @type {string} + * @since 2.14.0 + */ + editButtonContainer: ".edit-portal-link-container", + /** + * The events this view will listen to and the associated function to call. + * @type {Object} + */ + events: { + "click .portal-section-link": "handleSwitchSection", + "click .section-links-container": "toggleSectionLinks", + }, + + /** + * Is executed when a new PortalView is created + */ + initialize: function (options) { + // Set the current PortalView properties + this.portalId = options.portalId ? options.portalId : undefined; + this.model = options.model ? options.model : undefined; + this.nodeView = options.nodeView ? options.nodeView : undefined; + this.label = options.label ? options.label : undefined; + this.activeSection = options.activeSection + ? options.activeSection + : undefined; + this.activeSectionLabel = options.activeSectionLabel + ? options.activeSectionLabel + : undefined; + }, + + /** + * Initial render of the PortalView + * + * @return {PortalView} Returns itself for easy function stacking in the app + */ + render: function () { + var view = this; + + //Make sure the subviews array is reset + this.subviews = new Array(); + + // Add the overall class immediately so the navbar is styled correctly right away + $("body").addClass("PortalView"); + + this.$el.html( + this.loadingTemplate({ + msg: "Loading...", + }), + ); + + //Perform specific label checks + if (!MetacatUI.nodeModel.get("checked")) { + this.listenToOnce(MetacatUI.nodeModel, "change:checked", function () { + // perform node checks + if (view.isNode(view.label)) { + view.nodeView = true; + view.renderAsNode(); + } else { + view.nodeView = false; + view.renderAsPortal(); + } + }); - this.listenToOnce(MetacatUI.nodeModel, "error", function () { - this.showError(null, "Couldn't get the DataONE Node info document"); - }); - } - else if (MetacatUI.nodeModel.get("error")) { + this.listenToOnce(MetacatUI.nodeModel, "error", function () { this.showError(null, "Couldn't get the DataONE Node info document"); - } - else if (this.isNode(this.label)) { - this.nodeView = true; - this.renderAsNode(); - } - else if (!this.isNode(this.label)) { - this.nodeView = false; - this.renderAsPortal(); - } - - return this; - }, - - /** - * Entry point for portal rendering - */ - renderAsPortal: function () { - - // At this point we know that the given label is not a - // repository short identifier - - // Create a new Portal model - if (this.model === undefined || this.model === null) { - this.model = new Portal({ - seriesId: this.portalId, - label: this.label - }); - } - - // When the model has been synced, render the results - this.stopListening(); - this.listenToOnce(this.model, "sync", this.renderPortal); - - //If the portal isn't found, display a 404 message - this.listenTo(this.model, "notFound", this.handleNotFound); - - //Listen to errors that might occur during fetch() - this.listenToOnce(this.model, "error", this.showError); - - //Fetch the model - this.model.fetch({ objectOnly: true }); + }); + } else if (MetacatUI.nodeModel.get("error")) { + this.showError(null, "Couldn't get the DataONE Node info document"); + } else if (this.isNode(this.label)) { + this.nodeView = true; + this.renderAsNode(); + } else if (!this.isNode(this.label)) { + this.nodeView = false; + this.renderAsPortal(); + } - }, + return this; + }, - /** - * Entry point for a repository portal view - * At this point we know for sure that a given label/username is a repository user - */ - renderAsNode: function () { - var view = this; + /** + * Entry point for portal rendering + */ + renderAsPortal: function () { + // At this point we know that the given label is not a + // repository short identifier - //Create a UserModel with the username given - this.userModel = new User({ - username: view.label - }); - this.userModel.saveAsNode(); - // get the node Info - var nodeInfo = _.find(MetacatUI.nodeModel.get("members"), function (nodeModel) { - return nodeModel.identifier.toLowerCase() == "urn:node:" + view.label.toLowerCase(); - }); - this.nodeInfo = nodeInfo; - this.nodeName = this.nodeInfo.name; - this.portalId = this.nodeInfo.identifier; - - // create a portal model for repository + // Create a new Portal model + if (this.model === undefined || this.model === null) { this.model = new Portal({ seriesId: this.portalId, - label: view.label, - name: this.nodeInfo.name, - description: this.nodeInfo.description, + label: this.label, }); + } - // remove the members section directly from the model - this.model.removeSection("members"); - - this.model.createNodeAttributes(this.nodeInfo); - - //Setting the repo specific statsModel - var statsSearchModel = this.userModel.get("searchModel").clone(); - statsSearchModel.set("exclude", [], { silent: true }).set("formatType", [], { silent: true }); - MetacatUI.statsModel.set("query", statsSearchModel.getQuery()); - MetacatUI.statsModel.set("searchModel", statsSearchModel); - - if (_.contains(MetacatUI.appModel.get("dataoneHostedRepos"), this.nodeInfo.identifier)) { - MetacatUI.statsModel.set("mdqImageId", this.nodeInfo.identifier); - } + // When the model has been synced, render the results + this.stopListening(); + this.listenToOnce(this.model, "sync", this.renderPortal); + + //If the portal isn't found, display a 404 message + this.listenTo(this.model, "notFound", this.handleNotFound); + + //Listen to errors that might occur during fetch() + this.listenToOnce(this.model, "error", this.showError); + + //Fetch the model + this.model.fetch({ objectOnly: true }); + }, + + /** + * Entry point for a repository portal view + * At this point we know for sure that a given label/username is a repository user + */ + renderAsNode: function () { + var view = this; + + //Create a UserModel with the username given + this.userModel = new User({ + username: view.label, + }); + this.userModel.saveAsNode(); + // get the node Info + var nodeInfo = _.find( + MetacatUI.nodeModel.get("members"), + function (nodeModel) { + return ( + nodeModel.identifier.toLowerCase() == + "urn:node:" + view.label.toLowerCase() + ); + }, + ); + this.nodeInfo = nodeInfo; + this.nodeName = this.nodeInfo.name; + this.portalId = this.nodeInfo.identifier; + + // create a portal model for repository + this.model = new Portal({ + seriesId: this.portalId, + label: view.label, + name: this.nodeInfo.name, + description: this.nodeInfo.description, + }); + + // remove the members section directly from the model + this.model.removeSection("members"); + + this.model.createNodeAttributes(this.nodeInfo); + + //Setting the repo specific statsModel + var statsSearchModel = this.userModel.get("searchModel").clone(); + statsSearchModel + .set("exclude", [], { silent: true }) + .set("formatType", [], { silent: true }); + MetacatUI.statsModel.set("query", statsSearchModel.getQuery()); + MetacatUI.statsModel.set("searchModel", statsSearchModel); + + if ( + _.contains( + MetacatUI.appModel.get("dataoneHostedRepos"), + this.nodeInfo.identifier, + ) + ) { + MetacatUI.statsModel.set("mdqImageId", this.nodeInfo.identifier); + } - // render repository view as portal view - this.renderPortal(); - }, - - /** - * Render the Portal view - */ - renderPortal: function () { - - // Set the document title to the portal name - MetacatUI.appModel.set("title", this.model.get("name")) - MetacatUI.appModel.set("description", this.model.get("description")) - - // Getting the correct portal label and seriesID - this.label = this.model.get("label"); - this.portalId = this.model.get("seriesId"); - - // Remove the listeners that were set during the fetch() process - this.stopListening(this.model, "notFound", this.handleNotFound); - this.stopListening(this.model, "error", this.showError); - - //If this is in DataONE Plus Preview Mode, check that the portal is - // a Plus portal before rendering. Member Node portals are always displayed. - if (MetacatUI.appModel.get("dataonePlusPreviewMode") && !this.nodeView) { - var sourceMN = this.model.get("datasource"); - - //Check if the portal source node is from the active alt repo OR is - // configured as a Plus portal. - if (typeof sourceMN != "string" || - (sourceMN != MetacatUI.appModel.get("defaultAlternateRepositoryId") && - !_.findWhere(MetacatUI.appModel.get("dataonePlusPreviewPortals"), - { datasource: sourceMN, seriesId: this.model.get("seriesId") }))) { - - //Get the name of the source member node - var sourceMNName = "original data repository", - mnURL = ""; - if (typeof sourceMN == "string") { - var sourceMNObject = MetacatUI.nodeModel.getMember(sourceMN); - if (sourceMNObject) { - sourceMNName = sourceMNObject.name; - - //If there is a baseURL string - if (sourceMNObject.baseURL) { - //Parse out the origin of the baseURL string. We want to crop out the /metacat/d1/mn parts. - mnURL = sourceMNObject.baseURL.substring(0, sourceMNObject.baseURL.lastIndexOf(".")) + - sourceMNObject.baseURL.substring(sourceMNObject.baseURL.lastIndexOf("."), - sourceMNObject.baseURL.indexOf("/", sourceMNObject.baseURL.lastIndexOf("."))); - } + // render repository view as portal view + this.renderPortal(); + }, + + /** + * Render the Portal view + */ + renderPortal: function () { + // Set the document title to the portal name + MetacatUI.appModel.set("title", this.model.get("name")); + MetacatUI.appModel.set("description", this.model.get("description")); + + // Getting the correct portal label and seriesID + this.label = this.model.get("label"); + this.portalId = this.model.get("seriesId"); + + // Remove the listeners that were set during the fetch() process + this.stopListening(this.model, "notFound", this.handleNotFound); + this.stopListening(this.model, "error", this.showError); + + //If this is in DataONE Plus Preview Mode, check that the portal is + // a Plus portal before rendering. Member Node portals are always displayed. + if ( + MetacatUI.appModel.get("dataonePlusPreviewMode") && + !this.nodeView + ) { + var sourceMN = this.model.get("datasource"); + + //Check if the portal source node is from the active alt repo OR is + // configured as a Plus portal. + if ( + typeof sourceMN != "string" || + (sourceMN != + MetacatUI.appModel.get("defaultAlternateRepositoryId") && + !_.findWhere( + MetacatUI.appModel.get("dataonePlusPreviewPortals"), + { datasource: sourceMN, seriesId: this.model.get("seriesId") }, + )) + ) { + //Get the name of the source member node + var sourceMNName = "original data repository", + mnURL = ""; + if (typeof sourceMN == "string") { + var sourceMNObject = MetacatUI.nodeModel.getMember(sourceMN); + if (sourceMNObject) { + sourceMNName = sourceMNObject.name; + + //If there is a baseURL string + if (sourceMNObject.baseURL) { + //Parse out the origin of the baseURL string. We want to crop out the /metacat/d1/mn parts. + mnURL = + sourceMNObject.baseURL.substring( + 0, + sourceMNObject.baseURL.lastIndexOf("."), + ) + + sourceMNObject.baseURL.substring( + sourceMNObject.baseURL.lastIndexOf("."), + sourceMNObject.baseURL.indexOf( + "/", + sourceMNObject.baseURL.lastIndexOf("."), + ), + ); } } + } - //Show a message that the portal can be found on the repository website. - var message = $(document.createElement("h3")).addClass("center stripe"); - message.text("The " + this.model.get("name") + " " + MetacatUI.appModel.get("portalTermSingular") + - " can be viewed in the "); + //Show a message that the portal can be found on the repository website. + var message = $(document.createElement("h3")).addClass( + "center stripe", + ); + message.text( + "The " + + this.model.get("name") + + " " + + MetacatUI.appModel.get("portalTermSingular") + + " can be viewed in the ", + ); - if (mnURL) { - message.append($(document.createElement("a")) + if (mnURL) { + message.append( + $(document.createElement("a")) .attr("href", mnURL) .attr("target", "_blank") - .text(sourceMNName)); - } - else { - message.append(sourceMNName); - } + .text(sourceMNName), + ); + } else { + message.append(sourceMNName); + } - this.$el.html(message); + this.$el.html(message); - return; - } + return; } + } - // Check for theme/layout settings and add the required files - this.addTheming(); + // Check for theme/layout settings and add the required files + this.addTheming(); - // Insert the overall portal template - this.$el.html(this.template(this.model.toJSON())); + // Insert the overall portal template + this.$el.html(this.template(this.model.toJSON())); - // Render the header view - this.headerView = new PortalHeaderView({ - model: this.model, - nodeView: this.nodeView - }); - this.headerView.render(); - this.subviews.push(this.headerView); + // Render the header view + this.headerView = new PortalHeaderView({ + model: this.model, + nodeView: this.nodeView, + }); + this.headerView.render(); + this.subviews.push(this.headerView); - // only displaying the edit button for non-repository profiles - if (!this.nodeView) { - // Add edit button if user is authorized - this.insertOwnerControls(); - } + // only displaying the edit button for non-repository profiles + if (!this.nodeView) { + // Add edit button if user is authorized + this.insertOwnerControls(); + } - // Render the content sections - _.each(this.model.get("sections"), function (section) { + // Render the content sections + _.each( + this.model.get("sections"), + function (section) { this.addSection(section); - }, this); - - // Render the Data section - if (this.model.get("hideData") !== true) { - this.sectionDataView = new PortalDataView({ - model: this.model, - sectionName: "Data", - id: "Data", - nodeView: this.nodeView - }); - this.subviews.push(this.sectionDataView); - - this.$("#portal-sections").append(this.sectionDataView.el); + }, + this, + ); - //Render the section view and add it to the page - this.sectionDataView.render(); + // Render the Data section + if (this.model.get("hideData") !== true) { + this.sectionDataView = new PortalDataView({ + model: this.model, + sectionName: "Data", + id: "Data", + nodeView: this.nodeView, + }); + this.subviews.push(this.sectionDataView); - this.addSectionLink(this.sectionDataView); - } + this.$("#portal-sections").append(this.sectionDataView.el); - //Render the metrics section link - if (this.model.get("hideMetrics") !== true) { + //Render the section view and add it to the page + this.sectionDataView.render(); - //Create a PortalMetricsView - this.metricsView = new PortalMetricsView({ - model: this.model, - id: this.model.get("metricsLabel"), - uniqueSectionName: this.model.get("metricsLabel"), - nodeView: this.nodeView, - nodeName: this.nodeName - }); + this.addSectionLink(this.sectionDataView); + } - this.subviews.push(this.metricsView); - this.$("#portal-sections").append(this.metricsView.el); + //Render the metrics section link + if (this.model.get("hideMetrics") !== true) { + //Create a PortalMetricsView + this.metricsView = new PortalMetricsView({ + model: this.model, + id: this.model.get("metricsLabel"), + uniqueSectionName: this.model.get("metricsLabel"), + nodeView: this.nodeView, + nodeName: this.nodeName, + }); - this.metricsView.render(); + this.subviews.push(this.metricsView); + this.$("#portal-sections").append(this.metricsView.el); - this.addSectionLink(this.metricsView); + this.metricsView.render(); - } - - // Render the members section - if (this.model.get("hideMembers") !== true && - (this.model.get("associatedParties").length || this.model.get("acknowledgments"))) { + this.addSectionLink(this.metricsView); + } - this.sectionMembersView = new PortalMembersView({ - model: this.model, - id: "Members", - sectionName: "Members" - }); - this.subviews.push(this.sectionMembersView); + // Render the members section + if ( + this.model.get("hideMembers") !== true && + (this.model.get("associatedParties").length || + this.model.get("acknowledgments")) + ) { + this.sectionMembersView = new PortalMembersView({ + model: this.model, + id: "Members", + sectionName: "Members", + }); + this.subviews.push(this.sectionMembersView); - this.$("#portal-sections").append(this.sectionMembersView.el); + this.$("#portal-sections").append(this.sectionMembersView.el); - //Render the section view and add it to the page - this.sectionMembersView.render(); + //Render the section view and add it to the page + this.sectionMembersView.render(); - this.addSectionLink(this.sectionMembersView); - } + this.addSectionLink(this.sectionMembersView); + } - // Render the logos at the bottom of the portal page - var ackLogos = this.model.get("acknowledgmentsLogos") || []; - if (ackLogos.length) { - this.logosView = new PortalLogosView(); - this.logosView.logos = ackLogos; - this.subviews.push(this.logosView); - this.logosView.render(); - this.$(".portal-view").append(this.logosView.el); - } + // Render the logos at the bottom of the portal page + var ackLogos = this.model.get("acknowledgmentsLogos") || []; + if (ackLogos.length) { + this.logosView = new PortalLogosView(); + this.logosView.logos = ackLogos; + this.subviews.push(this.logosView); + this.logosView.render(); + this.$(".portal-view").append(this.logosView.el); + } - // Re-order the section tabs according the the portal editor's preference, - // if one has been set - try { - var pageOrder = this.model.get("pageOrder"); - if (pageOrder && pageOrder.length) { - var linksContainer = this.el.querySelector("#portal-section-tabs"), - sortableLinks = this.el.querySelectorAll("#portal-section-tabs .section-link-container"), - sortableLinksArray = Array.prototype.slice.call(sortableLinks, 0); - // sort the links according the pageOrder - sortableLinksArray.sort(function (a, b) { - var aName = $(a).text(); - var bName = $(b).text(); - var aIndex = pageOrder.indexOf(aName); - var bIndex = pageOrder.indexOf(bName); - // If the label can't be found in the list of labels, place it at the end - if (bIndex === -1) { - return +1 - } - if (aIndex === -1) { - return -1 - } - // Sort backwards, because we use preprend - return bIndex - aIndex; - }) - // Rearrange the links in the DOM - for (i = 0; i < sortableLinksArray.length; ++i) { - linksContainer.prepend(sortableLinksArray[i]); + // Re-order the section tabs according the the portal editor's preference, + // if one has been set + try { + var pageOrder = this.model.get("pageOrder"); + if (pageOrder && pageOrder.length) { + var linksContainer = this.el.querySelector("#portal-section-tabs"), + sortableLinks = this.el.querySelectorAll( + "#portal-section-tabs .section-link-container", + ), + sortableLinksArray = Array.prototype.slice.call(sortableLinks, 0); + // sort the links according the pageOrder + sortableLinksArray.sort(function (a, b) { + var aName = $(a).text(); + var bName = $(b).text(); + var aIndex = pageOrder.indexOf(aName); + var bIndex = pageOrder.indexOf(bName); + // If the label can't be found in the list of labels, place it at the end + if (bIndex === -1) { + return +1; } + if (aIndex === -1) { + return -1; + } + // Sort backwards, because we use preprend + return bIndex - aIndex; + }); + // Rearrange the links in the DOM + for (i = 0; i < sortableLinksArray.length; ++i) { + linksContainer.prepend(sortableLinksArray[i]); } - } catch (error) { - console.log("Error re-arranging tabs according to the pageOrder option. Error message: " + error) } + } catch (error) { + console.log( + "Error re-arranging tabs according to the pageOrder option. Error message: " + + error, + ); + } - //Switch to the active section - this.switchSection(); + //Switch to the active section + this.switchSection(); - //Scroll to an inner-page link if there is one specified - if (window.location.hash && this.$(window.location.hash).length) { - MetacatUI.appView.scrollTo(this.$(window.location.hash)); - } + //Scroll to an inner-page link if there is one specified + if (window.location.hash && this.$(window.location.hash).length) { + MetacatUI.appView.scrollTo(this.$(window.location.hash)); + } - // Save reference to this view + // Save reference to this view + var view = this; + + // On mobile, hide section tabs a moment after page loads so + // users notice where they are + setTimeout(function () { + view.toggleSectionLinks(); + }, 700); + + // On mobile where the section-links-container is set to fixed, + // hide the portal navigation element when user scrolls down, + // show again when the user scrolls up. + MetacatUI.appView.prevScrollpos = window.pageYOffset; + $(window).on("scroll", "", undefined, this.handleScroll); + }, + + /** + * Checks the portal model for theme or layout options. If there are any, and if + * they are supported, then add the associated CSS. + */ + addTheming: function () { + try { + // Check for theme and layout settings. + var theme = this.model.get("theme"); + var layout = this.model.get("layout"); + // TODO: make supported themes an app model config option? + var supportedThemes = ["dark", "light"]; + var supportedLayouts = ["panels"]; + // We must remove theme/layout CSS when the user navigates away from the + // portal in onClose(). To do this, we need to keep track of which CSS is + // added during this step. var view = this; - - // On mobile, hide section tabs a moment after page loads so - // users notice where they are - setTimeout(function () { - view.toggleSectionLinks(); - }, 700); - - // On mobile where the section-links-container is set to fixed, - // hide the portal navigation element when user scrolls down, - // show again when the user scrolls up. - MetacatUI.appView.prevScrollpos = window.pageYOffset; - $(window).on("scroll", "", undefined, this.handleScroll); - - }, - - /** - * Checks the portal model for theme or layout options. If there are any, and if - * they are supported, then add the associated CSS. - */ - addTheming: function () { - try { - // Check for theme and layout settings. - var theme = this.model.get("theme"); - var layout = this.model.get("layout"); - // TODO: make supported themes an app model config option? - var supportedThemes = ["dark", "light"]; - var supportedLayouts = ["panels"]; - // We must remove theme/layout CSS when the user navigates away from the - // portal in onClose(). To do this, we need to keep track of which CSS is - // added during this step. - var view = this - view.addedThemeCSS = [] - // Layout should be added before theme for CSS rules to work together properly - // when there is a theme + layout - if (layout && supportedLayouts.includes(layout)) { - require( - ["text!" + MetacatUI.root + "/css/portal-layouts/" + layout + ".css"], - function (ThemeCss) { - var cssID = "portal-layout-" + layout; - MetacatUI.appModel.addCSS(ThemeCss, cssID); - view.addedThemeCSS.push(cssID) - }) - } - if (theme && supportedThemes.includes(theme)) { - require( - ["text!" + MetacatUI.root + "/css/portal-themes/" + theme + ".css"], - function (ThemeCss) { - var cssID = "portal-theme-" + theme; - MetacatUI.appModel.addCSS(ThemeCss, cssID); - view.addedThemeCSS.push(cssID) - }) - } + view.addedThemeCSS = []; + // Layout should be added before theme for CSS rules to work together properly + // when there is a theme + layout + if (layout && supportedLayouts.includes(layout)) { + require([ + "text!" + + MetacatUI.root + + "/css/portal-layouts/" + + layout + + ".css", + ], function (ThemeCss) { + var cssID = "portal-layout-" + layout; + MetacatUI.appModel.addCSS(ThemeCss, cssID); + view.addedThemeCSS.push(cssID); + }); } - catch (error) { - console.log( - 'There was an error adding theme and/or layout styles in a PortalView' + - '. Error details: ' + error - ); + if (theme && supportedThemes.includes(theme)) { + require([ + "text!" + MetacatUI.root + "/css/portal-themes/" + theme + ".css", + ], function (ThemeCss) { + var cssID = "portal-theme-" + theme; + MetacatUI.appModel.addCSS(ThemeCss, cssID); + view.addedThemeCSS.push(cssID); + }); } - }, - - /** - * toggleSectionLinks - show or hide the section links nav. Used for - * mobile/small screens only. - */ - toggleSectionLinks: function () { - try { - // Only toggle the section links on mobile. On mobile, the - // ".show-sections-toggle" is visible. - if (this.$(".show-sections-toggle").is(":visible")) { - this.$("#portal-section-tabs").slideToggle(); - } - } catch (e) { - console.log("Failed to toggle section links, error message: " + e); + } catch (error) { + console.log( + "There was an error adding theme and/or layout styles in a PortalView" + + ". Error details: " + + error, + ); + } + }, + + /** + * toggleSectionLinks - show or hide the section links nav. Used for + * mobile/small screens only. + */ + toggleSectionLinks: function () { + try { + // Only toggle the section links on mobile. On mobile, the + // ".show-sections-toggle" is visible. + if (this.$(".show-sections-toggle").is(":visible")) { + this.$("#portal-section-tabs").slideToggle(); } - }, - - /* - * Checks the authority for the logged in user for this portal and - * inserts control elements onto the page for the user to interact - * with the portal. So far, this is just an 'edit portal' button. - */ - insertOwnerControls: function () { - - // Insert the button into the navbar - var container = $(this.editButtonContainer); - - var model = this.model; - - this.listenToOnce(this.model, "change:isAuthorized", function () { - if (!model.get("isAuthorized")) { - return false; - } else { - container.html( - this.editPortalsTemplate({ - editButtonText: "Edit " + MetacatUI.appModel.get('portalTermSingular'), - pathToEdit: MetacatUI.root + "/edit/" + MetacatUI.appModel.get("portalTermPlural") + "/" + model.get("label") - }) - ); - } - }); - - this.model.checkAuthority("write"); - }, - - /** - * Update the window location path with the active section name - * @param {boolean} [showSectionLabel] - If true, the section label will be added to the path - */ - updatePath: function (showSectionLabel) { - - var label = this.model.get("label") || this.newPortalTempName, - originalLabel = this.model.get("originalLabel") || this.newPortalTempName, - pathName = decodeURIComponent(window.location.pathname) - .substring(MetacatUI.root.length) - // remove trailing forward slash if one exists in path - .replace(/\/$/, ""); - - // Add or replace the label and section part of the path with updated values. - // pathRE matches "/label/section", where the "/section" part is optional - var pathRE = new RegExp("\\/(" + label + "|" + originalLabel + ")(\\/[^\\/]*)?$", "i"); - newPathName = pathName.replace(pathRE, "") + "/" + label; - - if (showSectionLabel && this.activeSection) { - newPathName += "/" + this.activeSection.uniqueSectionLabel; + } catch (e) { + console.log("Failed to toggle section links, error message: " + e); + } + }, + + /* + * Checks the authority for the logged in user for this portal and + * inserts control elements onto the page for the user to interact + * with the portal. So far, this is just an 'edit portal' button. + */ + insertOwnerControls: function () { + // Insert the button into the navbar + var container = $(this.editButtonContainer); + + var model = this.model; + + this.listenToOnce(this.model, "change:isAuthorized", function () { + if (!model.get("isAuthorized")) { + return false; + } else { + container.html( + this.editPortalsTemplate({ + editButtonText: + "Edit " + MetacatUI.appModel.get("portalTermSingular"), + pathToEdit: + MetacatUI.root + + "/edit/" + + MetacatUI.appModel.get("portalTermPlural") + + "/" + + model.get("label"), + }), + ); } + }); + + this.model.checkAuthority("write"); + }, + + /** + * Update the window location path with the active section name + * @param {boolean} [showSectionLabel] - If true, the section label will be added to the path + */ + updatePath: function (showSectionLabel) { + var label = this.model.get("label") || this.newPortalTempName, + originalLabel = + this.model.get("originalLabel") || this.newPortalTempName, + pathName = decodeURIComponent(window.location.pathname) + .substring(MetacatUI.root.length) + // remove trailing forward slash if one exists in path + .replace(/\/$/, ""); + + // Add or replace the label and section part of the path with updated values. + // pathRE matches "/label/section", where the "/section" part is optional + var pathRE = new RegExp( + "\\/(" + label + "|" + originalLabel + ")(\\/[^\\/]*)?$", + "i", + ); + newPathName = pathName.replace(pathRE, "") + "/" + label; + + if (showSectionLabel && this.activeSection) { + newPathName += "/" + this.activeSection.uniqueSectionLabel; + } - // Update the window location - MetacatUI.uiRouter.navigate(newPathName, { trigger: false }); - - }, - - /** - * Gets a list of section names from tab elements and updates the - * sectionNames attribute on this view. - */ - updateSectionNames: function () { - - // Get the section names from the tab elements - var sectionNames = []; - this.$(this.sectionLinks) - .each(function (i, anchorEl) { - sectionNames[i] = $(anchorEl) - .attr("href") - .substring(1) - }); - - // Set the array of sectionNames on the view - this.sectionNames = sectionNames - }, - - /** - * Manually switch to a section subview by making the tab and tab panel active. - * Navigation between sections is usually handled automatically by the Bootstrap - * library, but a manual switch may be necessary sometimes - * @param {PortalSectionView} [sectionView] - The section view to switch to. If not given, defaults to the activeSection set on the view. - */ - switchSection: function (sectionView) { - - //Create a flag for whether the section label should be shown in the URL - var showSectionLabelInURL = true; - - // If no section view is given, use the active section in the view. - if (!sectionView) { - //Use the sectionView set already - if (this.activeSection) { - var sectionView = this.activeSection; - } - //Or find the section view by name, which may have been passed through the URL - else if (this.activeSectionLabel) { - var sectionView = this.getSectionByLabel(this.activeSectionLabel); - } + // Update the window location + MetacatUI.uiRouter.navigate(newPathName, { trigger: false }); + }, + + /** + * Gets a list of section names from tab elements and updates the + * sectionNames attribute on this view. + */ + updateSectionNames: function () { + // Get the section names from the tab elements + var sectionNames = []; + this.$(this.sectionLinks).each(function (i, anchorEl) { + sectionNames[i] = $(anchorEl).attr("href").substring(1); + }); + + // Set the array of sectionNames on the view + this.sectionNames = sectionNames; + }, + + /** + * Manually switch to a section subview by making the tab and tab panel active. + * Navigation between sections is usually handled automatically by the Bootstrap + * library, but a manual switch may be necessary sometimes + * @param {PortalSectionView} [sectionView] - The section view to switch to. If not given, defaults to the activeSection set on the view. + */ + switchSection: function (sectionView) { + //Create a flag for whether the section label should be shown in the URL + var showSectionLabelInURL = true; + + // If no section view is given, use the active section in the view. + if (!sectionView) { + //Use the sectionView set already + if (this.activeSection) { + var sectionView = this.activeSection; } - - //If no section view was indicated, just default to the first visible one - if (!sectionView) { - var sectionView = this.$(this.sectionLinkContainer).first().data("view"); - - //If we are defaulting to the first section, don't show the section label in the URL - showSectionLabelInURL = false; - - //If there are no section views on the page at all, exit now - if (!sectionView) { - return; - } + //Or find the section view by name, which may have been passed through the URL + else if (this.activeSectionLabel) { + var sectionView = this.getSectionByLabel(this.activeSectionLabel); } + } - // Update the activeSection set on the view - this.activeSection = sectionView; + //If no section view was indicated, just default to the first visible one + if (!sectionView) { + var sectionView = this.$(this.sectionLinkContainer) + .first() + .data("view"); - // Activate the section content - this.$(this.sectionEls).each(function (i, contentEl) { - if ($(contentEl).data("view") == sectionView) { - $(contentEl).addClass("active"); - } else { - // make sure no other sections are active - $(contentEl).removeClass("active"); - } - }); + //If we are defaulting to the first section, don't show the section label in the URL + showSectionLabelInURL = false; - // Activate the link to the content - this.$(this.sectionLinkContainer).each(function (i, linkEl) { - if ($(linkEl).data("view") == sectionView) { - $(linkEl).addClass("active") - } else { - // make sure no other sections are active - $(linkEl).removeClass("active") - }; - }); - - //If the section view has post-render functionality, execute it now - if (typeof sectionView.postRender == "function") { - sectionView.postRender(); + //If there are no section views on the page at all, exit now + if (!sectionView) { + return; } + } - // Eventually, the panels layout will allow showing multiple sections at the - // same time in different panels. For now, the visualizations sections should - // take up the full height of the viewport (minus the header elements), and the - // footer should be hidden. - if ( - (this.model.get("layout") === "panels") && - (sectionView instanceof PortalVisualizationsView) - ) { - if( this.logosView ){ - this.logosView.el.style.setProperty('display', 'none') - } - if( MetacatUI.footerView ){ - MetacatUI.footerView.hide() - } + // Update the activeSection set on the view + this.activeSection = sectionView; + + // Activate the section content + this.$(this.sectionEls).each(function (i, contentEl) { + if ($(contentEl).data("view") == sectionView) { + $(contentEl).addClass("active"); } else { - if( this.logosView ){ - this.logosView.el.style.removeProperty('display') - } - if( MetacatUI.footerView ){ - MetacatUI.footerView.show() - } + // make sure no other sections are active + $(contentEl).removeClass("active"); } + }); - if (!this.nodeView) { - //Update the location path with the new section name - this.updatePath(showSectionLabelInURL); + // Activate the link to the content + this.$(this.sectionLinkContainer).each(function (i, linkEl) { + if ($(linkEl).data("view") == sectionView) { + $(linkEl).addClass("active"); + } else { + // make sure no other sections are active + $(linkEl).removeClass("active"); } + }); + //If the section view has post-render functionality, execute it now + if (typeof sectionView.postRender == "function") { + sectionView.postRender(); + } - }, - - /** - * When a section link has been clicked, switch to that section - * @param {Event} e - The click event on the section link - */ - handleSwitchSection: function (e) { - - e.preventDefault(); - - var sectionView = $(e.target).parents(this.sectionLinkContainer).first().data("view"); - - if (sectionView) { - this.switchSection(sectionView); - - // If the user clicks a link and is not near the top of the page - // (i.e. on mobile), scroll to the top of the section content. - // Otherwise it might look like the page hasn't changed (e.g. - // when focus is on the footer) - if (window.pageYOffset > this.$("#portal-sections").offset().top) { - MetacatUI.appView.scrollTo(this.$("#portal-sections")); - } - + // Eventually, the panels layout will allow showing multiple sections at the + // same time in different panels. For now, the visualizations sections should + // take up the full height of the viewport (minus the header elements), and the + // footer should be hidden. + if ( + this.model.get("layout") === "panels" && + sectionView instanceof PortalVisualizationsView + ) { + if (this.logosView) { + this.logosView.el.style.setProperty("display", "none"); } - - }, - - /** - * Returns the section view that has a label matching the one given. - * @param {string} label - The label for the section - * @return {PortalSectionView|false} - Returns false if a matching section view isn't found - */ - getSectionByLabel: function (label) { - - //If no label is given, exit - if (!label) { - return; + if (MetacatUI.footerView) { + MetacatUI.footerView.hide(); } - - //Find the section view whose unique label matches the given label. Case-insensitive matching. - return _.find(this.subviews, function (view) { - if (typeof view.uniqueSectionLabel == "string") { - return view.uniqueSectionLabel.toLowerCase() == label.toLowerCase(); - } - else { - return false; - } - }); - }, - - /** - * Creates and returns a unique label for the given section. This label is just used in the view, - * because portal sections can have duplicate labels. But unique labels need to be used for navigation in the view. - * @param {PortEditorSection} sectionModel - The section for which to create a unique label - * @return {string} The unique label string - */ - getUniqueSectionLabel: function (sectionModel) { - //Get the label for this section - var sectionLabel = sectionModel.get("label").replace(/[^a-zA-Z0-9 ]/g, "").replace(/ /g, "-"), - unalteredLabel = sectionLabel, - sectionLabels = this.sectionLabels || [], - i = 2; - - //Concatenate a number to the label if this one already exists - while (sectionLabels.includes(sectionLabel)) { - sectionLabel = unalteredLabel + i; - i++; + } else { + if (this.logosView) { + this.logosView.el.style.removeProperty("display"); } - - return sectionLabel; - }, - - /** - * Creates a PortalSectionView to display the content in the given portal - * section. Also creates a navigation link to the section. - * - * @param {PortalSectionModel} sectionModel - The section to render in this view - */ - addSection: function (sectionModel) { - - //If this is a visualization Section, render it differently with PortalVizSectionView - if (sectionModel.get("sectionType") == "visualization") { - this.addVizSection(sectionModel); - return; + if (MetacatUI.footerView) { + MetacatUI.footerView.show(); } - //All other portal section types are rendered with the basic PortalSectionView - else { - //Create a new PortalSectionView - var sectionView = new PortalSectionView({ - model: sectionModel - }); - - //Render the section - sectionView.render(); - - //Add the section view to this portal view - this.$("#portal-sections").append(sectionView.el); - - this.addSectionLink(sectionView); - - //Create a unique label for this section and save it - var uniqueLabel = this.getUniqueSectionLabel(sectionModel); - //Set the unique section label for this view - sectionView.uniqueSectionLabel = uniqueLabel; + } - this.subviews.push(sectionView); + if (!this.nodeView) { + //Update the location path with the new section name + this.updatePath(showSectionLabelInURL); + } + }, + + /** + * When a section link has been clicked, switch to that section + * @param {Event} e - The click event on the section link + */ + handleSwitchSection: function (e) { + e.preventDefault(); + + var sectionView = $(e.target) + .parents(this.sectionLinkContainer) + .first() + .data("view"); + + if (sectionView) { + this.switchSection(sectionView); + + // If the user clicks a link and is not near the top of the page + // (i.e. on mobile), scroll to the top of the section content. + // Otherwise it might look like the page hasn't changed (e.g. + // when focus is on the footer) + if (window.pageYOffset > this.$("#portal-sections").offset().top) { + MetacatUI.appView.scrollTo(this.$("#portal-sections")); } + } + }, + + /** + * Returns the section view that has a label matching the one given. + * @param {string} label - The label for the section + * @return {PortalSectionView|false} - Returns false if a matching section view isn't found + */ + getSectionByLabel: function (label) { + //If no label is given, exit + if (!label) { + return; + } - }, - - /** - * Creates a PortalSectionView to display the content in the given portal - * section. Also creates a navigation link to the section. - * @param {PortalVizSectionModel} sectionModel - The visualization section to render in this view - * - */ - addVizSection: function (sectionModel) { + //Find the section view whose unique label matches the given label. Case-insensitive matching. + return _.find(this.subviews, function (view) { + if (typeof view.uniqueSectionLabel == "string") { + return view.uniqueSectionLabel.toLowerCase() == label.toLowerCase(); + } else { + return false; + } + }); + }, + + /** + * Creates and returns a unique label for the given section. This label is just used in the view, + * because portal sections can have duplicate labels. But unique labels need to be used for navigation in the view. + * @param {PortEditorSection} sectionModel - The section for which to create a unique label + * @return {string} The unique label string + */ + getUniqueSectionLabel: function (sectionModel) { + //Get the label for this section + var sectionLabel = sectionModel + .get("label") + .replace(/[^a-zA-Z0-9 ]/g, "") + .replace(/ /g, "-"), + unalteredLabel = sectionLabel, + sectionLabels = this.sectionLabels || [], + i = 2; + + //Concatenate a number to the label if this one already exists + while (sectionLabels.includes(sectionLabel)) { + sectionLabel = unalteredLabel + i; + i++; + } + return sectionLabel; + }, + + /** + * Creates a PortalSectionView to display the content in the given portal + * section. Also creates a navigation link to the section. + * + * @param {PortalSectionModel} sectionModel - The section to render in this view + */ + addSection: function (sectionModel) { + //If this is a visualization Section, render it differently with PortalVizSectionView + if (sectionModel.get("sectionType") == "visualization") { + this.addVizSection(sectionModel); + return; + } + //All other portal section types are rendered with the basic PortalSectionView + else { //Create a new PortalSectionView - var sectionView = new PortalVisualizationsView({ - model: sectionModel + var sectionView = new PortalSectionView({ + model: sectionModel, }); //Render the section @@ -911,268 +944,321 @@

Source: src/js/views/portals/PortalView.js

//Create a unique label for this section and save it var uniqueLabel = this.getUniqueSectionLabel(sectionModel); - //Set the unique section label for this view sectionView.uniqueSectionLabel = uniqueLabel; this.subviews.push(sectionView); - - }, - - /** - * Add a link to a section of this portal page - * @param {PortalSectionView} sectionView - The view to add a link to - */ - addSectionLink: function (sectionView) { - - var label = sectionView.getName(); - var hrefLabel = sectionView.getName({ linkFriendly: true }); - - //Create a navigation link - this.$("#portal-section-tabs").append( - $(document.createElement("li")) - .addClass("section-link-container") - .data("view", sectionView) - .append($(document.createElement("a")) + } + }, + + /** + * Creates a PortalSectionView to display the content in the given portal + * section. Also creates a navigation link to the section. + * @param {PortalVizSectionModel} sectionModel - The visualization section to render in this view + * + */ + addVizSection: function (sectionModel) { + //Create a new PortalSectionView + var sectionView = new PortalVisualizationsView({ + model: sectionModel, + }); + + //Render the section + sectionView.render(); + + //Add the section view to this portal view + this.$("#portal-sections").append(sectionView.el); + + this.addSectionLink(sectionView); + + //Create a unique label for this section and save it + var uniqueLabel = this.getUniqueSectionLabel(sectionModel); + + //Set the unique section label for this view + sectionView.uniqueSectionLabel = uniqueLabel; + + this.subviews.push(sectionView); + }, + + /** + * Add a link to a section of this portal page + * @param {PortalSectionView} sectionView - The view to add a link to + */ + addSectionLink: function (sectionView) { + var label = sectionView.getName(); + var hrefLabel = sectionView.getName({ linkFriendly: true }); + + //Create a navigation link + this.$("#portal-section-tabs").append( + $(document.createElement("li")) + .addClass("section-link-container") + .data("view", sectionView) + .append( + $(document.createElement("a")) .text(label) .attr("href", "#" + hrefLabel) .attr("data-toggle", "tab") .addClass("portal-section-link") - .data("view", sectionView))); - - }, - - /** - * Handles the case where the PortalModel is fetched and nothing is found. - */ - handleNotFound: function () { - - var view = this; - - //If the user is NOT logged in OR - // if the user is logged in, and the last fetch was done with user credentials, then this Portal is either not accessible or non-existent - if (MetacatUI.appUserModel.get("checked") && !MetacatUI.appUserModel.get("loggedIn") || - (MetacatUI.appUserModel.get("checked") && MetacatUI.appUserModel.get("loggedIn") && this.model.get("fetchedWithAuth"))) { - - //Check if there is an indexing queue, because this model may still be indexing - var onError = function () { + .data("view", sectionView), + ), + ); + }, + + /** + * Handles the case where the PortalModel is fetched and nothing is found. + */ + handleNotFound: function () { + var view = this; + + //If the user is NOT logged in OR + // if the user is logged in, and the last fetch was done with user credentials, then this Portal is either not accessible or non-existent + if ( + (MetacatUI.appUserModel.get("checked") && + !MetacatUI.appUserModel.get("loggedIn")) || + (MetacatUI.appUserModel.get("checked") && + MetacatUI.appUserModel.get("loggedIn") && + this.model.get("fetchedWithAuth")) + ) { + //Check if there is an indexing queue, because this model may still be indexing + var onError = function () { //If the request to the monitor/status API fails, then show the not-found message view.showNotFound.call(view); }, - onSuccess = function (sizeOfQueue) { - - if (sizeOfQueue > 0) { - //Show a warning message about the index queue - MetacatUI.appView.showAlert( - "<p>We couldn't find a data portal named \" <span id='portal-view-not-found-name'></span>" + - "\".</p><p><i class='icon icon-exclamation-sign'></i> If this portal was created in the last few minutes, it may still be processing, since there are currently <b>" + sizeOfQueue + + onSuccess = function (sizeOfQueue) { + if (sizeOfQueue > 0) { + //Show a warning message about the index queue + MetacatUI.appView.showAlert( + "<p>We couldn't find a data portal named \" <span id='portal-view-not-found-name'></span>" + + "\".</p><p><i class='icon icon-exclamation-sign'></i> If this portal was created in the last few minutes, it may still be processing, since there are currently <b>" + + sizeOfQueue + "</b> submissions in the queue.</p>", - "alert-warning", - view.$el - ); - view.$(".loading").remove(); - - view.$("#portal-view-not-found-name").text(view.label || view.portalId); - } - else { - //If the size of the queue is 0, then show the not-found message - view.showNotFound.call(view); - } - + "alert-warning", + view.$el, + ); + view.$(".loading").remove(); + + view + .$("#portal-view-not-found-name") + .text(view.label || view.portalId); + } else { + //If the size of the queue is 0, then show the not-found message + view.showNotFound.call(view); } + }; - //Get the size of the index queue - MetacatUI.appLookupModel.getSizeOfIndexQueue(onSuccess, onError); - - } - //If the user IS logged in and we haven't fetched the model with user authentication yet - else if (MetacatUI.appUserModel.get("checked") && MetacatUI.appUserModel.get("loggedIn")) { - //Fetch again now that the user is logged in - this.model.fetch(); - } - //If the user login status is unknown, because authentication is still pending - else if (!MetacatUI.appUserModel.get("checked")) { - //Wait for the authentication to be checked, and then start this function over again - this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.handleNotFound); - } - - }, - - /** - * If the given portal doesn't exist, display a Not Found message. - */ - showNotFound: function () { - - var notFoundMessage = "The data portal \"<span id='portal-view-not-found-name'></span>" + + //Get the size of the index queue + MetacatUI.appLookupModel.getSizeOfIndexQueue(onSuccess, onError); + } + //If the user IS logged in and we haven't fetched the model with user authentication yet + else if ( + MetacatUI.appUserModel.get("checked") && + MetacatUI.appUserModel.get("loggedIn") + ) { + //Fetch again now that the user is logged in + this.model.fetch(); + } + //If the user login status is unknown, because authentication is still pending + else if (!MetacatUI.appUserModel.get("checked")) { + //Wait for the authentication to be checked, and then start this function over again + this.listenToOnce( + MetacatUI.appUserModel, + "change:checked", + this.handleNotFound, + ); + } + }, + + /** + * If the given portal doesn't exist, display a Not Found message. + */ + showNotFound: function () { + var notFoundMessage = + "The data portal \"<span id='portal-view-not-found-name'></span>" + "\" doesn't exist.", - notification = this.alertTemplate({ - classes: "alert-error", - msg: notFoundMessage, - includeEmail: true - }); + notification = this.alertTemplate({ + classes: "alert-error", + msg: notFoundMessage, + includeEmail: true, + }); - this.$el.html(notification); - - this.$("#portal-view-not-found-name").text(this.label || this.portalId); - - }, - - /** - * Show an error message in this view - * @param {SolrResult} model - * @param {XMLHttpRequest.response|string} reponse - */ - showError: function (model, response) { - - try { - var errorMsg = "", - errorClass = "alert-error", - icon = "frown", - portalTerm = MetacatUI.appModel.get("portalTermSingular") || "portal", - errorTitle = "Something went wrong displaying this " + portalTerm + "."; - - // For errors resulting from authorization errors, use a friendlier and more - // helpful error message than the default message returned from fetch - if (response && response.status == 401) { - errorTitle = 'You need permission to view this ' + portalTerm + "."; - errorClass = "alert-info"; - icon = "lock" - // Make a suggestion of how to fix the error based on whether the user is logged in or not. - if (!MetacatUI.appUserModel.get("loggedIn")) { - // If not logged in, suggest that the user signs in - errorMsg = '<strong><a href="' + - MetacatUI.appModel.get('signInUrlOrcid') + window.location.href + - '">Sign in</a></strong> to see if you have already been given access to view this ' + - portalTerm + '.'; - } else { - // If signed in, suggest that the user contacts that portal owner - errorMsg = "Contact the owner of this " + portalTerm + " to request access." - } - // For all other types of errors + this.$el.html(notification); + + this.$("#portal-view-not-found-name").text(this.label || this.portalId); + }, + + /** + * Show an error message in this view + * @param {SolrResult} model + * @param {XMLHttpRequest.response|string} reponse + */ + showError: function (model, response) { + try { + var errorMsg = "", + errorClass = "alert-error", + icon = "frown", + portalTerm = + MetacatUI.appModel.get("portalTermSingular") || "portal", + errorTitle = + "Something went wrong displaying this " + portalTerm + "."; + + // For errors resulting from authorization errors, use a friendlier and more + // helpful error message than the default message returned from fetch + if (response && response.status == 401) { + errorTitle = "You need permission to view this " + portalTerm + "."; + errorClass = "alert-info"; + icon = "lock"; + // Make a suggestion of how to fix the error based on whether the user is logged in or not. + if (!MetacatUI.appUserModel.get("loggedIn")) { + // If not logged in, suggest that the user signs in + errorMsg = + '<strong><a href="' + + MetacatUI.appModel.get("signInUrlOrcid") + + window.location.href + + '">Sign in</a></strong> to see if you have already been given access to view this ' + + portalTerm + + "."; } else { - if (response && response.responseText) { - errorMsg = "Error details: " + $(response.responseText).text(); - } - if (typeof response == "string") { - errorMsg = "Error details: " + response; - } - } - - if (errorMsg) { - errorMsg = "<p>" + errorMsg + "</p>" + // If signed in, suggest that the user contacts that portal owner + errorMsg = + "Contact the owner of this " + + portalTerm + + " to request access."; } - - //Show the error message - MetacatUI.appView.showAlert( - "<h4><i class='icon icon-" + icon + "'></i>" + errorTitle + "</h4>" + errorMsg, - errorClass + " portal-alert-container", - this.$el, - 0, - { includeEmail: true } - ); - - //Remove the loading message from this view - this.$el.find(".loading").remove(); - - } catch (error) { - console.log("There was a problem trying to display the error message in the Portal View. Error details: " + error); - } - - }, - - /** - * This function is called whenever the window is scrolled. - */ - handleScroll: function () { - var menu = $(".section-links-container")[0], - menuHeight = $(menu).height(), - hiddenHeight = (menuHeight * -1); - var currentScrollPos = window.pageYOffset; - if (MetacatUI.appView.prevScrollpos > currentScrollPos) { - //Get the height of any menu that may be displayed at the bottom of the page, too - - menu.style.bottom = "0px"; + // For all other types of errors } else { - menu.style.bottom = hiddenHeight + "px"; - } - MetacatUI.appView.prevScrollpos = currentScrollPos; - }, - - /** - * This function is called when the app navigates away from this view. - * Any clean-up or housekeeping happens at this time. - */ - onClose: function () { - - MetacatUI.appModel.resetTitle(); - MetacatUI.appModel.resetDescription(); - - // Run subView onClose functions if they exist - for (const subView of this.subviews) { - if (typeof subView?.onClose === "function") { - subView.onClose(); + if (response && response.responseText) { + errorMsg = "Error details: " + $(response.responseText).text(); + } + if (typeof response == "string") { + errorMsg = "Error details: " + response; } } - //Remove each subview from the DOM and remove listeners - _.invoke(this.subviews, "remove"); - - this.subviews = new Array(); + if (errorMsg) { + errorMsg = "<p>" + errorMsg + "</p>"; + } - // Remove any CSS that was added for the theme or layout - if (this.addedThemeCSS && this.addedThemeCSS.length) { - this.addedThemeCSS.forEach(function (cssID) { - MetacatUI.appModel.removeCSS(cssID); - }); + //Show the error message + MetacatUI.appView.showAlert( + "<h4><i class='icon icon-" + + icon + + "'></i>" + + errorTitle + + "</h4>" + + errorMsg, + errorClass + " portal-alert-container", + this.$el, + 0, + { includeEmail: true }, + ); + + //Remove the loading message from this view + this.$el.find(".loading").remove(); + } catch (error) { + console.log( + "There was a problem trying to display the error message in the Portal View. Error details: " + + error, + ); + } + }, + + /** + * This function is called whenever the window is scrolled. + */ + handleScroll: function () { + var menu = $(".section-links-container")[0], + menuHeight = $(menu).height(), + hiddenHeight = menuHeight * -1; + var currentScrollPos = window.pageYOffset; + if (MetacatUI.appView.prevScrollpos > currentScrollPos) { + //Get the height of any menu that may be displayed at the bottom of the page, too + + menu.style.bottom = "0px"; + } else { + menu.style.bottom = hiddenHeight + "px"; + } + MetacatUI.appView.prevScrollpos = currentScrollPos; + }, + + /** + * This function is called when the app navigates away from this view. + * Any clean-up or housekeeping happens at this time. + */ + onClose: function () { + MetacatUI.appModel.resetTitle(); + MetacatUI.appModel.resetDescription(); + + // Run subView onClose functions if they exist + for (const subView of this.subviews) { + if (typeof subView?.onClose === "function") { + subView.onClose(); } + } + + //Remove each subview from the DOM and remove listeners + _.invoke(this.subviews, "remove"); - //Remove all listeners - this.stopListening(); + this.subviews = new Array(); - //Reset the active alternate repository - //MetacatUI.appModel.set("activeAlternateRepositoryId", null); + // Remove any CSS that was added for the theme or layout + if (this.addedThemeCSS && this.addedThemeCSS.length) { + this.addedThemeCSS.forEach(function (cssID) { + MetacatUI.appModel.removeCSS(cssID); + }); + } - //Delete the metrics view from this view - delete this.sectionMetricsView; - //Delete the model from this view - delete this.model; + //Remove all listeners + this.stopListening(); - //Remove the scroll listener - $(window).off("scroll", "", this.handleScroll); + //Reset the active alternate repository + //MetacatUI.appModel.set("activeAlternateRepositoryId", null); - $("body").removeClass("PortalView"); + //Delete the metrics view from this view + delete this.sectionMetricsView; + //Delete the model from this view + delete this.model; - // Make sure the footer is visible (hidden for dataViz sections + panels layout) - MetacatUI.footerView.el.style.removeProperty('display') - document.body.style.removeProperty('--footer-height') + //Remove the scroll listener + $(window).off("scroll", "", this.handleScroll); - $("#editPortal").remove(); + $("body").removeClass("PortalView"); - this.undelegateEvents(); - }, + // Make sure the footer is visible (hidden for dataViz sections + panels layout) + MetacatUI.footerView.el.style.removeProperty("display"); + document.body.style.removeProperty("--footer-height"); - /** - * Checks if the label is a repository - * - * @param {string} username - The portal label or the member node repository identifier - */ - isNode: function (username) { + $("#editPortal").remove(); - if (username === undefined) { - this.showNotFound(); - return; - } - var model = this; - var node = _.find(MetacatUI.nodeModel.get("members"), function (nodeModel) { - return nodeModel.shortIdentifier.toLowerCase() == (username).toLowerCase(); - }); + this.undelegateEvents(); + }, - return (node && (node !== undefined)) + /** + * Checks if the label is a repository + * + * @param {string} username - The portal label or the member node repository identifier + */ + isNode: function (username) { + if (username === undefined) { + this.showNotFound(); + return; } - }); + var model = this; + var node = _.find( + MetacatUI.nodeModel.get("members"), + function (nodeModel) { + return ( + nodeModel.shortIdentifier.toLowerCase() == username.toLowerCase() + ); + }, + ); + + return node && node !== undefined; + }, + }, + ); - return PortalView; - }); + return PortalView; +});
diff --git a/docs/docs/src_js_views_portals_PortalVisualizationsView.js.html b/docs/docs/src_js_views_portals_PortalVisualizationsView.js.html index fc8b10db4..774fd4b99 100644 --- a/docs/docs/src_js_views_portals_PortalVisualizationsView.js.html +++ b/docs/docs/src_js_views_portals_PortalVisualizationsView.js.html @@ -44,213 +44,207 @@

Source: src/js/views/portals/PortalVisualizationsView.js<
-
define(["jquery",
-    "underscore",
-    "backbone",
-    "text!templates/portals/portalVisualizations.html",
-    "views/portals/PortalSectionView"],
-    function($, _, Backbone, PortalVisualizationsTemplate, PortalSectionView){
-
-    /**
-     * @class PortalVisualizationsView
-     * @classdesc The PortalVisualizationsView is a view to render the
-     * portal visualizations tab (within PortalSectionView)
-     * @classcategory Views/Portals
-     * @extends PortalSectionView
-     */
-     var PortalVisualizationsView = PortalSectionView.extend(
-       /** @lends PortalVisualizationsView.prototype */{
-
-       /**
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/portals/portalVisualizations.html",
+  "views/portals/PortalSectionView",
+], function ($, _, Backbone, PortalVisualizationsTemplate, PortalSectionView) {
+  /**
+   * @class PortalVisualizationsView
+   * @classdesc The PortalVisualizationsView is a view to render the
+   * portal visualizations tab (within PortalSectionView)
+   * @classcategory Views/Portals
+   * @extends PortalSectionView
+   */
+  var PortalVisualizationsView = PortalSectionView.extend(
+    /** @lends PortalVisualizationsView.prototype */ {
+      /**
        * The HTML classes to use for this view's element
        * @type {string}
        */
-        className: "portal-viz-section-view tab-pane portal-section-view",
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: "PortalVisualizations",
-
-        /**
-        * The PortalVizSectionModel
-        * @type {PortalVizSectionModel}
-        */
-        model: null,
-
-        /* The list of subview instances contained in this view*/
-        subviews: [], // Could be a literal object {}
-
-        /**
-        * Renders the compiled template into HTML
-        * @type {Underscore.Template}
-        */
-        template: _.template(PortalVisualizationsTemplate),
-
-        /**
-        * Construct a new instance of PortalVisualizationsView
-        * @param {Object} options - A literal object with options to pass to the view
-        */
-        initialize: function(options) {
-          // Get all the options and apply them to this view
-          if( typeof options == "object" ) {
-              var optionKeys = Object.keys(options);
-              _.each(optionKeys, function(key, i) {
-                  this[key] = options[key];
-              }, this);
-          }
-        },
-
-        /**
-        * Renders the view
-        */
-        render: function() {
+      className: "portal-viz-section-view tab-pane portal-section-view",
 
-          //Attach this view to the DOM element
-          this.$el.data("view", this);
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "PortalVisualizations",
 
-          // To speed up the portal load times, visualization content is rendered in the
-          // postRender function. This function is called only when a section is active. 
-          this.hasRendered = false;
+      /**
+       * The PortalVizSectionModel
+       * @type {PortalVizSectionModel}
+       */
+      model: null,
 
-        },
+      /* The list of subview instances contained in this view*/
+      subviews: [], // Could be a literal object {}
 
-        /**
-        * Renders a FEVer visualizattion in this view
-        */
-        renderFEVer: function(){
+      /**
+       * Renders the compiled template into HTML
+       * @type {Underscore.Template}
+       */
+      template: _.template(PortalVisualizationsTemplate),
 
-          //Exit if FEVer is disabled
-          if( !MetacatUI.appModel.get("enableFeverVisualizations") ){
-            return;
-          }
+      /**
+       * Construct a new instance of PortalVisualizationsView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        // Get all the options and apply them to this view
+        if (typeof options == "object") {
+          var optionKeys = Object.keys(options);
+          _.each(
+            optionKeys,
+            function (key, i) {
+              this[key] = options[key];
+            },
+            this,
+          );
+        }
+      },
 
-          //Insert the FEVer visualization into the page
-          var iframe = $(document.createElement("iframe"))
-                        .attr("src", MetacatUI.appModel.get("feverUrl"))
-                        .css("width", "100%");
-          this.$el.html(iframe);
+      /**
+       * Renders the view
+       */
+      render: function () {
+        //Attach this view to the DOM element
+        this.$el.data("view", this);
 
-        },
+        // To speed up the portal load times, visualization content is rendered in the
+        // postRender function. This function is called only when a section is active.
+        this.hasRendered = false;
+      },
 
-        /**
-        * Renders a {@link MapView} and inserts into this view
-        */
-        renderMap: function(){
+      /**
+       * Renders a FEVer visualizattion in this view
+       */
+      renderFEVer: function () {
+        //Exit if FEVer is disabled
+        if (!MetacatUI.appModel.get("enableFeverVisualizations")) {
+          return;
+        }
 
-          //Exit if Cesium is disabled
-          if( !MetacatUI.appModel.get("enableCesium") ){
-            return;
-          }
+        //Insert the FEVer visualization into the page
+        var iframe = $(document.createElement("iframe"))
+          .attr("src", MetacatUI.appModel.get("feverUrl"))
+          .css("width", "100%");
+        this.$el.html(iframe);
+      },
 
-          let thisView = this;
-
-          //Create a MapView and render it in this view
-           require(
-              ["views/maps/MapView", "models/maps/Map"],
-              function (MapView, Map) {
-                
-                let mapModel = thisView.model.get("mapModel")
-                if (!mapModel) {
-                  mapModel = new Map()
-                  thisView.model.set("mapModel", mapModel)
-                }
-                let mapView = new MapView({
-                  model: mapModel,
-                  isPortalMap: true,
-                });
-                thisView.$el.html(mapView.el);
-                mapView.render();
-              }
-           );
-
-        },
-
-        /**
-         * Function called by the PortalView when the section that contains this
-         * visualization becomes active (e.g. the user clicks on the section tab). Render
-         * the visualization and, if this is a fever viz, adjust the height.
-         */
-        postRender: function () {
-          try {
-            if (!this.hasRendered) {
-              if( this.model.get("visualizationType") == "fever" ){
-                this.renderFEVer();
-                this.hasRendered = true;
-              }
-              else if( this.model.get("visualizationType") == "cesium" ){
-                this.renderMap();
-                this.hasRendered = true;
-              }
-            }
-  
-            if( this.model.get("visualizationType") == "cesium" ){
-              document.body.style.setProperty("height", "100%");
-            }
-            else {
-              document.body.style.removeProperty("height");
-            }
+      /**
+       * Renders a {@link MapView} and inserts into this view
+       */
+      renderMap: function () {
+        //Exit if Cesium is disabled
+        if (!MetacatUI.appModel.get("enableCesium")) {
+          return;
+        }
 
-            if( this.model.get("visualizationType") == "fever" ){
-              $(window).resize(this.adjustVizHeight);
-              $(".auto-height-member").resize(this.adjustVizHeight);
-  
-              //Get the height of the visible part of the page for the iframe
-              this.adjustVizHeight();
-            }
+        let thisView = this;
+
+        //Create a MapView and render it in this view
+        require(["views/maps/MapView", "models/maps/Map"], function (
+          MapView,
+          Map,
+        ) {
+          let mapModel = thisView.model.get("mapModel");
+          if (!mapModel) {
+            mapModel = new Map();
+            thisView.model.set("mapModel", mapModel);
           }
-          catch (error) {
-            console.log(
-              'Failed to render the visualization in the PortalVisualizationsView ' +
-              'postRender function. Error details: ' + error
-            );
-          }
-        },
-
-        adjustVizHeight: function(){
-          // Get the heights of the header, navbar, and footer
-          var otherHeight = 0;
-          $(".auto-height-member").each(function(i, el) {
-              if ($(el).css("display") != "none" && !$(el).is("#Footer") ) {
-                  otherHeight += $(el).outerHeight(true);
-              }
+          let mapView = new MapView({
+            model: mapModel,
+            isPortalMap: true,
           });
-
-          // Get the remaining height left based on the window size
-          var remainingHeight = $(window).outerHeight(true) - otherHeight;
-          if (remainingHeight < 0){
-            remainingHeight = $(window).outerHeight(true) || 600;
-          }
-          else if (remainingHeight <= 120){
-            remainingHeight = ($(window).outerHeight(true) - remainingHeight) || 600;
+          thisView.$el.html(mapView.el);
+          mapView.render();
+        });
+      },
+
+      /**
+       * Function called by the PortalView when the section that contains this
+       * visualization becomes active (e.g. the user clicks on the section tab). Render
+       * the visualization and, if this is a fever viz, adjust the height.
+       */
+      postRender: function () {
+        try {
+          if (!this.hasRendered) {
+            if (this.model.get("visualizationType") == "fever") {
+              this.renderFEVer();
+              this.hasRendered = true;
+            } else if (this.model.get("visualizationType") == "cesium") {
+              this.renderMap();
+              this.hasRendered = true;
+            }
           }
 
-          this.$("iframe").css("height", remainingHeight + "px");
-        },
+          if (this.model.get("visualizationType") == "cesium") {
+            document.body.style.setProperty("height", "100%");
+          } else {
+            document.body.style.removeProperty("height");
+          }
 
-        /**
-         * Clean up
-         */
-        onClose: function() {
-          // Remove body height style tag
-          document.body.style.removeProperty("height");
+          if (this.model.get("visualizationType") == "fever") {
+            $(window).resize(this.adjustVizHeight);
+            $(".auto-height-member").resize(this.adjustVizHeight);
 
-          // this is failing for Cesium pages but might be necessary in FEVer ones
-          try {
-            $(window).removeListener("resize", this.adjustVizHeight);
+            //Get the height of the visible part of the page for the iframe
+            this.adjustVizHeight();
           }
-          catch (error) {
-            console.log(
-              'Failed to remove resize listener in portal viz onClose function. ' +
-              'Error details: ' + error
-            );
+        } catch (error) {
+          console.log(
+            "Failed to render the visualization in the PortalVisualizationsView " +
+              "postRender function. Error details: " +
+              error,
+          );
+        }
+      },
+
+      adjustVizHeight: function () {
+        // Get the heights of the header, navbar, and footer
+        var otherHeight = 0;
+        $(".auto-height-member").each(function (i, el) {
+          if ($(el).css("display") != "none" && !$(el).is("#Footer")) {
+            otherHeight += $(el).outerHeight(true);
           }
+        });
+
+        // Get the remaining height left based on the window size
+        var remainingHeight = $(window).outerHeight(true) - otherHeight;
+        if (remainingHeight < 0) {
+          remainingHeight = $(window).outerHeight(true) || 600;
+        } else if (remainingHeight <= 120) {
+          remainingHeight =
+            $(window).outerHeight(true) - remainingHeight || 600;
         }
 
-     });
+        this.$("iframe").css("height", remainingHeight + "px");
+      },
+
+      /**
+       * Clean up
+       */
+      onClose: function () {
+        // Remove body height style tag
+        document.body.style.removeProperty("height");
+
+        // this is failing for Cesium pages but might be necessary in FEVer ones
+        try {
+          $(window).removeListener("resize", this.adjustVizHeight);
+        } catch (error) {
+          console.log(
+            "Failed to remove resize listener in portal viz onClose function. " +
+              "Error details: " +
+              error,
+          );
+        }
+      },
+    },
+  );
 
-     return PortalVisualizationsView;
+  return PortalVisualizationsView;
 });
 
diff --git a/docs/docs/src_js_views_portals_PortalsSearchView.js.html b/docs/docs/src_js_views_portals_PortalsSearchView.js.html index 739bad68d..8c238faf6 100644 --- a/docs/docs/src_js_views_portals_PortalsSearchView.js.html +++ b/docs/docs/src_js_views_portals_PortalsSearchView.js.html @@ -44,124 +44,141 @@

Source: src/js/views/portals/PortalsSearchView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "collections/Filters",
-    "views/portals/PortalListView"],
-    function($, _, Backbone, Filters, PortalListView){
-
-      "use strict";
-
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/Filters",
+  "views/portals/PortalListView",
+], function ($, _, Backbone, Filters, PortalListView) {
+  "use strict";
+
+  /**
+   * @class PortalsSearchView
+   * @classdesc A view that shows a list of Portals in the main app window
+   * @classcategory Views/Portals
+   * @extends Backbone.View
+   * @constructor
+   * @since 2.16.0
+   * @screenshot views/portals/PortalsSearchView.png
+   */
+
+  var PortalsSearchView = Backbone.View.extend(
+    /** @lends PortalsSearchView.prototype */ {
       /**
-      * @class PortalsSearchView
-      * @classdesc A view that shows a list of Portals in the main app window
-      * @classcategory Views/Portals
-      * @extends Backbone.View
-      * @constructor
-      * @since 2.16.0
-      * @screenshot views/portals/PortalsSearchView.png
-      */
-
-      var PortalsSearchView = Backbone.View.extend(
-        /** @lends PortalsSearchView.prototype */{
-
-        /**
-        * The template for this view.
-        */
-        template: _.template('<div id="portals-list-container"><div id="portals-list-user"/> <div id="portals-list-all"/> </div>'),
-
-        /**
-        * Renders the list of portals
-        */
-        render: function(){
-
-          try{
-
-            //Set the header type
-            MetacatUI.appModel.set("headerType", "default");
-
-            //Insert the template
-            this.$el.html( this.template() );
-
-            //Render the view after the user's authentication has been checked
-            if( !MetacatUI.appUserModel.get("checked") ){
-              this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.render);
-              return;
-            }
-
-
-            let allPortalsView = new PortalListView();
-
-            //Create titles for the My Portals and All Portals sections
-            var title = $(document.createElement("h4"))
-                        .addClass("portals-list-title"),
-                allPortalsTitle = title.clone().text("All " + MetacatUI.appModel.get("portalTermPlural"));
-
-            // Filter datasets that the user has ownership of
-            if ( MetacatUI.appUserModel.get("loggedIn") ) {
-              let filters = new Filters();
-              filters.addWritePermissionFilter();
-
-              //Render My Portals list
-              let myPortalsView = new PortalListView();
-              myPortalsView.numPortals = 99999;
-              myPortalsView.numPortalsPerPage = 5;
-              myPortalsView.filters = filters;
-
-              //Create titles for the My Portals section
-              var myPortalsTitle = title.clone().text("My "  + MetacatUI.appModel.get("portalTermPlural"));
-
-              this.$("#portals-list-user").append(myPortalsTitle, myPortalsView.el);
-
-              myPortalsView.render();
+       * The template for this view.
+       */
+      template: _.template(
+        '<div id="portals-list-container"><div id="portals-list-user"/> <div id="portals-list-all"/> </div>',
+      ),
 
-              //Exclude portals the user is an owner of from the All portals list
-              let allPortalsFilters = new Filters();
-              allPortalsFilters.addWritePermissionFilter();
-              let permissionFilter = allPortalsFilters.at(allPortalsFilters.length-1);
-              if(permissionFilter){
-                permissionFilter.set("exclude", true);
-                allPortalsView.filters = allPortalsFilters;
-              }
+      /**
+       * Renders the list of portals
+       */
+      render: function () {
+        try {
+          //Set the header type
+          MetacatUI.appModel.set("headerType", "default");
+
+          //Insert the template
+          this.$el.html(this.template());
+
+          //Render the view after the user's authentication has been checked
+          if (!MetacatUI.appUserModel.get("checked")) {
+            this.listenToOnce(
+              MetacatUI.appUserModel,
+              "change:checked",
+              this.render,
+            );
+            return;
+          }
 
+          let allPortalsView = new PortalListView();
+
+          //Create titles for the My Portals and All Portals sections
+          var title = $(document.createElement("h4")).addClass(
+              "portals-list-title",
+            ),
+            allPortalsTitle = title
+              .clone()
+              .text("All " + MetacatUI.appModel.get("portalTermPlural"));
+
+          // Filter datasets that the user has ownership of
+          if (MetacatUI.appUserModel.get("loggedIn")) {
+            let filters = new Filters();
+            filters.addWritePermissionFilter();
+
+            //Render My Portals list
+            let myPortalsView = new PortalListView();
+            myPortalsView.numPortals = 99999;
+            myPortalsView.numPortalsPerPage = 5;
+            myPortalsView.filters = filters;
+
+            //Create titles for the My Portals section
+            var myPortalsTitle = title
+              .clone()
+              .text("My " + MetacatUI.appModel.get("portalTermPlural"));
+
+            this.$("#portals-list-user").append(
+              myPortalsTitle,
+              myPortalsView.el,
+            );
+
+            myPortalsView.render();
+
+            //Exclude portals the user is an owner of from the All portals list
+            let allPortalsFilters = new Filters();
+            allPortalsFilters.addWritePermissionFilter();
+            let permissionFilter = allPortalsFilters.at(
+              allPortalsFilters.length - 1,
+            );
+            if (permissionFilter) {
+              permissionFilter.set("exclude", true);
+              allPortalsView.filters = allPortalsFilters;
             }
-
-            //Render All Portals
-            allPortalsView.numPortals = 99999;
-            allPortalsView.numPortalsPerPage = 10;
-            allPortalsView.createBtnContainer = "#none";
-            allPortalsView.noResultsMessage = "There are no " + MetacatUI.appModel.get("portalTermPlural") + " to show.";
-
-            this.$("#portals-list-all").append(allPortalsTitle, allPortalsView.el);
-
-            allPortalsView.render();
-
-          }
-          catch(e){
-            console.error(e);
-            this.showError();
           }
-        },
-
-        /**
-        * Displays an error message when rendering this view has failed.
-        */
-        showError: function(){
 
-          //Remove the loading elements
-          this.$(".loading").remove();
-
-          //Show an error message
-          MetacatUI.appView.showAlert(
-            "Something went wrong while getting this list of " + MetacatUI.appModel.get("portalTermPlural") + ".",
-            "alert-error",
-            this.$el);
+          //Render All Portals
+          allPortalsView.numPortals = 99999;
+          allPortalsView.numPortalsPerPage = 10;
+          allPortalsView.createBtnContainer = "#none";
+          allPortalsView.noResultsMessage =
+            "There are no " +
+            MetacatUI.appModel.get("portalTermPlural") +
+            " to show.";
+
+          this.$("#portals-list-all").append(
+            allPortalsTitle,
+            allPortalsView.el,
+          );
+
+          allPortalsView.render();
+        } catch (e) {
+          console.error(e);
+          this.showError();
         }
+      },
 
-        });
-      return PortalsSearchView;
-  });
+      /**
+       * Displays an error message when rendering this view has failed.
+       */
+      showError: function () {
+        //Remove the loading elements
+        this.$(".loading").remove();
+
+        //Show an error message
+        MetacatUI.appView.showAlert(
+          "Something went wrong while getting this list of " +
+            MetacatUI.appModel.get("portalTermPlural") +
+            ".",
+          "alert-error",
+          this.$el,
+        );
+      },
+    },
+  );
+  return PortalsSearchView;
+});
 
diff --git a/docs/docs/src_js_views_portals_editor_PortEditorDataView.js.html b/docs/docs/src_js_views_portals_editor_PortEditorDataView.js.html index fe0b7bdb8..1aa7fe00b 100644 --- a/docs/docs/src_js_views_portals_editor_PortEditorDataView.js.html +++ b/docs/docs/src_js_views_portals_editor_PortEditorDataView.js.html @@ -44,165 +44,173 @@

Source: src/js/views/portals/editor/PortEditorDataView.js
-
define(['underscore',
-        'jquery',
-        'backbone',
-        "models/filters/FilterGroup",
-        "views/portals/editor/PortEditorSectionView",
-        "views/EditCollectionView",
-        "views/filters/FilterGroupsView",
-        "text!templates/portals/editor/portEditorData.html",
-      ],
-function( _, $, Backbone, FilterGroup, PortEditorSectionView, EditCollectionView,
-  FilterGroupsView, Template ){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/filters/FilterGroup",
+  "views/portals/editor/PortEditorSectionView",
+  "views/EditCollectionView",
+  "views/filters/FilterGroupsView",
+  "text!templates/portals/editor/portEditorData.html",
+], function (
+  _,
+  $,
+  Backbone,
+  FilterGroup,
+  PortEditorSectionView,
+  EditCollectionView,
+  FilterGroupsView,
+  Template,
+) {
   /**
-  * @class PortEditorDataView
-  * @classcategory Views/Portals/Editor
-  * @extends PortEditorSectionView
-  */
+   * @class PortEditorDataView
+   * @classcategory Views/Portals/Editor
+   * @extends PortEditorSectionView
+   */
   var PortEditorDataView = PortEditorSectionView.extend(
-    /** @lends PortEditorDataView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "PortEditorData",
-
-    /**
-    * The display name for this Section
-    * @type {string}
-    */
-    uniqueSectionLabel: "Data",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: PortEditorSectionView.prototype.className + " port-editor-data",
-
-    /**
-     * A reference to the parent editor view
-     * @type {PortalEditorView}
-     */
-    editorView: undefined,
-
-    /**
-    * The id attribute of the view element
-    * @param {string}
-    */
-    id: "Data",
-
-    /**
-    * The PortalModel that is being edited
-    * @type {Portal}
-    */
-    model: undefined,
-
-    /**
-    * The type of section view this is
-    * @type {string}
-    */
-    sectionType: "data",
-
-    /**
-    * A jQuery selector for the element that the EditCollectionView should be inserted into
-    * @type {string}
-    */
-    editCollectionViewContainer: ".edit-collection-container",
-
-    /**
-    * A jQuery selector for the element that the FilterGroupsView editor should be
-    * inserted into
-    * @type {string}
-    * @since 2.17.0
-    */
-    editFilterGroupsViewContainer: ".edit-filter-groups-container",
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    */
-    template: _.template(Template),
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events:{
-      // Open dataset links in new tab so user can keep editing their portal
-      "click a.route-to-metadata": "openInNewTab"
-    },
-
-    /**
-    * Creates a new PortEditorDataView
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options){
-      //Call the superclass initialize() function
-      PortEditorSectionView.prototype.initialize.call(this, options);
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-      //Insert the template into the view
-      this.$el.html(this.template());
-
-      // render EditCollectionView
-      var editCollectionView = new EditCollectionView({
-        model: this.model,
-      });
-      this.$(this.editCollectionViewContainer).html(editCollectionView.el);
-      editCollectionView.render();
-
-      // render the view the edit the custom portal search filters
-      // TODO: only render the filterGroupsView if there is a data collection already?
-
-      // Make sure there is at least one empty filter group view to render
-      if (!this.model.get("filterGroups") || this.model.get("filterGroups").length == 0){
-        this.model.set("filterGroups", [new FilterGroup({
-          label: "Search",
-          isUIFilterType: true
-        })])
-      }
-
-      var filterGroupsView = new FilterGroupsView({
-        filterGroups: this.model.get("filterGroups"),
-        edit: true,
-        editorView: this.editorView
-      });
-      this.$(this.editFilterGroupsViewContainer).html(filterGroupsView.el);
-      filterGroupsView.render();
-
-      //Save a reference to this view
-      this.$el.data("view", this);
-
+    /** @lends PortEditorDataView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "PortEditorData",
+
+      /**
+       * The display name for this Section
+       * @type {string}
+       */
+      uniqueSectionLabel: "Data",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className:
+        PortEditorSectionView.prototype.className + " port-editor-data",
+
+      /**
+       * A reference to the parent editor view
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * The id attribute of the view element
+       * @param {string}
+       */
+      id: "Data",
+
+      /**
+       * The PortalModel that is being edited
+       * @type {Portal}
+       */
+      model: undefined,
+
+      /**
+       * The type of section view this is
+       * @type {string}
+       */
+      sectionType: "data",
+
+      /**
+       * A jQuery selector for the element that the EditCollectionView should be inserted into
+       * @type {string}
+       */
+      editCollectionViewContainer: ".edit-collection-container",
+
+      /**
+       * A jQuery selector for the element that the FilterGroupsView editor should be
+       * inserted into
+       * @type {string}
+       * @since 2.17.0
+       */
+      editFilterGroupsViewContainer: ".edit-filter-groups-container",
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      template: _.template(Template),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        // Open dataset links in new tab so user can keep editing their portal
+        "click a.route-to-metadata": "openInNewTab",
+      },
+
+      /**
+       * Creates a new PortEditorDataView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        //Call the superclass initialize() function
+        PortEditorSectionView.prototype.initialize.call(this, options);
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        //Insert the template into the view
+        this.$el.html(this.template());
+
+        // render EditCollectionView
+        var editCollectionView = new EditCollectionView({
+          model: this.model,
+        });
+        this.$(this.editCollectionViewContainer).html(editCollectionView.el);
+        editCollectionView.render();
+
+        // render the view the edit the custom portal search filters
+        // TODO: only render the filterGroupsView if there is a data collection already?
+
+        // Make sure there is at least one empty filter group view to render
+        if (
+          !this.model.get("filterGroups") ||
+          this.model.get("filterGroups").length == 0
+        ) {
+          this.model.set("filterGroups", [
+            new FilterGroup({
+              label: "Search",
+              isUIFilterType: true,
+            }),
+          ]);
+        }
+
+        var filterGroupsView = new FilterGroupsView({
+          filterGroups: this.model.get("filterGroups"),
+          edit: true,
+          editorView: this.editorView,
+        });
+        this.$(this.editFilterGroupsViewContainer).html(filterGroupsView.el);
+        filterGroupsView.render();
+
+        //Save a reference to this view
+        this.$el.data("view", this);
+      },
+
+      /**
+       * Opens a link in a new tab even when the target=_blank attribute isn't set.
+       * Link is assumed to be relative; the base url is prepended to make it absolute.
+       * @param {Event} e - The click event on an HTML achor element
+       */
+      openInNewTab: function (e) {
+        try {
+          e.preventDefault();
+          e.stopPropagation();
+          var url = MetacatUI.root + $(e.currentTarget).attr("href");
+          window.open(url, "_blank");
+        } catch (error) {
+          "Failed to open data link in new window, error message: " + error;
+        }
+      },
     },
-
-    /**
-    * Opens a link in a new tab even when the target=_blank attribute isn't set.
-    * Link is assumed to be relative; the base url is prepended to make it absolute.
-    * @param {Event} e - The click event on an HTML achor element
-    */
-    openInNewTab: function(e){
-      try{
-        e.preventDefault();
-        e.stopPropagation();
-        var url = MetacatUI.root + $(e.currentTarget).attr('href');
-        window.open(url, '_blank');
-      }
-      catch(error){
-        "Failed to open data link in new window, error message: " + error
-      }
-    }
-
-  });
+  );
 
   return PortEditorDataView;
-
 });
 
diff --git a/docs/docs/src_js_views_portals_editor_PortEditorImageView.js.html b/docs/docs/src_js_views_portals_editor_PortEditorImageView.js.html index d2f836e3e..bf4c1ebb7 100644 --- a/docs/docs/src_js_views_portals_editor_PortEditorImageView.js.html +++ b/docs/docs/src_js_views_portals_editor_PortEditorImageView.js.html @@ -44,425 +44,429 @@

Source: src/js/views/portals/editor/PortEditorImageView.j
-
define(['underscore',
-        'jquery',
-        'backbone',
-        "models/portals/PortalImage",
-        "views/ImageUploaderView",
-        "text!templates/imageEdit.html"],
-function(_, $, Backbone, PortalImage, ImageUploaderView, Template){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/portals/PortalImage",
+  "views/ImageUploaderView",
+  "text!templates/imageEdit.html",
+], function (_, $, Backbone, PortalImage, ImageUploaderView, Template) {
   /**
-  * @class PortEditorImageView
-  * @classdesc A view that allows the user to upload an image as a DataONEObject
-  * @classcategory Views/Portals/Editor
-  * @extends Backbone.View
-  */
+   * @class PortEditorImageView
+   * @classdesc A view that allows the user to upload an image as a DataONEObject
+   * @classcategory Views/Portals/Editor
+   * @extends Backbone.View
+   */
   var PortEditorImageView = Backbone.View.extend(
-      /** @lends PortEditorImageView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "PortEditorImage",
-
-    /**
-    * The HTML tag name to use for this view's element
-    * @type {string}
-    */
-    tagName: "div",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "edit-image",
-
-    /**
-    * A jQuery selector for the element that the ImageUploaderView should be inserted into
-    * @type {string}
-    */
-    imageUploaderContainer: ".image-uploader-container",
-
-    /**
-     * The ImageUploaderView created and used by this ImageEdit view.
-     * @type {ImageUploader}
-     */
-    uploader: undefined,
-
-    /**
-    * The PortalImage model that is being edited
-    * @type {Image}
-    */
-    model: undefined,
-
-    /**
-    * The Portal model that contains the PortalImage
-    * @type {Portal}
-    */
-    parentModel: undefined,
-
-    /**
-    * A reference to the PortalEditorView
-    * @type {PortalEditorView}
-    */
-    editorView: undefined,
-
-    /**
-    * The maximum height of the image preview. If set to false,
-    * no css width property is set.
-    * @type {number}
-    */
-    imageHeight: 150,
-
-    /**
-    * The display width of the image preview. If set to false,
-    * no css width property is set.
-    * @type {number|boolean}
-    */
-    imageWidth: 150,
-
-    /**
-     * The minimum required height of the image file. If set, the uploader will
-     * reject images that are shorter than this. If null, any image height is
-     * accepted.
-     * @type {number}
-     */
-    minHeight: null,
-
-    /**
-     * The minimum required height of the image file. If set, the uploader will
-     * reject images that are shorter than this. If null, any image height is
-     * accepted.
-     * @type {number}
-     */
-    minWidth: null,
-
-    /**
-     * The maximum height for uploaded files. If a file is taller than this, it
-     * will be resized without warning before being uploaded. If set to null,
-     * the image won't be resized based on height (but might be depending on
-     * maxWidth).
-     * @type {number}
-     */
-    maxHeight: null,
-
-    /**
-     * The maximum width for uploaded files. If a file is wider than this, it
-     * will be resized without warning before being uploaded. If set to null,
-     * the image won't be resized based on width (but might be depending on
-     * maxHeight).
-     * @type {number}
-     */
-    maxWidth: null,
-
-
-    /**
-    * Text to instruct the user how to upload an image
-    * @type {string[]}
-    */
-    imageUploadInstructions: ["Drag & drop an image or click here to upload"],
-
-    /**
-     * Label for the first text input where the user enters the ImageModel label.
-     * If this is set to false, the label input will not be shown.
-     * @type {string|boolean}
-     */
-    nameLabel: "Name",
-
-    /**
-     * Label for the second text input where the user enters the ImageModel
-     * associated URL. If this is set to false, the URL input will not be shown.
-     * @type {string|boolean}
-     */
-    urlLabel: "URL",
-
-    /**
-     * The HTML tag name to insert the uploaded image into. Options are "img",
-     * in which case the image is inserted as an HTML <img>, or "div", in which
-     * case the image is inserted as the background of a "div".
-     * @type {string}
-     */
-    imageTagName: "div",
-
-    /**
-     * Whether or not a remove button should be shown.
-     * @type {boolean}
-     */
-    removeButton: false,
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    */
-    template: _.template(Template),
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "mouseover .toggle-remove-preview"    : "showRemovePreview",
-      "mouseout  .toggle-remove-preview"    : "hideRemovePreview",
-      "click .remove-image-edit-view"       : "removeSelf",
-      "focusout .basic-text"                : "redoValidation"
-    },
-
-    /**
-    * Creates a new PortEditorImageView
-    * @param {Object} options - A literal object with options to pass to the view
-    * @property {Portal}  options.parentModel - Gets set as PortEditorImageView.parentModel
-    * @property {PortalEditorView}  options.editorView - Gets set as PortEditorImageView.editorView
-    * @property {PortalImage}  options.model - Gets set as PortEditorImageView.model
-    * @property {string[]}  options.imageUploadInstructions - Gets set as ImageUploaderView.imageUploadInstructions
-    * @property {string}  options.nameLabel - Gets set as PortEditorImageView.nameLabel
-    * @property {string}  options.urlLabel - Gets set as PortEditorImageView.urlLabel
-    * @property {string}  options.imageTagName - Gets set as ImageUploaderView.imageTagName
-    * @property {string}  options.removeButton - Gets set as ImageUploaderView.removeButton
-    * @property {number}  options.imageWidth - Gets set as ImageUploaderView.width
-    * @property {number}  options.imageHeight - Gets set as ImageUploaderView.height
-    * @property {number}  options.minWidth - Gets set as ImageUploaderView.minWidth
-    * @property {number}  options.minHeight - Gets set as ImageUploaderView.minHeight
-    * @property {number}  options.maxWidth - Gets set as ImageUploaderView.maxWidth
-    * @property {number}  options.maxHeight - Gets set as ImageUploaderView.maxHeight
-    */
-    initialize: function(options){
-
-      try {
-
-        if( typeof options == "object" ){
-          this.parentModel              = options.parentModel;
-          this.editorView               = options.editorView;
-          this.model                    = options.model;
-          this.imageUploadInstructions  = options.imageUploadInstructions;
-          this.imageWidth               = options.imageWidth;
-          this.imageHeight              = options.imageHeight;
-          this.nameLabel                = options.nameLabel;
-          this.urlLabel                 = options.urlLabel;
-          this.imageTagName             = options.imageTagName;
-          this.removeButton             = options.removeButton;
-          this.minHeight                = options.minHeight;
-          this.minWidth                 = options.minWidth;
-          this.maxHeight                = options.maxHeight;
-          this.maxWidth                 = options.maxWidth;
-        }
-
-        if(!this.model){
-
-          this.model = new PortalImage();
-        }
-
-        //If an alternative repo is configured, upload to that alt repo
-        let useAltRepo = MetacatUI.appModel.getActiveAltRepo()? true : false;
-        this.model.set("useAltRepo", useAltRepo);
-
-      } catch (e) {
-        console.log("PortEditorImageView failed to initialize. Error message: " + e);
-      }
-
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-      try {
-        // Reference to this view
-        var view = this;
-
-        //Insert the template for this view
-        this.$el.html(this.template({
-          nameLabel:    this.nameLabel,
-          urlLabel:     this.urlLabel,
-          nameText:     this.model.get("label"),
-          urlText:      this.model.get("associatedURL"),
-          removeButton: this.removeButton
-        }));
-
-        // Create an ImageUploaderView and insert into this view. Allow it to be
-        // accessed from parent views.
-        this.uploader = new ImageUploaderView({
-          model:              this.model,
-          url:                this.model.get("imageURL"),
-          uploadInstructions: this.imageUploadInstructions,
-          imageTagName:       this.imageTagName,
-          height:             this.imageHeight,
-          width:              this.imageWidth,
-          minHeight:          this.minHeight,
-          minWidth:           this.minWidth,
-          maxHeight:          this.maxHeight,
-          maxWidth:           this.maxWidth
-        });
-        this.$(this.imageUploaderContainer).append(this.uploader.el);
-        this.uploader.render();
-
-        // Reset image attributes when user removes image
-        this.stopListening(this.uploader, "removedfile");
-        this.listenTo(this.uploader, "removedfile", function(){
-          var defaults = view.model.defaults();
-          view.model.set("identifier", defaults.identifier);
-          view.model.set("imageURL", defaults.imageURL);
-          view.redoValidation();
-        });
-
-        // Try to validate again when image is added but not yet uploaded
-        this.stopListening(this.uploader, "addedfile");
-        this.listenTo(this.uploader, "addedfile", function(){
-          view.redoValidation();
-        });
-
-        // Update the PortalImage model when the image is successfully uploaded
-        this.stopListening(this.uploader, "successSaving");
-        this.listenTo(this.uploader, "successSaving", function(dataONEObject){
-          view.model.set("identifier", dataONEObject.get("id"));
-          view.model.set("imageURL", dataONEObject.url());
-          view.redoValidation();
-        });
-
-        this.listenTo(this.model, "change:associatedURL", this.showValidation);
-
-        // Allows model to update when user types in text field
-        this.$el.find(".basic-text").data({ model: this.model, view: this });
-
-        //Initialize any tooltips
-        this.$(".tooltip-this").tooltip();
-
-        //Save a reference to this view
-        this.$el.data("view", this);
-
-      } catch (e) {
-        console.log("ImageEdit view not rendered, error message: " + e);
-      }
-
-    },
-
-    /**
-     * removeSelf - Removes this ImageEdit view and the associated PortalImage
-     * model from the parent Portal model.
-     */
-    removeSelf: function(){
-
-      try {
-
-        var view = this;
-
-        // Remove the model
-        this.parentModel.removePortalImage(this.model);
-        // Remove the view
-        this.$el.animate({width: "0px", overflow: "hidden"}, {
-          duration: 250,
-          complete: function(){
-            view.onClose();
-            view.remove();
+    /** @lends PortEditorImageView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "PortEditorImage",
+
+      /**
+       * The HTML tag name to use for this view's element
+       * @type {string}
+       */
+      tagName: "div",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "edit-image",
+
+      /**
+       * A jQuery selector for the element that the ImageUploaderView should be inserted into
+       * @type {string}
+       */
+      imageUploaderContainer: ".image-uploader-container",
+
+      /**
+       * The ImageUploaderView created and used by this ImageEdit view.
+       * @type {ImageUploader}
+       */
+      uploader: undefined,
+
+      /**
+       * The PortalImage model that is being edited
+       * @type {Image}
+       */
+      model: undefined,
+
+      /**
+       * The Portal model that contains the PortalImage
+       * @type {Portal}
+       */
+      parentModel: undefined,
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * The maximum height of the image preview. If set to false,
+       * no css width property is set.
+       * @type {number}
+       */
+      imageHeight: 150,
+
+      /**
+       * The display width of the image preview. If set to false,
+       * no css width property is set.
+       * @type {number|boolean}
+       */
+      imageWidth: 150,
+
+      /**
+       * The minimum required height of the image file. If set, the uploader will
+       * reject images that are shorter than this. If null, any image height is
+       * accepted.
+       * @type {number}
+       */
+      minHeight: null,
+
+      /**
+       * The minimum required height of the image file. If set, the uploader will
+       * reject images that are shorter than this. If null, any image height is
+       * accepted.
+       * @type {number}
+       */
+      minWidth: null,
+
+      /**
+       * The maximum height for uploaded files. If a file is taller than this, it
+       * will be resized without warning before being uploaded. If set to null,
+       * the image won't be resized based on height (but might be depending on
+       * maxWidth).
+       * @type {number}
+       */
+      maxHeight: null,
+
+      /**
+       * The maximum width for uploaded files. If a file is wider than this, it
+       * will be resized without warning before being uploaded. If set to null,
+       * the image won't be resized based on width (but might be depending on
+       * maxHeight).
+       * @type {number}
+       */
+      maxWidth: null,
+
+      /**
+       * Text to instruct the user how to upload an image
+       * @type {string[]}
+       */
+      imageUploadInstructions: ["Drag & drop an image or click here to upload"],
+
+      /**
+       * Label for the first text input where the user enters the ImageModel label.
+       * If this is set to false, the label input will not be shown.
+       * @type {string|boolean}
+       */
+      nameLabel: "Name",
+
+      /**
+       * Label for the second text input where the user enters the ImageModel
+       * associated URL. If this is set to false, the URL input will not be shown.
+       * @type {string|boolean}
+       */
+      urlLabel: "URL",
+
+      /**
+       * The HTML tag name to insert the uploaded image into. Options are "img",
+       * in which case the image is inserted as an HTML <img>, or "div", in which
+       * case the image is inserted as the background of a "div".
+       * @type {string}
+       */
+      imageTagName: "div",
+
+      /**
+       * Whether or not a remove button should be shown.
+       * @type {boolean}
+       */
+      removeButton: false,
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      template: _.template(Template),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "mouseover .toggle-remove-preview": "showRemovePreview",
+        "mouseout  .toggle-remove-preview": "hideRemovePreview",
+        "click .remove-image-edit-view": "removeSelf",
+        "focusout .basic-text": "redoValidation",
+      },
+
+      /**
+       * Creates a new PortEditorImageView
+       * @param {Object} options - A literal object with options to pass to the view
+       * @property {Portal}  options.parentModel - Gets set as PortEditorImageView.parentModel
+       * @property {PortalEditorView}  options.editorView - Gets set as PortEditorImageView.editorView
+       * @property {PortalImage}  options.model - Gets set as PortEditorImageView.model
+       * @property {string[]}  options.imageUploadInstructions - Gets set as ImageUploaderView.imageUploadInstructions
+       * @property {string}  options.nameLabel - Gets set as PortEditorImageView.nameLabel
+       * @property {string}  options.urlLabel - Gets set as PortEditorImageView.urlLabel
+       * @property {string}  options.imageTagName - Gets set as ImageUploaderView.imageTagName
+       * @property {string}  options.removeButton - Gets set as ImageUploaderView.removeButton
+       * @property {number}  options.imageWidth - Gets set as ImageUploaderView.width
+       * @property {number}  options.imageHeight - Gets set as ImageUploaderView.height
+       * @property {number}  options.minWidth - Gets set as ImageUploaderView.minWidth
+       * @property {number}  options.minHeight - Gets set as ImageUploaderView.minHeight
+       * @property {number}  options.maxWidth - Gets set as ImageUploaderView.maxWidth
+       * @property {number}  options.maxHeight - Gets set as ImageUploaderView.maxHeight
+       */
+      initialize: function (options) {
+        try {
+          if (typeof options == "object") {
+            this.parentModel = options.parentModel;
+            this.editorView = options.editorView;
+            this.model = options.model;
+            this.imageUploadInstructions = options.imageUploadInstructions;
+            this.imageWidth = options.imageWidth;
+            this.imageHeight = options.imageHeight;
+            this.nameLabel = options.nameLabel;
+            this.urlLabel = options.urlLabel;
+            this.imageTagName = options.imageTagName;
+            this.removeButton = options.removeButton;
+            this.minHeight = options.minHeight;
+            this.minWidth = options.minWidth;
+            this.maxHeight = options.maxHeight;
+            this.maxWidth = options.maxWidth;
           }
-        });
-
-      } catch (e) {
-        console.log("Failed to remove an ImageEdit view. Error message: " + e);
-      }
-
-    },
-
-    /**
-     * redoValidation - Called when a user focuses out of input fields
-     * with the .basic-text class (organization name and associated URL), or
-     * when an image is successfully uploaded or removed. This function
-     * validates the PortalImage model again and shows errors if there are any.
-     */
-    redoValidation: function(){
-      try {
-        view = this;
-        // Add a small pause so that the model is updated first.
-        setTimeout(function () {
-          view.removeValidation();
-          view.showValidation();
-        }, 1);
-      } catch (e) {
-        console.log(e);
-      }
-    },
 
-    /**
-     * showValidation - show validation errors for this ImageEdit view
-     */
-    showValidation: function(){
-
-      try {
-
-        var errors = this.model.validate();
-
-        if(errors){
+          if (!this.model) {
+            this.model = new PortalImage();
+          }
 
-          _.each(errors, function(errorMsg, category){
-            var categoryEls = this.$("[data-category='" + category + "']");
-            //Use the showValidationMessage function from the parent view
-            if( this.editorView && this.editorView.showValidationMessage ){
-              this.editorView.showValidationMessage(categoryEls, errorMsg);
+          //If an alternative repo is configured, upload to that alt repo
+          let useAltRepo = MetacatUI.appModel.getActiveAltRepo() ? true : false;
+          this.model.set("useAltRepo", useAltRepo);
+        } catch (e) {
+          console.log(
+            "PortEditorImageView failed to initialize. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        try {
+          // Reference to this view
+          var view = this;
+
+          //Insert the template for this view
+          this.$el.html(
+            this.template({
+              nameLabel: this.nameLabel,
+              urlLabel: this.urlLabel,
+              nameText: this.model.get("label"),
+              urlText: this.model.get("associatedURL"),
+              removeButton: this.removeButton,
+            }),
+          );
+
+          // Create an ImageUploaderView and insert into this view. Allow it to be
+          // accessed from parent views.
+          this.uploader = new ImageUploaderView({
+            model: this.model,
+            url: this.model.get("imageURL"),
+            uploadInstructions: this.imageUploadInstructions,
+            imageTagName: this.imageTagName,
+            height: this.imageHeight,
+            width: this.imageWidth,
+            minHeight: this.minHeight,
+            minWidth: this.minWidth,
+            maxHeight: this.maxHeight,
+            maxWidth: this.maxWidth,
+          });
+          this.$(this.imageUploaderContainer).append(this.uploader.el);
+          this.uploader.render();
+
+          // Reset image attributes when user removes image
+          this.stopListening(this.uploader, "removedfile");
+          this.listenTo(this.uploader, "removedfile", function () {
+            var defaults = view.model.defaults();
+            view.model.set("identifier", defaults.identifier);
+            view.model.set("imageURL", defaults.imageURL);
+            view.redoValidation();
+          });
+
+          // Try to validate again when image is added but not yet uploaded
+          this.stopListening(this.uploader, "addedfile");
+          this.listenTo(this.uploader, "addedfile", function () {
+            view.redoValidation();
+          });
+
+          // Update the PortalImage model when the image is successfully uploaded
+          this.stopListening(this.uploader, "successSaving");
+          this.listenTo(
+            this.uploader,
+            "successSaving",
+            function (dataONEObject) {
+              view.model.set("identifier", dataONEObject.get("id"));
+              view.model.set("imageURL", dataONEObject.url());
+              view.redoValidation();
+            },
+          );
+
+          this.listenTo(
+            this.model,
+            "change:associatedURL",
+            this.showValidation,
+          );
+
+          // Allows model to update when user types in text field
+          this.$el.find(".basic-text").data({ model: this.model, view: this });
+
+          //Initialize any tooltips
+          this.$(".tooltip-this").tooltip();
+
+          //Save a reference to this view
+          this.$el.data("view", this);
+        } catch (e) {
+          console.log("ImageEdit view not rendered, error message: " + e);
+        }
+      },
+
+      /**
+       * removeSelf - Removes this ImageEdit view and the associated PortalImage
+       * model from the parent Portal model.
+       */
+      removeSelf: function () {
+        try {
+          var view = this;
+
+          // Remove the model
+          this.parentModel.removePortalImage(this.model);
+          // Remove the view
+          this.$el.animate(
+            { width: "0px", overflow: "hidden" },
+            {
+              duration: 250,
+              complete: function () {
+                view.onClose();
+                view.remove();
+              },
+            },
+          );
+        } catch (e) {
+          console.log(
+            "Failed to remove an ImageEdit view. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * redoValidation - Called when a user focuses out of input fields
+       * with the .basic-text class (organization name and associated URL), or
+       * when an image is successfully uploaded or removed. This function
+       * validates the PortalImage model again and shows errors if there are any.
+       */
+      redoValidation: function () {
+        try {
+          view = this;
+          // Add a small pause so that the model is updated first.
+          setTimeout(function () {
+            view.removeValidation();
+            view.showValidation();
+          }, 1);
+        } catch (e) {
+          console.log(e);
+        }
+      },
+
+      /**
+       * showValidation - show validation errors for this ImageEdit view
+       */
+      showValidation: function () {
+        try {
+          var errors = this.model.validate();
+
+          if (errors) {
+            _.each(
+              errors,
+              function (errorMsg, category) {
+                var categoryEls = this.$("[data-category='" + category + "']");
+                //Use the showValidationMessage function from the parent view
+                if (this.editorView && this.editorView.showValidationMessage) {
+                  this.editorView.showValidationMessage(categoryEls, errorMsg);
+                }
+              },
+              this,
+            );
+
+            // add class to dropzone element if error has to do with image
+            if (errors.identifier) {
+              this.$el.find(".dropzone").addClass("error");
             }
-
-          }, this);
-
-          // add class to dropzone element if error has to do with image
-          if(errors.identifier){
-            this.$el.find(".dropzone").addClass("error");
           }
-
+        } catch (e) {
+          console.log("Failed to validate portalImage, error: " + e);
         }
-
-      } catch (e) {
-        console.log("Failed to validate portalImage, error: " + e);
-      }
-
-    },
-
-    /**
-     * removeValidation - Remove displayed validation errors, if any
-     */
-    removeValidation: function(){
-      this.$(".notification.error").removeClass("error").empty();
-      this.$(".section-link-container.error, input.error, textarea.error").removeClass("error");
-      this.$(".validation-error-icon").hide();
-      this.$el.find(".dropzone").removeClass("error");
-    },
-
-    /**
-    * Add the "remove-preview" class which will show a preview for removing this image, via CSS
-    */
-    showRemovePreview: function(){
-      try{
-        this.$el.addClass("remove-preview");
-      }
-      catch (error) {
-        console.error("Failed to preview the removal of an image edit view. Error message: " + error);
-      }
+      },
+
+      /**
+       * removeValidation - Remove displayed validation errors, if any
+       */
+      removeValidation: function () {
+        this.$(".notification.error").removeClass("error").empty();
+        this.$(
+          ".section-link-container.error, input.error, textarea.error",
+        ).removeClass("error");
+        this.$(".validation-error-icon").hide();
+        this.$el.find(".dropzone").removeClass("error");
+      },
+
+      /**
+       * Add the "remove-preview" class which will show a preview for removing this image, via CSS
+       */
+      showRemovePreview: function () {
+        try {
+          this.$el.addClass("remove-preview");
+        } catch (error) {
+          console.error(
+            "Failed to preview the removal of an image edit view. Error message: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Removes the "remove-preview" class which will hide the preview for removing this image, via CSS
+       */
+      hideRemovePreview: function (e) {
+        try {
+          this.$el.removeClass("remove-preview");
+        } catch (error) {
+          console.error(
+            "Failed to preview the removal of an image edit view. Error message: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * This function is called whenever this view is about to be removed from the page.
+       */
+      onClose: function () {
+        //Destroy any tooltips in this view that are still open
+        this.$(".tooltip-this").tooltip("destroy");
+      },
     },
-
-    /**
-    * Removes the "remove-preview" class which will hide the preview for removing this image, via CSS
-    */
-    hideRemovePreview: function(e){
-      try{
-        this.$el.removeClass("remove-preview");
-      }
-      catch (error) {
-        console.error("Failed to preview the removal of an image edit view. Error message: " + error);
-      }
-   },
-
-   /**
-   * This function is called whenever this view is about to be removed from the page.
-   */
-   onClose: function(){
-     //Destroy any tooltips in this view that are still open
-     this.$(".tooltip-this").tooltip("destroy");
-   }
-
-
-  });
+  );
 
   return PortEditorImageView;
-
 });
 
diff --git a/docs/docs/src_js_views_portals_editor_PortEditorLogosView.js.html b/docs/docs/src_js_views_portals_editor_PortEditorLogosView.js.html index f4f753b15..b4b78dd12 100644 --- a/docs/docs/src_js_views_portals_editor_PortEditorLogosView.js.html +++ b/docs/docs/src_js_views_portals_editor_PortEditorLogosView.js.html @@ -44,239 +44,237 @@

Source: src/js/views/portals/editor/PortEditorLogosView.j
-
define(['underscore',
-        'jquery',
-        'backbone',
-        "models/portals/PortalImage",
-        "views/portals/editor/PortEditorImageView"],
-function(_, $, Backbone, PortalImage, ImageEdit){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/portals/PortalImage",
+  "views/portals/editor/PortEditorImageView",
+], function (_, $, Backbone, PortalImage, ImageEdit) {
   /**
-  * @class PortEditorLogosView
-  * @classcategory Views/Portals/Editor
-  * @extends Backbone.View
-  */
+   * @class PortEditorLogosView
+   * @classcategory Views/Portals/Editor
+   * @extends Backbone.View
+   */
   var PortEditorLogosView = Backbone.View.extend(
-    /** @lends PortEditorLogosView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "PortEditorLogos",
-
-    /**
-    * The HTML tag name to use for this view's element
-    * @type {string}
-    */
-    tagName: "div",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "port-editor-logos",
-
-    /**
-    * The PortalModel that is being edited
-    * @type {Portal}
-    */
-    model: undefined,
-
-    /**
-    * A reference to the PortalEditorView
-    * @type {PortalEditorView}
-    */
-    editorView: undefined,
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "keyup .edit-image.new .basic-text" : "handleNewInput",
-      "click .remove" : "handleRemove"
-    },
-
-    /**
-    * Creates a new PortEditorLogosView
-    * @param {Object} options - A literal object with options to pass to the view
-    * @property {Portal} options.model - The Portal whose logos are rendered in this view
-    * @property {PortalEditorView}  options.editorView - Gets set as PortalEditorLogosView.editorView
-    */
-    initialize: function(options){
-
-      try{
-        if( typeof options == "object" ){
-          this.model = options.model || undefined;
-          this.editorView = options.editorView;
+    /** @lends PortEditorLogosView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "PortEditorLogos",
+
+      /**
+       * The HTML tag name to use for this view's element
+       * @type {string}
+       */
+      tagName: "div",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "port-editor-logos",
+
+      /**
+       * The PortalModel that is being edited
+       * @type {Portal}
+       */
+      model: undefined,
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "keyup .edit-image.new .basic-text": "handleNewInput",
+        "click .remove": "handleRemove",
+      },
+
+      /**
+       * Creates a new PortEditorLogosView
+       * @param {Object} options - A literal object with options to pass to the view
+       * @property {Portal} options.model - The Portal whose logos are rendered in this view
+       * @property {PortalEditorView}  options.editorView - Gets set as PortalEditorLogosView.editorView
+       */
+      initialize: function (options) {
+        try {
+          if (typeof options == "object") {
+            this.model = options.model || undefined;
+            this.editorView = options.editorView;
+          }
+        } catch (e) {
+          console.log(
+            "PortEditorLogosView failed to initialize. Error message: " + e,
+          );
         }
-      } catch(e){
-        console.log("PortEditorLogosView failed to initialize. Error message: " + e);
-      }
-
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-      try{
-        var savedLogos = this.model.get("acknowledgmentsLogos"),
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        try {
+          var savedLogos = this.model.get("acknowledgmentsLogos"),
             newLogo = new PortalImage({ nodeName: "acknowledgmentsLogo" });
 
-        // If there are no acknowledgmentsLogos yet, then set a new empty logo for
-        // the user to enter information into
-        if( !savedLogos || !savedLogos.length){
-          this.model.set( "acknowledgmentsLogos", [newLogo]);
-        // If there are already logos, add a new blank logo to the end of the list.
-        // Note that empty logos won't get serialized
-        } else {
-          savedLogos.push(newLogo);
-          this.model.set("acknowledgmentsLogos", savedLogos);
+          // If there are no acknowledgmentsLogos yet, then set a new empty logo for
+          // the user to enter information into
+          if (!savedLogos || !savedLogos.length) {
+            this.model.set("acknowledgmentsLogos", [newLogo]);
+            // If there are already logos, add a new blank logo to the end of the list.
+            // Note that empty logos won't get serialized
+          } else {
+            savedLogos.push(newLogo);
+            this.model.set("acknowledgmentsLogos", savedLogos);
+          }
+
+          // Iterate over each logo in the PortalModel and render an ImageView
+          _.each(
+            this.model.get("acknowledgmentsLogos"),
+            function (portalImage) {
+              this.renderAckLogoInput(portalImage);
+            },
+            this,
+          );
+        } catch (e) {
+          console.log(
+            "PortEditorLogosView failed to render, error message: " + e,
+          );
         }
-
-        // Iterate over each logo in the PortalModel and render an ImageView
-        _.each(this.model.get("acknowledgmentsLogos"), function(portalImage){
-          this.renderAckLogoInput(portalImage);
-        }, this);
-
-      }
-      catch(e){
-        console.log("PortEditorLogosView failed to render, error message: " + e );
-      }
-    },
-
-
-    /**
-     * renderAckLogoInput - Adds a new ImageEdit view for a specified PortalImage model for an acknowledgments logo.
-     *
-     * @param  {PortalImage} portalImage The PortalImage model to create an ImageEdit view for.
-     */
-    renderAckLogoInput: function(portalImage){
-
-      try {
-
-        var view = this;
-
-        // Check if this is a new, empty acknowledgmentsLogo
-        var isNew = !portalImage.get("identifier") &&
-                    !portalImage.get("associatedURL") &&
-                    !portalImage.get("label");
-
-        var imageEdit = new ImageEdit({
-          parentModel: this.model,
-          editorView: this.editorView,
-          model: portalImage,
-          imageUploadInstructions: "Drag & drop a partner logo or click to upload",
-          imageWidth: 150,
-          imageHeight: 150,
-          minWidth: 100,
-          minHeight: 100,
-          maxHeight: 300,
-          maxWidth: 300,
-          nameLabel: "Organization name",
-          urlLabel: "Organization URL",
-          imageTagName: "img",
-          removeButton: true
-        });
-        $(this.el).append(imageEdit.el);
-        imageEdit.render();
-
-        // When user adds a file, this imageEdit is no longer new
-        imageEdit.listenToOnce(imageEdit.uploader, "addedfile", function(){
-          view.handleNewInput(this)
-        });
-
-        if(isNew){
-          $(imageEdit.el).addClass("new");
-          // Don't allow users to remove the new portalImage -
-          // it's the only place to add an acknowledgmentsLogo.
-          $(imageEdit.el).find(".remove.icon").hide();
+      },
+
+      /**
+       * renderAckLogoInput - Adds a new ImageEdit view for a specified PortalImage model for an acknowledgments logo.
+       *
+       * @param  {PortalImage} portalImage The PortalImage model to create an ImageEdit view for.
+       */
+      renderAckLogoInput: function (portalImage) {
+        try {
+          var view = this;
+
+          // Check if this is a new, empty acknowledgmentsLogo
+          var isNew =
+            !portalImage.get("identifier") &&
+            !portalImage.get("associatedURL") &&
+            !portalImage.get("label");
+
+          var imageEdit = new ImageEdit({
+            parentModel: this.model,
+            editorView: this.editorView,
+            model: portalImage,
+            imageUploadInstructions:
+              "Drag & drop a partner logo or click to upload",
+            imageWidth: 150,
+            imageHeight: 150,
+            minWidth: 100,
+            minHeight: 100,
+            maxHeight: 300,
+            maxWidth: 300,
+            nameLabel: "Organization name",
+            urlLabel: "Organization URL",
+            imageTagName: "img",
+            removeButton: true,
+          });
+          $(this.el).append(imageEdit.el);
+          imageEdit.render();
+
+          // When user adds a file, this imageEdit is no longer new
+          imageEdit.listenToOnce(imageEdit.uploader, "addedfile", function () {
+            view.handleNewInput(this);
+          });
+
+          if (isNew) {
+            $(imageEdit.el).addClass("new");
+            // Don't allow users to remove the new portalImage -
+            // it's the only place to add an acknowledgmentsLogo.
+            $(imageEdit.el).find(".remove.icon").hide();
+          }
+        } catch (e) {
+          console.log(
+            "Could not render an ImageEdit view for an acknowledgmentsLogo. Error message: " +
+              e,
+          );
         }
+      },
+
+      /**
+       * handleNewInput - Called when a user enters any input into a new ImageEdit
+       * view. It removes the "new" class, shows the "remove" button, and adds a new
+       * ImageEdit view with a blank PortalImage model.
+       *
+       * @param  {object} eventOrView either the keyup event when user enters text
+       * into an imageEdit input, OR the imageEdit view which contains the
+       * imageUploader where a user just uploaded an image.
+       */
+      handleNewInput: function (eventOrView) {
+        try {
+          // Get the relevant imageEdit view element
+          var imageEditEl = eventOrView.target
+            ? // when the arguement is an event
+              $(eventOrView.target).closest(".edit-image.new")
+            : // when the argument is a view
+              eventOrView.$el;
+
+          // This function should only modify new image-edit views
+          if (!imageEditEl || !imageEditEl.hasClass("new")) {
+            return;
+          }
+
+          var currentLogos = this.model.get("acknowledgmentsLogos"),
+            newLogo = new PortalImage({ nodeName: "acknowledgmentsLogo" });
 
-      } catch (e) {
-        console.log("Could not render an ImageEdit view for an acknowledgmentsLogo. Error message: " + e);
-      }
-
-    },
+          // Remove the 'new' class
+          imageEditEl.removeClass("new");
 
-    /**
-     * handleNewInput - Called when a user enters any input into a new ImageEdit
-     * view. It removes the "new" class, shows the "remove" button, and adds a new
-     * ImageEdit view with a blank PortalImage model.
-     *
-     * @param  {object} eventOrView either the keyup event when user enters text
-     * into an imageEdit input, OR the imageEdit view which contains the
-     * imageUploader where a user just uploaded an image.
-     */
-    handleNewInput: function(eventOrView){
-
-      try {
-        // Get the relevant imageEdit view element
-        var imageEditEl   = eventOrView.target ?
-                            // when the arguement is an event
-                            $(eventOrView.target).closest(".edit-image.new") :
-                            // when the argument is a view
-                            eventOrView.$el;
-
-        // This function should only modify new image-edit views
-        if(!imageEditEl || !imageEditEl.hasClass("new")){
-          return
-        }
+          // Allow users to delete this logo
+          imageEditEl.find(".remove.icon").show();
 
-        var currentLogos  = this.model.get("acknowledgmentsLogos"),
-            newLogo       = new PortalImage({ nodeName: "acknowledgmentsLogo" });
+          // Add a new blank portalImage
+          currentLogos.push(newLogo);
+          this.model.set("acknowledgmentsLogos", currentLogos);
 
-        // Remove the 'new' class
-        imageEditEl.removeClass("new");
+          // Show the new EditImage view
+          this.renderAckLogoInput(newLogo);
 
-        // Allow users to delete this logo
-        imageEditEl.find(".remove.icon").show();
-
-        // Add a new blank portalImage
-        currentLogos.push(newLogo);
-        this.model.set("acknowledgmentsLogos", currentLogos);
+          this.editorView.showControls();
+        } catch (e) {
+          console.log(
+            "Failed to handle user input in an acknowledgments logo imageEdit view. Error message: " +
+              e,
+          );
+        }
+      },
 
-        // Show the new EditImage view
-        this.renderAckLogoInput(newLogo);
+      /**
+       * showValidation - Show validation errors for each logoView
+       */
+      showValidation: function () {
+        var logoViews = $(this.el).find(".edit-image");
 
+        _.each(logoViews, function (logoView) {
+          $(logoView).data("view").showValidation();
+        });
+      },
+
+      /**
+       * This function is called when a logo is removed. The logo removal itself is done
+       * by the PortEditorImageView. This function performs additional functionality that
+       * should happen after the removal.
+       */
+      handleRemove: function () {
         this.editorView.showControls();
-
-      } catch (e) {
-        console.log("Failed to handle user input in an acknowledgments logo imageEdit view. Error message: " + e);
-      }
-
+      },
     },
-
-
-    /**
-     * showValidation - Show validation errors for each logoView
-     */
-    showValidation: function(){
-
-      var logoViews = $(this.el).find(".edit-image");
-
-      _.each(logoViews, function(logoView){
-        $(logoView).data("view").showValidation();
-      });
-
-    },
-
-    /**
-    * This function is called when a logo is removed. The logo removal itself is done
-    * by the PortEditorImageView. This function performs additional functionality that
-    * should happen after the removal.
-    */
-    handleRemove: function(){
-      this.editorView.showControls();
-    }
-
-  });
+  );
 
   return PortEditorLogosView;
-
 });
 
diff --git a/docs/docs/src_js_views_portals_editor_PortEditorMdSectionView.js.html b/docs/docs/src_js_views_portals_editor_PortEditorMdSectionView.js.html index 0bf3cfb56..a0d6d9b6b 100644 --- a/docs/docs/src_js_views_portals_editor_PortEditorMdSectionView.js.html +++ b/docs/docs/src_js_views_portals_editor_PortEditorMdSectionView.js.html @@ -44,280 +44,302 @@

Source: src/js/views/portals/editor/PortEditorMdSectionVi
-
define(['underscore',
-        'jquery',
-        'backbone',
-        "models/portals/PortalSectionModel",
-        "models/portals/PortalImage",
-        "views/ImageUploaderView",
-        "views/MarkdownEditorView",
-        "views/portals/editor/PortEditorSectionView",
-        "views/portals/editor/PortEditorImageView",
-        "text!templates/portals/editor/portEditorMdSection.html"],
-function(_, $, Backbone, PortalSectionModel, PortalImage, ImageUploader, MarkdownEditor, PortEditorSectionView, ImageEdit, Template){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/portals/PortalSectionModel",
+  "models/portals/PortalImage",
+  "views/ImageUploaderView",
+  "views/MarkdownEditorView",
+  "views/portals/editor/PortEditorSectionView",
+  "views/portals/editor/PortEditorImageView",
+  "text!templates/portals/editor/portEditorMdSection.html",
+], function (
+  _,
+  $,
+  Backbone,
+  PortalSectionModel,
+  PortalImage,
+  ImageUploader,
+  MarkdownEditor,
+  PortEditorSectionView,
+  ImageEdit,
+  Template,
+) {
   /**
-  * @class PortEditorMdSectionView
-  * @classdesc A section of the Portal Editor for adding/editing a Markdown page to a Portal
-  * @classcategory Views/Portals/Editor
-  * @extends PortEditorSectionView
-  * @constructor
-  */
+   * @class PortEditorMdSectionView
+   * @classdesc A section of the Portal Editor for adding/editing a Markdown page to a Portal
+   * @classcategory Views/Portals/Editor
+   * @extends PortEditorSectionView
+   * @constructor
+   */
   var PortEditorMdSectionView = PortEditorSectionView.extend(
-    /** @lends PortEditorMdSectionView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    * @readonly
-    */
-    type: "PortEditorMdSection",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: PortEditorSectionView.prototype.className + " port-editor-md",
-
-    /**
-    * The HTML attributes to set on this view's element
-    * @type {object}
-    */
-    attributes: {
-      "data-category": "sections"
-    },
-
-    /**
-    * The PortalSectionModel that is being edited
-    * @type {PortalSection}
-    */
-    model: undefined,
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    * @type {Underscore.Template}
-    */
-    template: _.template(Template),
-
-    /**
-    * A jQuery selector for the element that will contain the ImageUploader view
-    * @type {string}
-    */
-    imageUploaderContainer: ".portal-display-image",
-
-    /**
-    * A jQuery selector for the element that will contain the markdown section
-    * title text
-    * @type {string}
-    */
-    titleEl: ".title",
-
-    /**
-    * A jQuery selector for the element that will contain the markdown section
-    * introduction text
-    * @type {string}
-    */
-    introEl: ".introduction",
-
-    /**
-    * A jQuery selector for the element that will contain the markdown editor
-    * @type {string}
-    */
-    markdownEditorContainer: ".markdown-editor-container",
-
-    /**
-    * A reference to the PortalEditorView
-    * @type {PortalEditorView}
-    */
-    editorView: undefined,
-
-    /**
-    * The type of section view this is
-    * @type {string}
-    * @readonly
-    */
-    sectionType: "freeform",
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-    },
-
-    /**
-    * Is executed when a new PortEditorMdSectionView is created
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options){
-
-      //Call the superclass initialize() function
-      //Passing the parameters to the super class constructor
-      PortEditorSectionView.prototype.initialize(options);
-
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-      try{
-
-        //Attach this view to the view Element
-        this.$el.data("view", this);
-
-        /**
-        * PortalVizSection models aren't editable yet, so show a message and exit.
-        * @todo Create a PortalVizSectionView for PortalVizSection models, rather than
-        * checking the section type here. */
-        if( this.model.type == "PortalVizSection" ){
+    /** @lends PortEditorMdSectionView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       * @readonly
+       */
+      type: "PortEditorMdSection",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: PortEditorSectionView.prototype.className + " port-editor-md",
+
+      /**
+       * The HTML attributes to set on this view's element
+       * @type {object}
+       */
+      attributes: {
+        "data-category": "sections",
+      },
+
+      /**
+       * The PortalSectionModel that is being edited
+       * @type {PortalSection}
+       */
+      model: undefined,
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       * @type {Underscore.Template}
+       */
+      template: _.template(Template),
+
+      /**
+       * A jQuery selector for the element that will contain the ImageUploader view
+       * @type {string}
+       */
+      imageUploaderContainer: ".portal-display-image",
+
+      /**
+       * A jQuery selector for the element that will contain the markdown section
+       * title text
+       * @type {string}
+       */
+      titleEl: ".title",
+
+      /**
+       * A jQuery selector for the element that will contain the markdown section
+       * introduction text
+       * @type {string}
+       */
+      introEl: ".introduction",
+
+      /**
+       * A jQuery selector for the element that will contain the markdown editor
+       * @type {string}
+       */
+      markdownEditorContainer: ".markdown-editor-container",
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * The type of section view this is
+       * @type {string}
+       * @readonly
+       */
+      sectionType: "freeform",
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {},
+
+      /**
+       * Is executed when a new PortEditorMdSectionView is created
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        //Call the superclass initialize() function
+        //Passing the parameters to the super class constructor
+        PortEditorSectionView.prototype.initialize(options);
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        try {
+          //Attach this view to the view Element
+          this.$el.data("view", this);
+
+          /**
+           * PortalVizSection models aren't editable yet, so show a message and exit.
+           * @todo Create a PortalVizSectionView for PortalVizSection models, rather than
+           * checking the section type here. */
+          if (this.model.type == "PortalVizSection") {
+            MetacatUI.appView.showAlert(
+              "You're all set! A Fluid Earth Viewer data visualization will appear here.",
+              "alert-info",
+              this.$el,
+            );
+            this.$el.addClass("port-editor-viz");
+
+            return;
+          }
 
-          MetacatUI.appView.showAlert("You're all set! A Fluid Earth Viewer data visualization will appear here.",
-                                      "alert-info",
-                                      this.$el);
-          this.$el.addClass("port-editor-viz");
+          // Insert the template into the view
+          this.$el
+            .html(
+              this.template({
+                title: this.model.get("title"),
+                titlePlaceholder: "Add a page title",
+                introduction: this.model.get("introduction"),
+                introPlaceholder:
+                  "Add a sub-title or an introductory blurb about the content on this page.",
+                // unique ID to use for the bootstrap accordion component, which
+                // breaks when targeting two + components with the same ID
+                cid: this.model.cid,
+              }),
+            )
+            .data("view", this);
+
+          // Render the Markdown Editor View
+          var mdEditor = new MarkdownEditor({
+            model: this.model.get("content"),
+            markdownPlaceholder:
+              "# Content\n\nAdd content here. Styling with markdown is supported.",
+            previewPlaceholder:
+              "Add some text in the Edit tab to show a preview here",
+            showTOC: true,
+          });
+          mdEditor.render();
+          this.$(this.markdownEditorContainer).html(mdEditor.el);
+
+          // Attach the appropriate models to the textarea elements,
+          // so that PortalEditorView.updateBasicText(e) can access them
+          // Don't use the updateBasicText function on content/markdown sections,
+          // because we don't want to "cleanXMLText" for markdown
+          this.$(this.titleEl).data({ model: this.model });
+          this.$(this.introEl).data({ model: this.model });
+
+          // Add an ImageEdit view for the sectionImage
+          // If the section has no image yet, add the default PortalImage model
+          if (!this.model.get("image")) {
+            this.model.set("image", new PortalImage({ nodeName: "image" }));
+          }
 
-          return;
+          // Add the edit image view (incl. uploader) for the section image
+          this.sectionImageUploader = new ImageEdit({
+            model: this.model.get("image"),
+            editorView: this.editorView,
+            imageUploadInstructions: [
+              "Drag & drop a high quality image here or click to upload",
+              "Suggested image size: 1200 x 1000 pixels",
+            ],
+            nameLabel: false,
+            urlLabel: false,
+            imageTagName: "div",
+            removeButton: false,
+            imageWidth: false, // set to 100% in metacatui-common.css
+            imageHeight: 300,
+            minWidth: 800,
+            minHeight: 300,
+            maxHeight: 4000,
+            maxWidth: 9000,
+          });
+          this.$(this.imageUploaderContainer).append(
+            this.sectionImageUploader.el,
+          );
+          this.sectionImageUploader.render();
+          this.$(this.imageUploaderContainer).data(
+            "view",
+            this.sectionImageUploader,
+          );
+
+          // Set listeners to auto-resize the height of the intoduction and title
+          // textareas on user-input and on window resize events. This way the
+          // fields appear more closely to how they will look on the portal view.
+          var view = this;
+          $(window).resize(function () {
+            view.$("textarea.auto-resize").trigger("textareaResize");
+          });
+          this.$("textarea.auto-resize").off("input textareaResize");
+          this.$("textarea.auto-resize").on(
+            "input textareaResize",
+            function (e) {
+              view.resizeTextarea($(e.target));
+            },
+          );
+
+          // Make sure the textareas are the right size with their pre-filled
+          // content the first time the section is viewed, because scrollHeight
+          // is 0px when the element is not displayed.
+          this.listenToOnce(this, "active", function () {
+            view.resizeTextarea(view.$("textarea.auto-resize"));
+          });
+
+          this.listenTo(this.model.get("content"), "change", function () {
+            this.editorView.showControls();
+          });
+          this.listenTo(this.model.get("image"), "change", function () {
+            this.editorView.showControls();
+          });
+        } catch (e) {
+          console.log(
+            "The portal editor markdown section view could not be rendered, error message: " +
+              e,
+          );
         }
-
-        // Insert the template into the view
-        this.$el.html(this.template({
-          title: this.model.get("title"),
-          titlePlaceholder: "Add a page title",
-          introduction: this.model.get("introduction"),
-          introPlaceholder: "Add a sub-title or an introductory blurb about the content on this page.",
-          // unique ID to use for the bootstrap accordion component, which
-          // breaks when targeting two + components with the same ID
-          cid: this.model.cid
-        })).data("view", this);
-
-        // Render the Markdown Editor View
-        var mdEditor = new MarkdownEditor({
-          model: this.model.get("content"),
-          markdownPlaceholder: "# Content\n\nAdd content here. Styling with markdown is supported.",
-          previewPlaceholder: "Add some text in the Edit tab to show a preview here",
-          showTOC: true
-        });
-        mdEditor.render();
-        this.$(this.markdownEditorContainer).html(mdEditor.el);
-
-        // Attach the appropriate models to the textarea elements,
-        // so that PortalEditorView.updateBasicText(e) can access them
-        // Don't use the updateBasicText function on content/markdown sections,
-        // because we don't want to "cleanXMLText" for markdown
-        this.$(this.titleEl).data({ model: this.model });
-        this.$(this.introEl).data({ model: this.model });
-
-        // Add an ImageEdit view for the sectionImage
-        // If the section has no image yet, add the default PortalImage model
-        if( !this.model.get("image") ){
-          this.model.set("image", new PortalImage({ nodeName: "image" }) );
-        };
-
-        // Add the edit image view (incl. uploader) for the section image
-        this.sectionImageUploader = new ImageEdit({
-
-          model: this.model.get("image"),
-          editorView: this.editorView,
-          imageUploadInstructions: ["Drag & drop a high quality image here or click to upload",
-                                    "Suggested image size: 1200 x 1000 pixels"],
-          nameLabel: false,
-          urlLabel: false,
-          imageTagName: "div",
-          removeButton: false,
-          imageWidth: false, // set to 100% in metacatui-common.css
-          imageHeight: 300,
-          minWidth: 800,
-          minHeight: 300,
-          maxHeight: 4000,
-          maxWidth: 9000
-
-        });
-        this.$(this.imageUploaderContainer).append(this.sectionImageUploader.el);
-        this.sectionImageUploader.render();
-        this.$(this.imageUploaderContainer).data("view", this.sectionImageUploader);
-
-        // Set listeners to auto-resize the height of the intoduction and title
-        // textareas on user-input and on window resize events. This way the
-        // fields appear more closely to how they will look on the portal view.
-        var view = this;
-        $( window ).resize(function() {
-          view.$("textarea.auto-resize").trigger("textareaResize");
-        });
-        this.$("textarea.auto-resize").off('input textareaResize');
-        this.$("textarea.auto-resize").on('input textareaResize', function(e){
-          view.resizeTextarea($(e.target));
-        });
-
-        // Make sure the textareas are the right size with their pre-filled
-        // content the first time the section is viewed, because scrollHeight
-        // is 0px when the element is not displayed.
-        this.listenToOnce(this, "active", function(){
-          view.resizeTextarea(view.$("textarea.auto-resize"));
-        });
-
-        this.listenTo(this.model.get("content"), "change", function(){
-          this.editorView.showControls();
-        });
-        this.listenTo(this.model.get("image"), "change", function(){
-          this.editorView.showControls();
-        });
-
-      }
-      catch(e){
-        console.log("The portal editor markdown section view could not be rendered, error message: " + e);
-      }
-
-    },
-
-    /**
-     * resizeTextarea - Set the height of a textarea element based on its
-     * scrollHeight.
-     *
-     * @param  {jQuery} textareas The textarea element or elements to be resized.
-     */
-    resizeTextarea: function(textareas){
-      try {
-        if(textareas){
-          _.each(textareas, function(textarea){
-            if(textarea.style){
-              textarea.style.height = '0px'; // note: textfield MUST have a min-height set
-              textarea.style.height = (textarea.scrollHeight) + 'px';
-            }
-          })
+      },
+
+      /**
+       * resizeTextarea - Set the height of a textarea element based on its
+       * scrollHeight.
+       *
+       * @param  {jQuery} textareas The textarea element or elements to be resized.
+       */
+      resizeTextarea: function (textareas) {
+        try {
+          if (textareas) {
+            _.each(textareas, function (textarea) {
+              if (textarea.style) {
+                textarea.style.height = "0px"; // note: textfield MUST have a min-height set
+                textarea.style.height = textarea.scrollHeight + "px";
+              }
+            });
+          }
+        } catch (e) {
+          console.log("failed to resize textarea element. Error message: " + r);
+        }
+      },
+
+      /**
+       * showValidation - Display validation errors if any are retuned by the PortalSection model
+       */
+      showValidation: function () {
+        try {
+          var errors = this.model.validate();
+
+          _.each(
+            errors,
+            function (errorMsg, category) {
+              var categoryEls = this.$("[data-category='" + category + "']");
+
+              //Use the showValidationMessage function from the parent view
+              if (this.editorView && this.editorView.showValidationMessage) {
+                this.editorView.showValidationMessage(categoryEls, errorMsg);
+              }
+            },
+            this,
+          );
+        } catch (e) {
+          console.error(e);
         }
-      } catch (e) {
-        console.log("failed to resize textarea element. Error message: " + r);
-      }
+      },
     },
-
-    /**
-     * showValidation - Display validation errors if any are retuned by the PortalSection model
-     */
-    showValidation: function(){
-      try{
-        var errors = this.model.validate();
-
-        _.each(errors, function(errorMsg, category){
-          var categoryEls = this.$("[data-category='" + category + "']");
-
-          //Use the showValidationMessage function from the parent view
-          if( this.editorView && this.editorView.showValidationMessage ){
-            this.editorView.showValidationMessage(categoryEls, errorMsg);
-          }
-
-        }, this);
-      }
-      catch(e){
-        console.error(e);
-      }
-    }
-
-  });
+  );
 
   return PortEditorMdSectionView;
-
 });
 
diff --git a/docs/docs/src_js_views_portals_editor_PortEditorSectionView.js.html b/docs/docs/src_js_views_portals_editor_PortEditorSectionView.js.html index c4edb4ec1..f60568076 100644 --- a/docs/docs/src_js_views_portals_editor_PortEditorSectionView.js.html +++ b/docs/docs/src_js_views_portals_editor_PortEditorSectionView.js.html @@ -44,333 +44,359 @@

Source: src/js/views/portals/editor/PortEditorSectionView
-
define(['underscore',
-        'jquery',
-        'backbone',
-        'models/portals/PortalSectionModel',
-        "text!templates/portals/editor/portEditorSection.html",
-        "text!templates/portals/editor/portEditorSectionOption.html",
-        "text!templates/portals/editor/portEditorSectionOptionImgs/freeform.svg",
-        "text!templates/portals/editor/portEditorSectionOptionImgs/metrics.svg"],
-function(_, $, Backbone, PortalSectionModel, Template, SectionOptionTemplate, FreeformSVG, MetricsSVG){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/portals/PortalSectionModel",
+  "text!templates/portals/editor/portEditorSection.html",
+  "text!templates/portals/editor/portEditorSectionOption.html",
+  "text!templates/portals/editor/portEditorSectionOptionImgs/freeform.svg",
+  "text!templates/portals/editor/portEditorSectionOptionImgs/metrics.svg",
+], function (
+  _,
+  $,
+  Backbone,
+  PortalSectionModel,
+  Template,
+  SectionOptionTemplate,
+  FreeformSVG,
+  MetricsSVG,
+) {
   /**
-  * @class PortEditorSectionView
-  * @classdesc A view of a single section of the PortalEditorView.
-  * This default section view displays a choice of which PortalSection to add to the Portal.
-  * @classcategory Views/Portals/Editor
-  * @extends Backbone.View
-  * @constructor
-  */
+   * @class PortEditorSectionView
+   * @classdesc A view of a single section of the PortalEditorView.
+   * This default section view displays a choice of which PortalSection to add to the Portal.
+   * @classcategory Views/Portals/Editor
+   * @extends Backbone.View
+   * @constructor
+   */
   var PortEditorSectionView = Backbone.View.extend(
-    /** @lends PortEditorSectionView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "PortEditorSection",
-
-    /**
-    * The unique label for this Section. It most likely matches the label on the model, but
-    * may include a number after if more than one section has the same name.
-    * @type {string}
-    */
-    uniqueSectionLabel: "",
-
-    /**
-    * The HTML tag name for this view's element
-    * @type {string}
-    */
-    tagName: "div",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "port-editor-section tab-pane",
-
-    /**
-    * The PortalSectionModel being displayed
-    * @type {PortalSection}
-    */
-    model: undefined,
-
-    /**
-    * A reference to the PortalEditorView
-    * @type {PortalEditorView}
-    */
-    editorView: null,
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    */
-    template: _.template(Template),
-    sectionOptionTemplate: _.template(SectionOptionTemplate),
-
-    /**
-    * A jQuery selector for the element that the section option buttons should be inserted into
-    * @type {string}
-    */
-    sectionsOptionsContainer: "#section-options-container",
-
-    /**
-     * @typedef {Object} PorttEditorSectionView#sectionOption - Information about a section type that can be added to a portal
-     * @property {string} title - The name of the section type to be displayed to the user
-     * @property {string} description - A brief description of the section type, to be displayed to the user
-     * @property {string|number} limiter - The limiter is used to determine whether the user is allowed to add more of this section type. If limiter is a number, then it's used as the maximum number of the given sections allowed (currently this only applies to 'freeform'/markdown sections). If limiter is a string, it should be the name of the 'hide' option in the project model (e.g. hideMetrics for the metrics view). In this case, it's assumed that only one of the given page type is allowed.
-     * @property {string} svg - SVG that illustrates the section type. SVG elements should use classes to define fill colors that are not greyscale, so that they may be greyed-out to indicate that a section type is unavailable. SVG elements that use theme colors should use the classes 'theme-primary-fill', 'theme-secondary-fill', and 'theme-accent-fill'.
-    */
-
-    /**
-    * Information about each of the section types available to a user. Note that the key (e.g. "freeform") is used to ID the UI selection element.
-    * @type {PorttEditorSectionView#sectionOption[]}
-    */
-    sectionsOptions: {
-      freeform: {
-        title: "Freeform",
-        description: "Add content and images styled using markdown",
-        limiter: 30,
-        svg: FreeformSVG,
+    /** @lends PortEditorSectionView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "PortEditorSection",
+
+      /**
+       * The unique label for this Section. It most likely matches the label on the model, but
+       * may include a number after if more than one section has the same name.
+       * @type {string}
+       */
+      uniqueSectionLabel: "",
+
+      /**
+       * The HTML tag name for this view's element
+       * @type {string}
+       */
+      tagName: "div",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "port-editor-section tab-pane",
+
+      /**
+       * The PortalSectionModel being displayed
+       * @type {PortalSection}
+       */
+      model: undefined,
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: null,
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      template: _.template(Template),
+      sectionOptionTemplate: _.template(SectionOptionTemplate),
+
+      /**
+       * A jQuery selector for the element that the section option buttons should be inserted into
+       * @type {string}
+       */
+      sectionsOptionsContainer: "#section-options-container",
+
+      /**
+       * @typedef {Object} PorttEditorSectionView#sectionOption - Information about a section type that can be added to a portal
+       * @property {string} title - The name of the section type to be displayed to the user
+       * @property {string} description - A brief description of the section type, to be displayed to the user
+       * @property {string|number} limiter - The limiter is used to determine whether the user is allowed to add more of this section type. If limiter is a number, then it's used as the maximum number of the given sections allowed (currently this only applies to 'freeform'/markdown sections). If limiter is a string, it should be the name of the 'hide' option in the project model (e.g. hideMetrics for the metrics view). In this case, it's assumed that only one of the given page type is allowed.
+       * @property {string} svg - SVG that illustrates the section type. SVG elements should use classes to define fill colors that are not greyscale, so that they may be greyed-out to indicate that a section type is unavailable. SVG elements that use theme colors should use the classes 'theme-primary-fill', 'theme-secondary-fill', and 'theme-accent-fill'.
+       */
+
+      /**
+       * Information about each of the section types available to a user. Note that the key (e.g. "freeform") is used to ID the UI selection element.
+       * @type {PorttEditorSectionView#sectionOption[]}
+       */
+      sectionsOptions: {
+        freeform: {
+          title: "Freeform",
+          description: "Add content and images styled using markdown",
+          limiter: 30,
+          svg: FreeformSVG,
+        },
+        metrics: {
+          title: "Metrics",
+          description: "Show visual summaries of your data collection",
+          limiter: "hideMetrics",
+          svg: MetricsSVG,
+        },
       },
-      metrics: {
-        title: "Metrics",
-        description: "Show visual summaries of your data collection",
-        limiter: "hideMetrics",
-        svg: MetricsSVG
-      }
-    },
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "click .section-option" : "addNewSection"
-    },
 
-    /**
-    * Creates a new PortEditorSectionView
-    * @param {Object} options - A literal object with options to pass to the view
-    * @property {PortalSection} options.model - The PortalSection rendered in this view
-    */
-    initialize: function(options){
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "click .section-option": "addNewSection",
+      },
 
-      // Get all the options and apply them to this view
-      if( typeof options == "object" ) {
+      /**
+       * Creates a new PortEditorSectionView
+       * @param {Object} options - A literal object with options to pass to the view
+       * @property {PortalSection} options.model - The PortalSection rendered in this view
+       */
+      initialize: function (options) {
+        // Get all the options and apply them to this view
+        if (typeof options == "object") {
           var optionKeys = Object.keys(options);
-          _.each(optionKeys, function(key, i) {
+          _.each(
+            optionKeys,
+            function (key, i) {
               this[key] = options[key];
-          }, this);
-      }
-
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-      try{
-
-        // Insert the template into the view
-        this.$el.html(this.template());
-
-        // Add a section option element for each section type the user can select from.
-        _.each(this.sectionsOptions, function(sectionData, sectionType){
-
-          this.$(this.sectionsOptionsContainer).append(
-            this.sectionOptionTemplate({
-              id: "section-option-" + sectionType,
-              title: sectionData.title,
-              description: sectionData.description,
-              img: sectionData.svg,
-              sectionType: sectionType
-            })
-
-          )
-
-          // Check whether the section option is available to user
-          this.toggleDisableSectionOption(sectionType);
+            },
+            this,
+          );
+        }
+      },
 
-          // For metrics, data, and members sections, add a listener to update the
-          // section availability when the associated model option is changed.
-          if(typeof sectionData.limiter === 'string' || sectionData.limiter instanceof String){
-            this.stopListening(this.model, "change:"+sectionData.limiter);
-            this.listenTo(this.model, "change:"+sectionData.limiter, function(){
-              try{
-                this.toggleDisableSectionOption(sectionType);
-              }
-              catch(e){
-                console.log("Cannot toggle disabling of section types, error message: " + e);
+      /**
+       * Renders this view
+       */
+      render: function () {
+        try {
+          // Insert the template into the view
+          this.$el.html(this.template());
+
+          // Add a section option element for each section type the user can select from.
+          _.each(
+            this.sectionsOptions,
+            function (sectionData, sectionType) {
+              this.$(this.sectionsOptionsContainer).append(
+                this.sectionOptionTemplate({
+                  id: "section-option-" + sectionType,
+                  title: sectionData.title,
+                  description: sectionData.description,
+                  img: sectionData.svg,
+                  sectionType: sectionType,
+                }),
+              );
+
+              // Check whether the section option is available to user
+              this.toggleDisableSectionOption(sectionType);
+
+              // For metrics, data, and members sections, add a listener to update the
+              // section availability when the associated model option is changed.
+              if (
+                typeof sectionData.limiter === "string" ||
+                sectionData.limiter instanceof String
+              ) {
+                this.stopListening(this.model, "change:" + sectionData.limiter);
+                this.listenTo(
+                  this.model,
+                  "change:" + sectionData.limiter,
+                  function () {
+                    try {
+                      this.toggleDisableSectionOption(sectionType);
+                    } catch (e) {
+                      console.log(
+                        "Cannot toggle disabling of section types, error message: " +
+                          e,
+                      );
+                    }
+                  },
+                );
               }
-            });
-          }
-        }, this);
-
-        //Save a reference to this view
-        this.$el.data("view", this);
-
-      }
-      catch(e){
-        console.log("Section view cannot be rendered, error message: " + e);
-      }
-
-    },
-
-
-    /**
-    * Checks whether a section type is available to a user to add, then calls functions that change content and styling to indicate the availability to the user.
-    * @param {string} sectionType - The section name. This is the same string used as the key in sectionsOptions (e.g. "freeform").
-    */
-    toggleDisableSectionOption: function(sectionType){
-
-      try{
-
-        var limiter  = this.sectionsOptions[sectionType].limiter;
+            },
+            this,
+          );
+
+          //Save a reference to this view
+          this.$el.data("view", this);
+        } catch (e) {
+          console.log("Section view cannot be rendered, error message: " + e);
+        }
+      },
 
-        // If limiter's a string, look up whether this section type is hidden
-        if(typeof limiter === 'string' || limiter instanceof String){
-          // If it's currently hidden
-          if(this.model.get(limiter)){
-            // then allow user to 'unhide' it.
-            this.enableSectionOption(sectionType);
-          // If it's already displayed
-          } else {
-            // then user can't add more of this type of section.
-            this.disableSectionOption(sectionType);
-          }
-        // If limiter's a number, compare it to the count of sections in the model
-        } else if (typeof limiter === 'number' || limiter instanceof Number){
-          if(this.model.get("sections").length < limiter){
-            this.enableSectionOption(sectionType);
+      /**
+       * Checks whether a section type is available to a user to add, then calls functions that change content and styling to indicate the availability to the user.
+       * @param {string} sectionType - The section name. This is the same string used as the key in sectionsOptions (e.g. "freeform").
+       */
+      toggleDisableSectionOption: function (sectionType) {
+        try {
+          var limiter = this.sectionsOptions[sectionType].limiter;
+
+          // If limiter's a string, look up whether this section type is hidden
+          if (typeof limiter === "string" || limiter instanceof String) {
+            // If it's currently hidden
+            if (this.model.get(limiter)) {
+              // then allow user to 'unhide' it.
+              this.enableSectionOption(sectionType);
+              // If it's already displayed
+            } else {
+              // then user can't add more of this type of section.
+              this.disableSectionOption(sectionType);
+            }
+            // If limiter's a number, compare it to the count of sections in the model
+          } else if (typeof limiter === "number" || limiter instanceof Number) {
+            if (this.model.get("sections").length < limiter) {
+              this.enableSectionOption(sectionType);
+            } else {
+              this.disableSectionOption(sectionType);
+            }
+            // If limiter is neither a string nor a number
           } else {
-            this.disableSectionOption(sectionType);
+            console.log(
+              "Error: In toggleDisableSectionOption(sectionType), the sectionType must be a string or a number.",
+            );
+            return;
           }
-        // If limiter is neither a string nor a number
-        } else {
-          console.log("Error: In toggleDisableSectionOption(sectionType), the sectionType must be a string or a number.");
-          return
+        } catch (e) {
+          console.error(e);
         }
+      },
 
-      } catch(e) {
-        console.error(e);
-      }
-
-
-    },
-
-    /**
-    * Adds styling and content to a section option element to indicate that the user already added the maximum allowable number of this section type (i.e. it's disabled).
-    * @param {string} sectionType - The section name. This is the same string used as the key in sectionsOptions (e.g. "freeform").
-    */
-    disableSectionOption: function(sectionType){
-
-      try{
+      /**
+       * Adds styling and content to a section option element to indicate that the user already added the maximum allowable number of this section type (i.e. it's disabled).
+       * @param {string} sectionType - The section name. This is the same string used as the key in sectionsOptions (e.g. "freeform").
+       */
+      disableSectionOption: function (sectionType) {
+        try {
+          if (
+            !sectionType ||
+            !(typeof sectionType === "string" || sectionType instanceof String)
+          ) {
+            console.error(
+              "Error: In disableSectionOption(sectionType), a string that indicates the sectionType is required",
+            );
+            return;
+          }
 
-        if(!sectionType || !(typeof sectionType === 'string' || sectionType instanceof String)){
-          console.error("Error: In disableSectionOption(sectionType), a string that indicates the sectionType is required");
-          return
+          var sectionOption = this.$("#section-option-" + sectionType),
+            description = sectionOption.find(".description"),
+            limiter = this.sectionsOptions[sectionType].limiter,
+            title = this.sectionsOptions[sectionType].title.toLowerCase(),
+            limit =
+              typeof limiter === "number" || limiter instanceof Number
+                ? limiter
+                : 1,
+            singOrPlur = limit > 1 ? "s" : "",
+            descriptionMsg =
+              "You've already added " +
+              limit +
+              " " +
+              title +
+              " page" +
+              singOrPlur;
+
+          //Add the disabled class
+          sectionOption.addClass("disabled");
+
+          //Add the new description
+          description.html(descriptionMsg);
+
+          //Create a tooltip
+          sectionOption.tooltip({
+            placement: "top",
+            delay: {
+              show: 800,
+              hide: 0,
+            },
+            title: descriptionMsg,
+            trigger: "hover",
+            container: sectionOption,
+          });
+
+          // Make sure disabled option isn't clickable
+          sectionOption.off("click");
+        } catch (e) {
+          console.error(e);
         }
+      },
 
-        var sectionOption = this.$("#section-option-" + sectionType),
-            description   = sectionOption.find(".description"),
-            limiter       = this.sectionsOptions[sectionType].limiter,
-            title         = this.sectionsOptions[sectionType].title.toLowerCase(),
-            limit         = (typeof limiter === 'number' || limiter instanceof Number) ?
-                              limiter : 1,
-            singOrPlur    = (limit > 1) ? "s" : "",
-            descriptionMsg = "You've already added " + limit + " " + title + " page" + singOrPlur;
-
-        //Add the disabled class
-        sectionOption.addClass("disabled");
-
-        //Add the new description
-        description.html(descriptionMsg);
-
-        //Create a tooltip
-        sectionOption.tooltip({
-          placement: "top",
-          delay: {
-            show: 800,
-            hide: 0
-          },
-          title: descriptionMsg,
-          trigger: "hover",
-          container: sectionOption
-        });
-
-        // Make sure disabled option isn't clickable
-        sectionOption.off("click");
-
-      } catch(e){
-        console.error(e);
-      }
-
-    },
-
-    /**
-    * Adds styling and content to a section option element to indicate that the user is able to add more of this section type (i.e. it's not disabled).
-    * @param {string} sectionType - The section name. This is the same string used as the key in sectionsOptions (e.g. "freeform").
-    */
-    enableSectionOption: function(sectionType){
-
-      try{
-
-        if(!sectionType || !(typeof sectionType === 'string' || sectionType instanceof String)){
-          console.log("Error: In enableSectionOption(sectionType), a string that indicates the sectionType is required");
-          return
-        }
+      /**
+       * Adds styling and content to a section option element to indicate that the user is able to add more of this section type (i.e. it's not disabled).
+       * @param {string} sectionType - The section name. This is the same string used as the key in sectionsOptions (e.g. "freeform").
+       */
+      enableSectionOption: function (sectionType) {
+        try {
+          if (
+            !sectionType ||
+            !(typeof sectionType === "string" || sectionType instanceof String)
+          ) {
+            console.log(
+              "Error: In enableSectionOption(sectionType), a string that indicates the sectionType is required",
+            );
+            return;
+          }
 
-        var sectionOption   = this.$("#section-option-" + sectionType),
-            descriptionEl   = sectionOption.find(".description"),
+          var sectionOption = this.$("#section-option-" + sectionType),
+            descriptionEl = sectionOption.find(".description"),
             descriptionText = this.sectionsOptions[sectionType].description;
 
-        //Remove the disabled class
-        sectionOption.removeClass("disabled");
-
-        //Update the description
-        descriptionEl.html(descriptionText);
-
-        //Remove the tooltip
-        sectionOption.tooltip("destroy");
-
-      } catch(e) {
-        console.error(e);
-      }
-
-    },
-
-    /**
-    * Gets the section type to add, and triggers an event so the rest of the app will add a new section
-    * @param {Event} e - The element that was clicked that represents the section option
-    */
-    addNewSection: function(e){
+          //Remove the disabled class
+          sectionOption.removeClass("disabled");
 
-      if( !e || $(e.target).is(".disabled") || $(e.target).parents(".section-option").first().is(".disabled") ){
-        return;
-      }
+          //Update the description
+          descriptionEl.html(descriptionText);
 
-      //Get the section type
-      var sectionType = $(e.target).data("section-type");
+          //Remove the tooltip
+          sectionOption.tooltip("destroy");
+        } catch (e) {
+          console.error(e);
+        }
+      },
 
-      if( !sectionType ){
-        sectionType = $(e.target).parents("[data-section-type]").first().data("section-type");
-        if( !sectionType ){
+      /**
+       * Gets the section type to add, and triggers an event so the rest of the app will add a new section
+       * @param {Event} e - The element that was clicked that represents the section option
+       */
+      addNewSection: function (e) {
+        if (
+          !e ||
+          $(e.target).is(".disabled") ||
+          $(e.target).parents(".section-option").first().is(".disabled")
+        ) {
           return;
         }
-      }
 
-      this.trigger("addNewSection", sectionType);
+        //Get the section type
+        var sectionType = $(e.target).data("section-type");
 
-      this.toggleDisableSectionOption(sectionType);
+        if (!sectionType) {
+          sectionType = $(e.target)
+            .parents("[data-section-type]")
+            .first()
+            .data("section-type");
+          if (!sectionType) {
+            return;
+          }
+        }
 
-    }
+        this.trigger("addNewSection", sectionType);
 
-  });
+        this.toggleDisableSectionOption(sectionType);
+      },
+    },
+  );
 
   return PortEditorSectionView;
-
 });
 
diff --git a/docs/docs/src_js_views_portals_editor_PortEditorSectionsView.js.html b/docs/docs/src_js_views_portals_editor_PortEditorSectionsView.js.html index 6513e8b65..f9d9e9837 100644 --- a/docs/docs/src_js_views_portals_editor_PortEditorSectionsView.js.html +++ b/docs/docs/src_js_views_portals_editor_PortEditorSectionsView.js.html @@ -44,1545 +44,1577 @@

Source: src/js/views/portals/editor/PortEditorSectionsVie
-
define(['underscore',
-        'jquery',
-        'backbone',
-        'sortable',
-        'models/portals/PortalModel',
-        'models/portals/PortalSectionModel',
-        "views/portals/editor/PortEditorSectionView",
-        "views/portals/editor/PortEditorSettingsView",
-        "views/portals/editor/PortEditorDataView",
-        "views/portals/editor/PortEditorMdSectionView",
-        "text!templates/portals/editor/portEditorSections.html",
-        "text!templates/portals/editor/portEditorMetrics.html",
-        "text!templates/portals/editor/portEditorSectionLink.html",
-        "text!templates/portals/editor/portEditorSectionOptionImgs/metrics.svg"],
-function(_, $, Backbone, Sortable, Portal, PortalSection,
-          PortEditorSectionView, PortEditorSettingsView, PortEditorDataView,
-          PortEditorMdSectionView,
-          Template, MetricsSectionTemplate, SectionLinkTemplate,
-          MetricsSVG){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "sortable",
+  "models/portals/PortalModel",
+  "models/portals/PortalSectionModel",
+  "views/portals/editor/PortEditorSectionView",
+  "views/portals/editor/PortEditorSettingsView",
+  "views/portals/editor/PortEditorDataView",
+  "views/portals/editor/PortEditorMdSectionView",
+  "text!templates/portals/editor/portEditorSections.html",
+  "text!templates/portals/editor/portEditorMetrics.html",
+  "text!templates/portals/editor/portEditorSectionLink.html",
+  "text!templates/portals/editor/portEditorSectionOptionImgs/metrics.svg",
+], function (
+  _,
+  $,
+  Backbone,
+  Sortable,
+  Portal,
+  PortalSection,
+  PortEditorSectionView,
+  PortEditorSettingsView,
+  PortEditorDataView,
+  PortEditorMdSectionView,
+  Template,
+  MetricsSectionTemplate,
+  SectionLinkTemplate,
+  MetricsSVG,
+) {
   /**
-  * @class PortEditorSectionsView
-  * @classdesc A view of one or more Portal Editor sections
-  * @classcategory Views/Portals/Editor
-  * @extends Backbone.View
-  * @constructor
-  */
+   * @class PortEditorSectionsView
+   * @classdesc A view of one or more Portal Editor sections
+   * @classcategory Views/Portals/Editor
+   * @extends Backbone.View
+   * @constructor
+   */
   var PortEditorSectionsView = Backbone.View.extend(
-    /** @lends PortEditorSectionsView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "PortEditorSections",
-
-    /**
-    * The HTML tag name for this view's element
-    * @type {string}
-    */
-    tagName: "div",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: "port-editor-sections",
-
-    /**
-    * The PortalModel that is being edited
-    * @type {Portal}
-    */
-    model: undefined,
-
-    /**
-    * A reference to the currently active editor section. e.g. Data, Metrics, Settings, etc.
-    * @type {PortEditorSectionView}
-    */
-    activeSection: undefined,
-
-    /**
-    * The name of the active section when the view is first loaded. This is retrieved from the router/URL
-    * @type {string}
-    */
-    activeSectionLabel: undefined,
-
-    /**
-    * The unique labels for each section in this Portal
-    * @type {string[]}
-    */
-    sectionLabels: [],
-
-    /**
-    * The subviews contained within this view to be removed with onClose
-    * @type {Array}
-    */
-    subviews: new Array(),
-
-    /**
-    * A reference to the PortalEditorView
-    * @type {PortalEditorView}
-    */
-    editorView: undefined,
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    */
-    /**
+    /** @lends PortEditorSectionsView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "PortEditorSections",
+
+      /**
+       * The HTML tag name for this view's element
+       * @type {string}
+       */
+      tagName: "div",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className: "port-editor-sections",
+
+      /**
+       * The PortalModel that is being edited
+       * @type {Portal}
+       */
+      model: undefined,
+
+      /**
+       * A reference to the currently active editor section. e.g. Data, Metrics, Settings, etc.
+       * @type {PortEditorSectionView}
+       */
+      activeSection: undefined,
+
+      /**
+       * The name of the active section when the view is first loaded. This is retrieved from the router/URL
+       * @type {string}
+       */
+      activeSectionLabel: undefined,
+
+      /**
+       * The unique labels for each section in this Portal
+       * @type {string[]}
+       */
+      sectionLabels: [],
+
+      /**
+       * The subviews contained within this view to be removed with onClose
+       * @type {Array}
+       */
+      subviews: new Array(),
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      /**
     @type {Underscore.Template}
     */
-    template: _.template(Template),
-    /**
+      template: _.template(Template),
+      /**
     @type {Underscore.Template}
     */
-    sectionLinkTemplate: _.template(SectionLinkTemplate),
-    /**
+      sectionLinkTemplate: _.template(SectionLinkTemplate),
+      /**
     @type {Underscore.Template}
     */
-    metricsSectionTemplate: _.template(MetricsSectionTemplate),
-
-    /**
-    * A jQuery selector for the elements that are links to the individual sections
-    * @type {string}
-    */
-    sectionLinks: ".portal-section-link",
-    /**
-    * A jQuery selector for the element that the section links should be inserted into
-    * @type {string}
-    */
-    sectionLinksContainer: ".section-links-container",
-    /**
-    * A jQuery selector for the element that a single section link will be inserted into
-    * @type {string}
-    */
-    sectionLinkContainer: ".section-link-container",
-    /**
-    * A jQuery selector for the element that the editor sections should be inserted into
-    * @type {string}
-    */
-    sectionsContainer: ".sections-container",
-
-    /**
-    * A jQuery selector for the section elements
-    * @type {string}
-    */
-    sectionEls: ".port-editor-section",
-
-    /**
-    * A selector for link or tab elements that the user is allowed to re-order,
-    * starting from the sectionLinksContainer
-    * @type {string}
-    */
-    sortableLinksSelector: ">li:not(.unsortable)",
-
-    /**
-    * A class name for the handles on tabs that the user can drag to re-order
-    * @type {string}
-    */
-    handleClass: "handle",
-
-    /**
-    * A label for the section used to add a new page
-    * @type {string}
-    */
-    addPageLabel: "AddPage",
-
-    /**
-    * Flag to add section name to URL. Enabled by default.
-    * @type {boolean}
-    */
-    displaySectionInUrl: true,
-
-    /**
-    * @borrows PortalEditorView.newPortalTempName as newPortalTempName
-    */
-    newPortalTempName: "",
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "click .rename-section" : "renameSection",
-      "dblclick .portal-section-link": "renameSection",
-      "click .show-section"   : "showSection",
-      "click .portal-section-link"   : "handleSwitchSection",
-      "focusout .portal-section-link[contenteditable=true]" : "updateName",
-      "click .cancelled-section-removal" : "closePopovers",
-      "click .confirmed-section-removal" : "removeSection",
-      // both keyup and keydown events are needed for limitLabelLength function
-      "keyup .portal-section-link[contenteditable=true]"    : "limitLabelInput",
-      "keydown .portal-section-link[contenteditable=true]"  : "limitLabelInput",
-      "click #link-to-data"                                 :  "navigateToData"
-    },
-
-    /**
-    * Creates a new PortEditorSectionsView
-    * @constructs PortEditorSectionsView
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options){
-      //Reset arrays and objects set on this View, otherwise they will be shared across intances, causing errors
-      this.subviews = new Array();
-      this.editorView = null;
+      metricsSectionTemplate: _.template(MetricsSectionTemplate),
+
+      /**
+       * A jQuery selector for the elements that are links to the individual sections
+       * @type {string}
+       */
+      sectionLinks: ".portal-section-link",
+      /**
+       * A jQuery selector for the element that the section links should be inserted into
+       * @type {string}
+       */
+      sectionLinksContainer: ".section-links-container",
+      /**
+       * A jQuery selector for the element that a single section link will be inserted into
+       * @type {string}
+       */
+      sectionLinkContainer: ".section-link-container",
+      /**
+       * A jQuery selector for the element that the editor sections should be inserted into
+       * @type {string}
+       */
+      sectionsContainer: ".sections-container",
+
+      /**
+       * A jQuery selector for the section elements
+       * @type {string}
+       */
+      sectionEls: ".port-editor-section",
+
+      /**
+       * A selector for link or tab elements that the user is allowed to re-order,
+       * starting from the sectionLinksContainer
+       * @type {string}
+       */
+      sortableLinksSelector: ">li:not(.unsortable)",
+
+      /**
+       * A class name for the handles on tabs that the user can drag to re-order
+       * @type {string}
+       */
+      handleClass: "handle",
+
+      /**
+       * A label for the section used to add a new page
+       * @type {string}
+       */
+      addPageLabel: "AddPage",
+
+      /**
+       * Flag to add section name to URL. Enabled by default.
+       * @type {boolean}
+       */
+      displaySectionInUrl: true,
+
+      /**
+       * @borrows PortalEditorView.newPortalTempName as newPortalTempName
+       */
+      newPortalTempName: "",
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "click .rename-section": "renameSection",
+        "dblclick .portal-section-link": "renameSection",
+        "click .show-section": "showSection",
+        "click .portal-section-link": "handleSwitchSection",
+        "focusout .portal-section-link[contenteditable=true]": "updateName",
+        "click .cancelled-section-removal": "closePopovers",
+        "click .confirmed-section-removal": "removeSection",
+        // both keyup and keydown events are needed for limitLabelLength function
+        "keyup .portal-section-link[contenteditable=true]": "limitLabelInput",
+        "keydown .portal-section-link[contenteditable=true]": "limitLabelInput",
+        "click #link-to-data": "navigateToData",
+      },
+
+      /**
+       * Creates a new PortEditorSectionsView
+       * @constructs PortEditorSectionsView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        //Reset arrays and objects set on this View, otherwise they will be shared across intances, causing errors
+        this.subviews = new Array();
+        this.editorView = null;
 
-      // Get all the options and apply them to this view
-      if (options) {
+        // Get all the options and apply them to this view
+        if (options) {
           var optionKeys = Object.keys(options);
-          _.each(optionKeys, function(key, i) {
+          _.each(
+            optionKeys,
+            function (key, i) {
               this[key] = options[key];
-          }, this);
-      }
-    },
-
-    /**
-    * Renders the PortEditorSectionsView
-    */
-    render: function(){
+            },
+            this,
+          );
+        }
+      },
 
-      //Insert the template into the view
-      this.$el.html(this.template());
+      /**
+       * Renders the PortEditorSectionsView
+       */
+      render: function () {
+        //Insert the template into the view
+        this.$el.html(this.template());
 
-      //Render the Data section
-      this.renderDataSection();
+        //Render the Data section
+        this.renderDataSection();
 
-      //Render the Metrics section
-      this.renderMetricsSection();
+        //Render the Metrics section
+        this.renderMetricsSection();
 
-      //Render the Add Section tab
-      this.renderAddSection();
+        //Render the Add Section tab
+        this.renderAddSection();
 
-      //Render the Settings
-      this.renderSettings();
+        //Render the Settings
+        this.renderSettings();
 
-      //Render a Section View for each content section in the Portal
-      this.renderContentSections();
+        //Render a Section View for each content section in the Portal
+        this.renderContentSections();
 
-      // Disable the delete/hide section option if there is only one section
-      this.toggleRemoveSectionOption();
+        // Disable the delete/hide section option if there is only one section
+        this.toggleRemoveSectionOption();
 
-      var view = this,
+        var view = this,
           linksContainer = view.el.querySelector(view.sectionLinksContainer),
           sortableLinksSelector = view.sortableLinksSelector,
-          sortableLinks = view.el.querySelectorAll(view.sectionLinksContainer + view.sortableLinksSelector),
+          sortableLinks = view.el.querySelectorAll(
+            view.sectionLinksContainer + view.sortableLinksSelector,
+          ),
           sortableLinksArray = Array.prototype.slice.call(sortableLinks, 0),
           pageOrder = this.model.get("pageOrder");
 
-      // Arrange tabs in the order the user has pre-selected
-      try {
-        if(pageOrder && pageOrder.length){
-          // sort the links according the pageOrder
-          sortableLinksArray.sort(function(a,b){
-            var aName = $(a).data("section-name");
-            var bName = $(b).data("section-name");
-            var aIndex = pageOrder.indexOf(aName);
-            var bIndex = pageOrder.indexOf(bName);
-            // If the label can't be found in the list of labels, place it at the end
-            if(bIndex === -1){
-              return +1
-            }
-            if(aIndex === -1){
-              return -1
+        // Arrange tabs in the order the user has pre-selected
+        try {
+          if (pageOrder && pageOrder.length) {
+            // sort the links according the pageOrder
+            sortableLinksArray.sort(function (a, b) {
+              var aName = $(a).data("section-name");
+              var bName = $(b).data("section-name");
+              var aIndex = pageOrder.indexOf(aName);
+              var bIndex = pageOrder.indexOf(bName);
+              // If the label can't be found in the list of labels, place it at the end
+              if (bIndex === -1) {
+                return +1;
+              }
+              if (aIndex === -1) {
+                return -1;
+              }
+              // Sort backwards because we use prepend
+              return bIndex - aIndex;
+            });
+            // Rearrange the links in the DOM
+            for (i = 0; i < sortableLinksArray.length; ++i) {
+              // Use preprend so that Settings and AddPage tabs remain last in list
+              linksContainer.prepend(sortableLinksArray[i]);
             }
-            // Sort backwards because we use prepend
-            return bIndex - aIndex;
-          })
-          // Rearrange the links in the DOM
-          for (i = 0; i < sortableLinksArray.length; ++i) {
-            // Use preprend so that Settings and AddPage tabs remain last in list
-            linksContainer.prepend(sortableLinksArray[i]);
           }
+        } catch (error) {
+          console.log(
+            "Error re-arranging tabs according to the pageOrder option. Error message: " +
+              error,
+          );
         }
-      } catch (error) {
-        console.log("Error re-arranging tabs according to the pageOrder option. Error message: " + error)
-      }
-
-      // Initialize user-controlled tab re-ordering
-      var sortable = Sortable.create(linksContainer, {
-        direction: 'horizontal',
-        easing: "cubic-bezier(1, 0, 0, 1)",
-        animation: 200,
-        // Only tabs that have an element with this class will be draggable
-        handle: "." + view.handleClass,
-        draggable: sortableLinksSelector,
-        // When the tab order is changed, update the portal model option with new order
-        onUpdate: function (evt) {
-          view.updatePageOrder();
-        },
-      })
-
-      // Switch to the active section, if there is one
-      if( this.activeSectionLabel ){
-        this.activeSection = this.getSectionByLabel(this.activeSectionLabel);
-
-        //Switch to the active section
-        this.switchSection(this.activeSection);
-
-        //Reset the active section label, since it is only used during the initial rendering
-        this.activeSectionLabel = undefined;
-
-      }
-      else{
-        //Switch to the default section
-        this.switchSection();
-      }
-    },
-
-    /**
-    * Render a section for adding a new section
-    */
-    renderAddSection: function(){
-
-      //Create a unique label for this section and save it
-      this.updateSectionLabelsList(this.addPageLabel);
-
-      // Add a "Add section" button/tab
-      var addSectionView = new PortEditorSectionView({
-        model: this.model,
-        uniqueSectionLabel: this.addPageLabel,
-        sectionType: "addpage",
-        editorView: this.editorView
-      });
-
-      addSectionView.$el.addClass("tab-pane")
-        .addClass("port-editor-add-section-container")
-        .attr("id", this.addPageLabel);
-
-      //Add the section element to this view
-      this.$(this.sectionsContainer).append(addSectionView.$el);
-
-      //Render the section view
-      addSectionView.render();
 
-      // Add the tab to the tab navigation
-      this.addSectionLink(addSectionView);
-
-      // Replace the name "AddSection" with fontawsome "+" icon
-      this.$el.find( this.sectionLinkContainer + "[data-section-name='" + this.addPageLabel + "'] a")
-              .html("<i class='icon icon-plus'></i>")
-              .attr("title", "Add a new page");
-
-      // When a sectionOption is clicked in the addSectionView subview,
-      // the "addNewSection" event is triggered.
-      this.listenTo(addSectionView, "addNewSection", this.addSection);
-
-      //Add the view to the subviews array
-      this.subviews.push(addSectionView);
-
-    },
-
-    /**
-    * Render all sections in the editor for each content section in the Portal
-    */
-    renderContentSections: function(){
-
-      // Get the sections from the Portal
-      var sections = this.model.get("sections");
+        // Initialize user-controlled tab re-ordering
+        var sortable = Sortable.create(linksContainer, {
+          direction: "horizontal",
+          easing: "cubic-bezier(1, 0, 0, 1)",
+          animation: 200,
+          // Only tabs that have an element with this class will be draggable
+          handle: "." + view.handleClass,
+          draggable: sortableLinksSelector,
+          // When the tab order is changed, update the portal model option with new order
+          onUpdate: function (evt) {
+            view.updatePageOrder();
+          },
+        });
 
-      // Render each markdown (aka "freeform") section already in the PortalModel
-      _.each(sections, function(section){
+        // Switch to the active section, if there is one
+        if (this.activeSectionLabel) {
+          this.activeSection = this.getSectionByLabel(this.activeSectionLabel);
 
+          //Switch to the active section
+          this.switchSection(this.activeSection);
 
-        try{
-          if(section){
-            //Render the content section
-            this.renderContentSection(section);
-          }
-        }
-        catch(e){
-          console.error(e);
+          //Reset the active section label, since it is only used during the initial rendering
+          this.activeSectionLabel = undefined;
+        } else {
+          //Switch to the default section
+          this.switchSection();
         }
-      }, this);
+      },
 
-    },
+      /**
+       * Render a section for adding a new section
+       */
+      renderAddSection: function () {
+        //Create a unique label for this section and save it
+        this.updateSectionLabelsList(this.addPageLabel);
 
-    /**
-    * Render a single markdown section in the editor (sectionView + link)
-    * @param {PortalSectionModel} section - The section to render
-    * @param {boolean} isNew - If true, this section will be rendered as a section that was just added by the user
-    */
-    renderContentSection: function(section, isNew){
+        // Add a "Add section" button/tab
+        var addSectionView = new PortEditorSectionView({
+          model: this.model,
+          uniqueSectionLabel: this.addPageLabel,
+          sectionType: "addpage",
+          editorView: this.editorView,
+        });
 
-      try{
+        addSectionView.$el
+          .addClass("tab-pane")
+          .addClass("port-editor-add-section-container")
+          .attr("id", this.addPageLabel);
 
-        if( typeof isNew == "undefined" || isNew == null) {
-          var isNew = false;
-        }
+        //Add the section element to this view
+        this.$(this.sectionsContainer).append(addSectionView.$el);
 
-        if(section){
+        //Render the section view
+        addSectionView.render();
 
-          // Create and render and markdown section view
-          var sectionView = new PortEditorMdSectionView({
-            model: section
-          });
+        // Add the tab to the tab navigation
+        this.addSectionLink(addSectionView);
+
+        // Replace the name "AddSection" with fontawsome "+" icon
+        this.$el
+          .find(
+            this.sectionLinkContainer +
+              "[data-section-name='" +
+              this.addPageLabel +
+              "'] a",
+          )
+          .html("<i class='icon icon-plus'></i>")
+          .attr("title", "Add a new page");
+
+        // When a sectionOption is clicked in the addSectionView subview,
+        // the "addNewSection" event is triggered.
+        this.listenTo(addSectionView, "addNewSection", this.addSection);
+
+        //Add the view to the subviews array
+        this.subviews.push(addSectionView);
+      },
+
+      /**
+       * Render all sections in the editor for each content section in the Portal
+       */
+      renderContentSections: function () {
+        // Get the sections from the Portal
+        var sections = this.model.get("sections");
+
+        // Render each markdown (aka "freeform") section already in the PortalModel
+        _.each(
+          sections,
+          function (section) {
+            try {
+              if (section) {
+                //Render the content section
+                this.renderContentSection(section);
+              }
+            } catch (e) {
+              console.error(e);
+            }
+          },
+          this,
+        );
+      },
+
+      /**
+       * Render a single markdown section in the editor (sectionView + link)
+       * @param {PortalSectionModel} section - The section to render
+       * @param {boolean} isNew - If true, this section will be rendered as a section that was just added by the user
+       */
+      renderContentSection: function (section, isNew) {
+        try {
+          if (typeof isNew == "undefined" || isNew == null) {
+            var isNew = false;
+          }
 
-          // Pass the portal editor view onto the section
-          sectionView.editorView = this.editorView;
+          if (section) {
+            // Create and render and markdown section view
+            var sectionView = new PortEditorMdSectionView({
+              model: section,
+            });
 
-          //Create a unique label for this section and save it
-          var uniqueLabel = this.getUniqueSectionLabel(section);
+            // Pass the portal editor view onto the section
+            sectionView.editorView = this.editorView;
 
-          //Set the unique section label for this view
-          sectionView.uniqueSectionLabel = uniqueLabel;
+            //Create a unique label for this section and save it
+            var uniqueLabel = this.getUniqueSectionLabel(section);
 
-          this.updateSectionLabelsList(uniqueLabel);
+            //Set the unique section label for this view
+            sectionView.uniqueSectionLabel = uniqueLabel;
 
-          //Attach the editor view to this view
-          sectionView.editorView = this.editorView;
+            this.updateSectionLabelsList(uniqueLabel);
 
-          sectionView.$el.attr("id", uniqueLabel);
+            //Attach the editor view to this view
+            sectionView.editorView = this.editorView;
 
-          //Insert the PortEditorMdSectionView element into this view
-          this.$(this.sectionsContainer).append(sectionView.$el);
+            sectionView.$el.attr("id", uniqueLabel);
 
-          //Render the PortEditorMdSectionView
-          sectionView.render();
+            //Insert the PortEditorMdSectionView element into this view
+            this.$(this.sectionsContainer).append(sectionView.$el);
 
-          // Add the tab to the tab navigation
-          this.addSectionLink(sectionView, ["Rename", "Delete"], isNew);
+            //Render the PortEditorMdSectionView
+            sectionView.render();
 
-          // Add the sections to the list of subviews
-          this.subviews.push(sectionView);
+            // Add the tab to the tab navigation
+            this.addSectionLink(sectionView, ["Rename", "Delete"], isNew);
 
+            // Add the sections to the list of subviews
+            this.subviews.push(sectionView);
+          }
+        } catch (e) {
+          console.error(e);
         }
-      }
-      catch(e){
-        console.error(e);
-      }
-
-    },
+      },
 
-    /**
-    * Renders a Data section in this view
-    */
-    renderDataSection: function(){
-
-      try{
-
-        this.updateSectionLabelsList("Data");
-
-        // Render a PortEditorDataView and corresponding tab
-        var dataView = new PortEditorDataView({
-          model: this.model,
-          uniqueSectionLabel: "Data",
-          editorView: this.editorView
-        });
+      /**
+       * Renders a Data section in this view
+       */
+      renderDataSection: function () {
+        try {
+          this.updateSectionLabelsList("Data");
 
-        //Save a reference to this view
-        this.dataView = dataView;
+          // Render a PortEditorDataView and corresponding tab
+          var dataView = new PortEditorDataView({
+            model: this.model,
+            uniqueSectionLabel: "Data",
+            editorView: this.editorView,
+          });
 
-        //Add the view to the page
-        this.$(this.sectionsContainer).append(dataView.el);
+          //Save a reference to this view
+          this.dataView = dataView;
 
-        //Render the PortEditorDataView
-        dataView.render();
+          //Add the view to the page
+          this.$(this.sectionsContainer).append(dataView.el);
 
-        //Create the menu options for the Data section link
-        var menuOptions = [];
-        if( this.model.get("hideData") === true ){
-          menuOptions.push("Show");
-        }
-        else{
-          menuOptions.push("Hide");
-        }
+          //Render the PortEditorDataView
+          dataView.render();
 
-        // Add the tab to the tab navigation
-        this.addSectionLink(dataView, menuOptions);
-
-        //When the Data section has been hidden or shown, update the section link
-        this.stopListening(this.model, "change:hideData");
-        this.listenTo(this.model, "change:hideData", function(){
           //Create the menu options for the Data section link
           var menuOptions = [];
-          if( this.model.get("hideData") === true ){
+          if (this.model.get("hideData") === true) {
             menuOptions.push("Show");
-          }
-          else{
+          } else {
             menuOptions.push("Hide");
           }
 
-          this.updateSectionLink(dataView, menuOptions);
-        });
-
-        // Add the data section to the list of subviews
-        this.subviews.push(dataView);
-      }
-      catch(e){
-        console.error(e);
-      }
-    },
-
-    /**
-    * Renders the Metrics section of the editor
-    */
-    renderMetricsSection: function(){
-
-      // Render a PortEditorSectionView for the Metrics section if metrics is set
-      // to show, and the view hasn't already been rendered.
-      if(this.model.get("hideMetrics") !== true && !this.metricsView){
-
-        //Create a unique label for this section and save it
-        this.updateSectionLabelsList("Metrics");
-
-        //Create a section view
-        this.metricsView = new PortEditorSectionView({
-          model: this.model,
-          uniqueSectionLabel: "Metrics",
-          template: this.metricsSectionTemplate,
-          sectionType: "metrics",
-          editorView: this.editorView
-        });
-
-        this.metricsView.$el.attr("id", "Metrics");
-        this.$(this.sectionsContainer).append(this.metricsView.el);
-
-        //Render the view
-        this.metricsView.render();
-
-        // Insert the metrics illustration
-        $(this.metricsView.el).find(".metrics-figure-container").html(MetricsSVG);
-
-        // Add the data section to the list of subviews
-        this.subviews.push(this.metricsView);
+          // Add the tab to the tab navigation
+          this.addSectionLink(dataView, menuOptions);
+
+          //When the Data section has been hidden or shown, update the section link
+          this.stopListening(this.model, "change:hideData");
+          this.listenTo(this.model, "change:hideData", function () {
+            //Create the menu options for the Data section link
+            var menuOptions = [];
+            if (this.model.get("hideData") === true) {
+              menuOptions.push("Show");
+            } else {
+              menuOptions.push("Hide");
+            }
 
-      }
-      //If the metrics aren't hidden AND the metrics view was created already, then show it
-      else if( this.model.get("hideMetrics") !== true && this.metricsView ){
+            this.updateSectionLink(dataView, menuOptions);
+          });
 
-        this.$(this.sectionsContainer).append(this.metricsView.$el);
-        this.metricsView.$el.data({
-          view: this.metricsView,
-          model: this.model
-        })
+          // Add the data section to the list of subviews
+          this.subviews.push(dataView);
+        } catch (e) {
+          console.error(e);
+        }
+      },
+
+      /**
+       * Renders the Metrics section of the editor
+       */
+      renderMetricsSection: function () {
+        // Render a PortEditorSectionView for the Metrics section if metrics is set
+        // to show, and the view hasn't already been rendered.
+        if (this.model.get("hideMetrics") !== true && !this.metricsView) {
+          //Create a unique label for this section and save it
+          this.updateSectionLabelsList("Metrics");
+
+          //Create a section view
+          this.metricsView = new PortEditorSectionView({
+            model: this.model,
+            uniqueSectionLabel: "Metrics",
+            template: this.metricsSectionTemplate,
+            sectionType: "metrics",
+            editorView: this.editorView,
+          });
 
-      }
+          this.metricsView.$el.attr("id", "Metrics");
+          this.$(this.sectionsContainer).append(this.metricsView.el);
 
-      //When the metrics section has been toggled, remove or add the link
-      this.toggleMetricsLink();
+          //Render the view
+          this.metricsView.render();
 
-    },
+          // Insert the metrics illustration
+          $(this.metricsView.el)
+            .find(".metrics-figure-container")
+            .html(MetricsSVG);
 
+          // Add the data section to the list of subviews
+          this.subviews.push(this.metricsView);
+        }
+        //If the metrics aren't hidden AND the metrics view was created already, then show it
+        else if (this.model.get("hideMetrics") !== true && this.metricsView) {
+          this.$(this.sectionsContainer).append(this.metricsView.$el);
+          this.metricsView.$el.data({
+            view: this.metricsView,
+            model: this.model,
+          });
+        }
 
-    /**
-     * navigateToData - Navigate to the data tab.
-     */
-    navigateToData: function(){
-      if(this.dataView){
-        this.switchSection(this.dataView);
-      }
-    },
+        //When the metrics section has been toggled, remove or add the link
+        this.toggleMetricsLink();
+      },
 
-    /**
-    * Adds or removes the metrics link depending on the 'hideMetrics' option in
-    * the model.
-    */
-    toggleMetricsLink: function(){
+      /**
+       * navigateToData - Navigate to the data tab.
+       */
+      navigateToData: function () {
+        if (this.dataView) {
+          this.switchSection(this.dataView);
+        }
+      },
 
-      try{
-        // Need a metrics view to exist already if metrics is set to show
-      /*  if(!this.metricsView && !this.model.get("hideMetrics") === true){
+      /**
+       * Adds or removes the metrics link depending on the 'hideMetrics' option in
+       * the model.
+       */
+      toggleMetricsLink: function () {
+        try {
+          // Need a metrics view to exist already if metrics is set to show
+          /*  if(!this.metricsView && !this.model.get("hideMetrics") === true){
           this.renderMetricsSection();
         }*/
-        //If hideMetrics has been set to true, remove the link
-        if( this.model.get("hideMetrics") === true ){
-          this.removeSectionLink(this.metricsView);
-        // Otherwise add it
-        } else {
-          this.addSectionLink(this.metricsView, ["Delete"]);
+          //If hideMetrics has been set to true, remove the link
+          if (this.model.get("hideMetrics") === true) {
+            this.removeSectionLink(this.metricsView);
+            // Otherwise add it
+          } else {
+            this.addSectionLink(this.metricsView, ["Delete"]);
+          }
+        } catch (e) {
+          console.error(e);
         }
-      }
-      catch(e){
-        console.error(e);
-      }
-    },
+      },
 
-    /**
-    * Renders the Settings section of the editor
-    */
-    renderSettings: function(){
-
-      //Create a unique label for this section and save it
-      this.updateSectionLabelsList("Settings");
-
-      //Create a PortEditorSettingsView
-      var settingsView = new PortEditorSettingsView({
-        model: this.model,
-        uniqueSectionLabel: "Settings"
-      });
-
-      settingsView.editorView = this.editorView;
-
-      //Add the Settings view to the page
-      this.$(this.sectionsContainer).append(settingsView.$el);
+      /**
+       * Renders the Settings section of the editor
+       */
+      renderSettings: function () {
+        //Create a unique label for this section and save it
+        this.updateSectionLabelsList("Settings");
 
-      //Render the PortEditorSettingsView
-      settingsView.render();
+        //Create a PortEditorSettingsView
+        var settingsView = new PortEditorSettingsView({
+          model: this.model,
+          uniqueSectionLabel: "Settings",
+        });
 
-      //Create and add a section link
-      this.addSectionLink(settingsView);
+        settingsView.editorView = this.editorView;
 
-      // Add the data section to the list of subviews
-      this.subviews.push(settingsView);
+        //Add the Settings view to the page
+        this.$(this.sectionsContainer).append(settingsView.$el);
 
-    },
+        //Render the PortEditorSettingsView
+        settingsView.render();
 
-    /**
-     * Update the window location path with the active section name
-     * @param {boolean} [showSectionLabel] - If true, the editor section label will be added to the path
-    */
-    updatePath: function(showSectionLabel){
+        //Create and add a section link
+        this.addSectionLink(settingsView);
 
-      //Get the current portal label
-      var label         = this.model.get("label") || "",
-      //Get the last-saved portal label
+        // Add the data section to the list of subviews
+        this.subviews.push(settingsView);
+      },
+
+      /**
+       * Update the window location path with the active section name
+       * @param {boolean} [showSectionLabel] - If true, the editor section label will be added to the path
+       */
+      updatePath: function (showSectionLabel) {
+        //Get the current portal label
+        var label = this.model.get("label") || "",
+          //Get the last-saved portal label
           originalLabel = this.model.get("originalLabel") || "",
-      //Get the current path from the window location
-          pathName      = decodeURIComponent(window.location.pathname)
-                          .substring(MetacatUI.root.length)
-                          // remove trailing forward slash if one exists in path
-                          .replace(/\/$/, "");
-
-      // Add or replace the label part of the path with the new label.
-      // pathRE matches "/label/section", where the "/section" part is optional
-      var pathRE = new RegExp("\\/(" + label + "|" + originalLabel + ")(\\/[^\\/]*)?$", "i");
-      newPathName = pathName.replace(pathRE, "");
-
-      //If there is a label, add it to the new path.
-      // (there will always be a label unless this is a new portal)
-      if( label ){
-        newPathName += "/" + label;
-      }
-
-      //If there is an active section, and the showSectionLabel parameter is true, add the section label to the path.
-      if( showSectionLabel && this.activeSection ){
-        newPathName += "/" + this.activeSection.uniqueSectionLabel;
-      }
-
-      //If the path has changed, navigate to the new path, which creates a record in the browser history
-      if( pathName != newPathName ){
-        // Update the window location
-        MetacatUI.uiRouter.navigate( newPathName, {
-          trigger: false
-        });
-      }
-
-    },
-
-    /**
-    * Returns the section view that has a label matching the one given.
-    * @param {string} label - The label for the section
-    * @return {PortEditorSectionView|false} - Returns false if a matching section view isn't found
-    */
-    getSectionByLabel: function(label){
+          //Get the current path from the window location
+          pathName = decodeURIComponent(window.location.pathname)
+            .substring(MetacatUI.root.length)
+            // remove trailing forward slash if one exists in path
+            .replace(/\/$/, "");
+
+        // Add or replace the label part of the path with the new label.
+        // pathRE matches "/label/section", where the "/section" part is optional
+        var pathRE = new RegExp(
+          "\\/(" + label + "|" + originalLabel + ")(\\/[^\\/]*)?$",
+          "i",
+        );
+        newPathName = pathName.replace(pathRE, "");
+
+        //If there is a label, add it to the new path.
+        // (there will always be a label unless this is a new portal)
+        if (label) {
+          newPathName += "/" + label;
+        }
 
-      //If no label is given, exit
-      if(!label){
-        return;
-      }
+        //If there is an active section, and the showSectionLabel parameter is true, add the section label to the path.
+        if (showSectionLabel && this.activeSection) {
+          newPathName += "/" + this.activeSection.uniqueSectionLabel;
+        }
 
-      //Find the section view whose unique label matches the given label. Case-insensitive matching.
-      return _.find( this.subviews, function(view){
-        if( typeof view.uniqueSectionLabel == "string" ){
-          return view.uniqueSectionLabel.toLowerCase() == label.toLowerCase();
+        //If the path has changed, navigate to the new path, which creates a record in the browser history
+        if (pathName != newPathName) {
+          // Update the window location
+          MetacatUI.uiRouter.navigate(newPathName, {
+            trigger: false,
+          });
         }
-        else{
-          return false;
+      },
+
+      /**
+       * Returns the section view that has a label matching the one given.
+       * @param {string} label - The label for the section
+       * @return {PortEditorSectionView|false} - Returns false if a matching section view isn't found
+       */
+      getSectionByLabel: function (label) {
+        //If no label is given, exit
+        if (!label) {
+          return;
         }
-      });
-    },
-
-    /**
-    * Returns the section view that has a label matching the one given.
-    * @param {PortalSectionModel} section - The section model
-    * @return {PortEditorSectionView|false} - Returns false if a matching section view isn't found
-    */
-    getSectionByModel: function(section){
-
-      //If no section is given, exit
-      if(!section){
-        return;
-      }
 
-      //Find the section view whose unique label matches the given label. Case-insensitive matching.
-      return _.findWhere( this.subviews, { model: section });
-    },
+        //Find the section view whose unique label matches the given label. Case-insensitive matching.
+        return _.find(this.subviews, function (view) {
+          if (typeof view.uniqueSectionLabel == "string") {
+            return view.uniqueSectionLabel.toLowerCase() == label.toLowerCase();
+          } else {
+            return false;
+          }
+        });
+      },
+
+      /**
+       * Returns the section view that has a label matching the one given.
+       * @param {PortalSectionModel} section - The section model
+       * @return {PortEditorSectionView|false} - Returns false if a matching section view isn't found
+       */
+      getSectionByModel: function (section) {
+        //If no section is given, exit
+        if (!section) {
+          return;
+        }
 
-    /**
-    * Creates and returns a unique label for the given section. This label is just used in the view,
-    * because portal sections can have duplicate labels. But unique labels need to be used for navigation in the view.
-    * @param {PortEditorSection} sectionModel - The section for which to create a unique label
-    * @return {string} The unique label string
-    */
-    getUniqueSectionLabel: function(sectionModel){
-      //Get the label for this section
-      var sectionLabel = sectionModel.get("label").replace(/[^a-zA-Z0-9 ]/g, "").replace(/ /g, "-"),
+        //Find the section view whose unique label matches the given label. Case-insensitive matching.
+        return _.findWhere(this.subviews, { model: section });
+      },
+
+      /**
+       * Creates and returns a unique label for the given section. This label is just used in the view,
+       * because portal sections can have duplicate labels. But unique labels need to be used for navigation in the view.
+       * @param {PortEditorSection} sectionModel - The section for which to create a unique label
+       * @return {string} The unique label string
+       */
+      getUniqueSectionLabel: function (sectionModel) {
+        //Get the label for this section
+        var sectionLabel = sectionModel
+            .get("label")
+            .replace(/[^a-zA-Z0-9 ]/g, "")
+            .replace(/ /g, "-"),
           unalteredLabel = sectionLabel,
           sectionLabels = this.sectionLabels || [],
           i = 2;
 
-      //Concatenate a number to the label if this one already exists
-      while( sectionLabels.includes(sectionLabel) ){
-        sectionLabel = unalteredLabel + i;
-        i++;
-      }
-
-      return sectionLabel;
-    },
-
-    /**
-    * Manually switch to a section subview by making the tab and tab panel active.
-    * Navigation between sections is usually handled automatically by the Bootstrap
-    * library, but a manual switch may be necessary sometimes
-    * @param {PortEditorSectionView} [sectionView] - The section view to switch to. If not given, defaults to the activeSection set on the view.
-    */
-    switchSection: function(sectionView){
-
-      //Create a flag for whether the section label should be shown in the URL
-      var showSectionLabelInURL = true;
-
-      // If no section view is given, use the active section in the view.
-      if( !sectionView ){
-        //Use the sectionView set already
-        if( this.activeSection ){
-          var sectionView = this.activeSection;
-        }
-        //Or find the section view by name, which may have been passed through the URL
-        else if( this.activeSectionLabel ){
-          var sectionView = this.getSectionByLabel(this.activeSectionLabel);
-        }
-      }
-
-      //If no section view was indicated, just default to the first visible one
-      if( !sectionView ){
-        var sectionView = this.$(this.sectionLinkContainer + ":not(.removing)").first().data("view");
-
-        //If we are defaulting to the first section, don't show the section label in the URL
-        showSectionLabelInURL = false;
-
-        //If there are no section views on the page at all, exit now
-        if( !sectionView ){
-          return;
+        //Concatenate a number to the label if this one already exists
+        while (sectionLabels.includes(sectionLabel)) {
+          sectionLabel = unalteredLabel + i;
+          i++;
         }
-      }
 
-      // Update the activeSection set on the view
-      this.activeSection = sectionView;
-
-      // Activate the section content
-      this.$(this.sectionEls).each(function(i, contentEl){
-        if($(contentEl).data("view") == sectionView){
-          $(contentEl).addClass("active");
-          sectionView.trigger("active");
-        } else {
-          // make sure no other sections are active
-          $(contentEl).removeClass("active");
+        return sectionLabel;
+      },
+
+      /**
+       * Manually switch to a section subview by making the tab and tab panel active.
+       * Navigation between sections is usually handled automatically by the Bootstrap
+       * library, but a manual switch may be necessary sometimes
+       * @param {PortEditorSectionView} [sectionView] - The section view to switch to. If not given, defaults to the activeSection set on the view.
+       */
+      switchSection: function (sectionView) {
+        //Create a flag for whether the section label should be shown in the URL
+        var showSectionLabelInURL = true;
+
+        // If no section view is given, use the active section in the view.
+        if (!sectionView) {
+          //Use the sectionView set already
+          if (this.activeSection) {
+            var sectionView = this.activeSection;
+          }
+          //Or find the section view by name, which may have been passed through the URL
+          else if (this.activeSectionLabel) {
+            var sectionView = this.getSectionByLabel(this.activeSectionLabel);
+          }
         }
-      });
-
-      // Activate the link to the content
-      this.$(this.sectionLinkContainer).each(function(i, linkEl){
-        if( $(linkEl).data("view") == sectionView ){
-          $(linkEl).addClass("active")
-        } else {
-          // make sure no other sections are active
-          $(linkEl).removeClass("active")
-        };
-      });
-
-      //Never show the section label in the URL, since it messes with the back/forward browser navigation. See #1364
-      showSectionLabelInURL = false;
-      //Update the location path
-      this.updatePath(showSectionLabelInURL);
-
-    },
-
-    /**
-    * When a section link has been clicked, switch to that section
-    * @param {Event} e - The click event on the section link
-    */
-    handleSwitchSection: function(e){
-
-      e.preventDefault();
 
-      // Make sure any markdown editor toolbar modals are closed
-      // (otherwise they persist in new tab)
-      $("body").find(".wk-prompt").remove();
+        //If no section view was indicated, just default to the first visible one
+        if (!sectionView) {
+          var sectionView = this.$(
+            this.sectionLinkContainer + ":not(.removing)",
+          )
+            .first()
+            .data("view");
 
-      // Make sure any markdown editor toolbar modals are closed
-      // (otherwise they persist in new tab)
-      $("body").find(".wk-prompt").remove();
+          //If we are defaulting to the first section, don't show the section label in the URL
+          showSectionLabelInURL = false;
 
-      var sectionView = $(e.target).parents(this.sectionLinkContainer).first().data("view");
-
-      if( sectionView ){
-        this.switchSection(sectionView);
-        // If the user clicks a link and is not near the top of the page
-        // (i.e. on mobile), scroll to the top of the section content.
-        // Otherwise it might look like the page hasn't changed
-        if(window.pageYOffset > $("#editor-body").offset().top){
-          MetacatUI.appView.scrollTo($("#editor-body"));
+          //If there are no section views on the page at all, exit now
+          if (!sectionView) {
+            return;
+          }
         }
-      }
-
-    },
-
-    /**
-    * Add a link to the given editor section
-    * @param {PortEditorSectionView} sectionView - The view to add a link to
-    * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
-    * @param {boolean} isFocused - A boolean flag to enable focus on new section link
-    */
-    addSectionLink: function(sectionView, menuOptions, isFocused){
 
-      try{
+        // Update the activeSection set on the view
+        this.activeSection = sectionView;
 
-        if (typeof isFocused != "boolean") {
-          var isFocused = false;
-        }
-
-        var view = this;
-
-        var newLink = this.createSectionLink(sectionView, menuOptions);
-        var isMarkdownSection = $(newLink).data("view").type == "PortEditorMdSection"
-
-        // Make the tab hidden to start
-        $(newLink)
-          .find(this.sectionLinks)
-          .css('max-width','0px')
-          .css('opacity','0.2')
-          .css('white-space', 'nowrap');
-
-        $(newLink)
-          .find(".section-menu-link")
-          .css('opacity','0.5')
-          .css('transition', 'opacity 0.1s');
-
-        // Find the "+" link to help determine the order in which we should add links
-        var addSectionEl = this.$(this.sectionLinksContainer)
-                               .find(this.sectionLinkContainer + "[data-section-name='" + this.addPageLabel + "']")[0];
-
-        // If the new link is for a markdown section and there's no user-defined page
-        // order, then insert the markdown sections before the data and metrics section
-        // (this is the default order when there is no page ordering).
-        if(
-          isMarkdownSection &&
-          (!view.model.get("pageOrder") || !view.model.get("pageOrder").length)
-        ){
-        
-          // Find the last markdown section in the list of links
-          var currentLinks = this.$(this.sectionLinksContainer).find(this.sectionLinkContainer);
-          var i = _.map(currentLinks, function (li) {
-            return $(li).data("view") ? $(li).data("view").type : "";
-          }).lastIndexOf("PortEditorMdSection");
-          var lastMdSection = currentLinks[i];
-          // Append the new link after the last markdown section, or add it first.
-          if (lastMdSection) {
-            $(lastMdSection).after(newLink);
+        // Activate the section content
+        this.$(this.sectionEls).each(function (i, contentEl) {
+          if ($(contentEl).data("view") == sectionView) {
+            $(contentEl).addClass("active");
+            sectionView.trigger("active");
           } else {
-            this.$(this.sectionLinksContainer).prepend(newLink);
+            // make sure no other sections are active
+            $(contentEl).removeClass("active");
           }
-        // If there is already some user-defined page ordering, or if not a markdown
-        // section and not the Settings section, and if there is already a "+" link, add
-        // new link before the "+" link
-        } else if (addSectionEl && sectionView.uniqueSectionLabel != "Settings"){
-          $(addSectionEl).before(newLink);
-        // If the new link is "Settings", or there's no "+" link yet, insert new link last.
-        } else {
-          this.$(this.sectionLinksContainer).append(newLink);
-        }
+        });
 
-        // If this is a newly added markdown section, highlight the section name and make
-        // it content editable. Currently only markdown sections labels are editable.
-        if (isFocused && isMarkdownSection) {
-          var newSectionLink = $(newLink).children(".portal-section-link");
-          newSectionLink.attr("contenteditable", true);
-          newSectionLink.focus();
+        // Activate the link to the content
+        this.$(this.sectionLinkContainer).each(function (i, linkEl) {
+          if ($(linkEl).data("view") == sectionView) {
+            $(linkEl).addClass("active");
+          } else {
+            // make sure no other sections are active
+            $(linkEl).removeClass("active");
+          }
+        });
 
-          //Select the text of the link
-          if (window.getSelection && window.document.createRange) {
-            var selection = window.getSelection();
-            var range = window.document.createRange();
-            range.selectNodeContents(newSectionLink[0]);
-            selection.removeAllRanges();
-            selection.addRange(range);
-          } else if (window.document.body.createTextRange) {
-            range = window.document.body.createTextRange();
-            range.moveToElementText(newSectionLink[0]);
-            range.select();
+        //Never show the section label in the URL, since it messes with the back/forward browser navigation. See #1364
+        showSectionLabelInURL = false;
+        //Update the location path
+        this.updatePath(showSectionLabelInURL);
+      },
+
+      /**
+       * When a section link has been clicked, switch to that section
+       * @param {Event} e - The click event on the section link
+       */
+      handleSwitchSection: function (e) {
+        e.preventDefault();
+
+        // Make sure any markdown editor toolbar modals are closed
+        // (otherwise they persist in new tab)
+        $("body").find(".wk-prompt").remove();
+
+        // Make sure any markdown editor toolbar modals are closed
+        // (otherwise they persist in new tab)
+        $("body").find(".wk-prompt").remove();
+
+        var sectionView = $(e.target)
+          .parents(this.sectionLinkContainer)
+          .first()
+          .data("view");
+
+        if (sectionView) {
+          this.switchSection(sectionView);
+          // If the user clicks a link and is not near the top of the page
+          // (i.e. on mobile), scroll to the top of the section content.
+          // Otherwise it might look like the page hasn't changed
+          if (window.pageYOffset > $("#editor-body").offset().top) {
+            MetacatUI.appView.scrollTo($("#editor-body"));
           }
         }
-
-        // Animate the link to full width / opacity
-        $(newLink).find(this.sectionLinks).animate({
-            'max-width': "500px",
-            overflow: "hidden",
-            opacity: 1
-          }, {
-          duration: 300,
-          complete: function(){
-            $(newLink)
-              .find(".section-menu-link")
-              .css('opacity','1')
+      },
+
+      /**
+       * Add a link to the given editor section
+       * @param {PortEditorSectionView} sectionView - The view to add a link to
+       * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
+       * @param {boolean} isFocused - A boolean flag to enable focus on new section link
+       */
+      addSectionLink: function (sectionView, menuOptions, isFocused) {
+        try {
+          if (typeof isFocused != "boolean") {
+            var isFocused = false;
           }
-        });
-      }
-      catch(e) {
-        console.error("Could not add a new section link. Error message: "+ e);
-      }
 
-    },
+          var view = this;
+
+          var newLink = this.createSectionLink(sectionView, menuOptions);
+          var isMarkdownSection =
+            $(newLink).data("view").type == "PortEditorMdSection";
+
+          // Make the tab hidden to start
+          $(newLink)
+            .find(this.sectionLinks)
+            .css("max-width", "0px")
+            .css("opacity", "0.2")
+            .css("white-space", "nowrap");
+
+          $(newLink)
+            .find(".section-menu-link")
+            .css("opacity", "0.5")
+            .css("transition", "opacity 0.1s");
+
+          // Find the "+" link to help determine the order in which we should add links
+          var addSectionEl = this.$(this.sectionLinksContainer).find(
+            this.sectionLinkContainer +
+              "[data-section-name='" +
+              this.addPageLabel +
+              "']",
+          )[0];
+
+          // If the new link is for a markdown section and there's no user-defined page
+          // order, then insert the markdown sections before the data and metrics section
+          // (this is the default order when there is no page ordering).
+          if (
+            isMarkdownSection &&
+            (!view.model.get("pageOrder") ||
+              !view.model.get("pageOrder").length)
+          ) {
+            // Find the last markdown section in the list of links
+            var currentLinks = this.$(this.sectionLinksContainer).find(
+              this.sectionLinkContainer,
+            );
+            var i = _.map(currentLinks, function (li) {
+              return $(li).data("view") ? $(li).data("view").type : "";
+            }).lastIndexOf("PortEditorMdSection");
+            var lastMdSection = currentLinks[i];
+            // Append the new link after the last markdown section, or add it first.
+            if (lastMdSection) {
+              $(lastMdSection).after(newLink);
+            } else {
+              this.$(this.sectionLinksContainer).prepend(newLink);
+            }
+            // If there is already some user-defined page ordering, or if not a markdown
+            // section and not the Settings section, and if there is already a "+" link, add
+            // new link before the "+" link
+          } else if (
+            addSectionEl &&
+            sectionView.uniqueSectionLabel != "Settings"
+          ) {
+            $(addSectionEl).before(newLink);
+            // If the new link is "Settings", or there's no "+" link yet, insert new link last.
+          } else {
+            this.$(this.sectionLinksContainer).append(newLink);
+          }
 
-    /**
-     * toggleRemoveSectionOption - Disables the hide and remove option from
-     * section link if it's the only section left. Re-enables the remove/hide
-     * link when a new section is added. Called on initial render and each time
-     * a section is added, removed, shown, or hidden.
-     */
-    toggleRemoveSectionOption: function(){
-      try {
-
-        // Determine the number of pages (sections + metrics + data)
-        var totalPages         = this.model.get("sections").length +
-                                  !this.model.get("hideMetrics") +
-                                  !this.model.get("hideData"),
-            removeSectionLinks = this.$(this.sectionLinkContainer)
-                                  .find(".remove-section");
-
-        // If there's just one section, hide the delete and hide option on last
-        // remaining section link
-        if(totalPages == 1){
-
-          removeSectionLinks.addClass("disabled");
-
-          if(!removeSectionLinks.closest("li").find(".tooltip").length){
-            removeSectionLinks.closest("li").tooltip({
-              placement: "bottom",
-              trigger: "hover",
-              title: "At least one displayed page is required. To remove this page, first add or show another page."
-            });
+          // If this is a newly added markdown section, highlight the section name and make
+          // it content editable. Currently only markdown sections labels are editable.
+          if (isFocused && isMarkdownSection) {
+            var newSectionLink = $(newLink).children(".portal-section-link");
+            newSectionLink.attr("contenteditable", true);
+            newSectionLink.focus();
+
+            //Select the text of the link
+            if (window.getSelection && window.document.createRange) {
+              var selection = window.getSelection();
+              var range = window.document.createRange();
+              range.selectNodeContents(newSectionLink[0]);
+              selection.removeAllRanges();
+              selection.addRange(range);
+            } else if (window.document.body.createTextRange) {
+              range = window.document.body.createTextRange();
+              range.moveToElementText(newSectionLink[0]);
+              range.select();
+            }
           }
 
-        // If there are 2 sections, re-show the delete or hide options.
-        } else if(totalPages == 2){
+          // Animate the link to full width / opacity
+          $(newLink)
+            .find(this.sectionLinks)
+            .animate(
+              {
+                "max-width": "500px",
+                overflow: "hidden",
+                opacity: 1,
+              },
+              {
+                duration: 300,
+                complete: function () {
+                  $(newLink).find(".section-menu-link").css("opacity", "1");
+                },
+              },
+            );
+        } catch (e) {
+          console.error(
+            "Could not add a new section link. Error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * toggleRemoveSectionOption - Disables the hide and remove option from
+       * section link if it's the only section left. Re-enables the remove/hide
+       * link when a new section is added. Called on initial render and each time
+       * a section is added, removed, shown, or hidden.
+       */
+      toggleRemoveSectionOption: function () {
+        try {
+          // Determine the number of pages (sections + metrics + data)
+          var totalPages =
+              this.model.get("sections").length +
+              !this.model.get("hideMetrics") +
+              !this.model.get("hideData"),
+            removeSectionLinks = this.$(this.sectionLinkContainer).find(
+              ".remove-section",
+            );
+
+          // If there's just one section, hide the delete and hide option on last
+          // remaining section link
+          if (totalPages == 1) {
+            removeSectionLinks.addClass("disabled");
+
+            if (!removeSectionLinks.closest("li").find(".tooltip").length) {
+              removeSectionLinks.closest("li").tooltip({
+                placement: "bottom",
+                trigger: "hover",
+                title:
+                  "At least one displayed page is required. To remove this page, first add or show another page.",
+              });
+            }
 
-          removeSectionLinks.removeClass("disabled");
-          removeSectionLinks.closest("li").tooltip("destroy");
+            // If there are 2 sections, re-show the delete or hide options.
+          } else if (totalPages == 2) {
+            removeSectionLinks.removeClass("disabled");
+            removeSectionLinks.closest("li").tooltip("destroy");
 
-        // If there are three or more pages, nothing needs to be changed.
-        } else {
-          return
+            // If there are three or more pages, nothing needs to be changed.
+          } else {
+            return;
+          }
+        } catch (e) {
+          console.log(
+            "Failure to show/hide the remove section option. Error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * Add a link to the given editor section
+       * @param {PortEditorSectionView} sectionView - The view to add a link to
+       * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
+       * @return {Element}
+       */
+      createSectionLink: function (sectionView, menuOptions) {
+        var classes = "";
+        if (Array.isArray(menuOptions) && menuOptions.includes("Show")) {
+          classes = "hidden-section";
         }
 
+        // Do not allow dragging/sorting of the Settings or Add Page sections
+        var sortable = true;
+        if (["addpage", "settings"].includes(sectionView.sectionType)) {
+          sortable = false;
+        }
 
-      } catch (e) {
-        console.log("Failure to show/hide the remove section option. Error message: " + e);
-      }
-    },
-
-    /**
-    * Add a link to the given editor section
-    * @param {PortEditorSectionView} sectionView - The view to add a link to
-    * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
-    * @return {Element}
-    */
-    createSectionLink: function(sectionView, menuOptions){
-
-      var classes = "";
-      if( Array.isArray(menuOptions) && menuOptions.includes("Show") ){
-        classes = "hidden-section";
-      }
-
-      // Do not allow dragging/sorting of the Settings or Add Page sections
-      var sortable = true;
-      if(["addpage", "settings"].includes(sectionView.sectionType)){
-        sortable = false;
-      }
-
-      //Create a section link
-      var sectionLink = $(this.sectionLinkTemplate({
-        menuOptions: menuOptions || [],
-        uniqueLabel: sectionView.uniqueSectionLabel,
-        sectionLabel: PortalSection.prototype.isPrototypeOf(sectionView.model) ?
-                      sectionView.model.get("label") : sectionView.uniqueSectionLabel,
-        sectionURL: this.model.get("label") + "/" + sectionView.uniqueSectionLabel,
-        sectionType: sectionView.sectionType,
-        classes: classes,
-        handleClass: this.handleClass,
-        sortable: sortable
-      }));
-
-      //Attach the section model to the link
-      sectionLink.data({
-        model: sectionView.model,
-        view:  sectionView
-      });
-
-      if( sectionView.sectionType == "freeform" && menuOptions.includes("Delete") ){
-        var content = $(document.createElement("div"))
-                        .append(  $(document.createElement("div"))
-                                    .append( $(document.createElement("p"))
-                                               .text("Deleting this page will premanently remove it from this " +
-                                                     MetacatUI.appModel.get("portalTermSingular") + ".") ),
-                                  $(document.createElement("div"))
-                                    .addClass("inline-buttons")
-                                    .append( $(document.createElement("button"))
-                                               .addClass("btn cancelled-section-removal")
-                                               .text("No, keep page"),
-                                             $(document.createElement("button"))
-                                               .addClass("btn btn-danger confirmed-section-removal")
-                                               .text("Yes, delete page")));
-
-        // Create a popover with the confirmation buttons
-        sectionLink.find(".remove-section").addClass("popover-this").popover({
-          html            : true,
-          placement       : 'right',
-          title           : 'Delete this page?',
-          content         : content,
-          container       : sectionLink,
-          trigger         : "click"
+        //Create a section link
+        var sectionLink = $(
+          this.sectionLinkTemplate({
+            menuOptions: menuOptions || [],
+            uniqueLabel: sectionView.uniqueSectionLabel,
+            sectionLabel: PortalSection.prototype.isPrototypeOf(
+              sectionView.model,
+            )
+              ? sectionView.model.get("label")
+              : sectionView.uniqueSectionLabel,
+            sectionURL:
+              this.model.get("label") + "/" + sectionView.uniqueSectionLabel,
+            sectionType: sectionView.sectionType,
+            classes: classes,
+            handleClass: this.handleClass,
+            sortable: sortable,
+          }),
+        );
+
+        //Attach the section model to the link
+        sectionLink.data({
+          model: sectionView.model,
+          view: sectionView,
         });
-      }
-
-      return sectionLink[0];
-    },
 
-    /**
-    * Add a link to the given editor section
-    * @param {PortEditorSectionView} sectionView - The view to add a link to
-    * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
-    */
-    updateSectionLink: function(sectionView, menuOptions){
-
-      //Create a new link to the section
-      var sectionLink = this.createSectionLink(sectionView, menuOptions);
+        if (
+          sectionView.sectionType == "freeform" &&
+          menuOptions.includes("Delete")
+        ) {
+          var content = $(document.createElement("div")).append(
+            $(document.createElement("div")).append(
+              $(document.createElement("p")).text(
+                "Deleting this page will premanently remove it from this " +
+                  MetacatUI.appModel.get("portalTermSingular") +
+                  ".",
+              ),
+            ),
+            $(document.createElement("div"))
+              .addClass("inline-buttons")
+              .append(
+                $(document.createElement("button"))
+                  .addClass("btn cancelled-section-removal")
+                  .text("No, keep page"),
+                $(document.createElement("button"))
+                  .addClass("btn btn-danger confirmed-section-removal")
+                  .text("Yes, delete page"),
+              ),
+          );
+
+          // Create a popover with the confirmation buttons
+          sectionLink.find(".remove-section").addClass("popover-this").popover({
+            html: true,
+            placement: "right",
+            title: "Delete this page?",
+            content: content,
+            container: sectionLink,
+            trigger: "click",
+          });
+        }
 
-      //Replace the existing link
-      this.$(this.sectionLinksContainer).children().each(function(i, link){
-        if( $(link).data("view") == sectionView ){
-          $(link).replaceWith(sectionLink);
+        return sectionLink[0];
+      },
+
+      /**
+       * Add a link to the given editor section
+       * @param {PortEditorSectionView} sectionView - The view to add a link to
+       * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
+       */
+      updateSectionLink: function (sectionView, menuOptions) {
+        //Create a new link to the section
+        var sectionLink = this.createSectionLink(sectionView, menuOptions);
+
+        //Replace the existing link
+        this.$(this.sectionLinksContainer)
+          .children()
+          .each(function (i, link) {
+            if ($(link).data("view") == sectionView) {
+              $(link).replaceWith(sectionLink);
+            }
+          });
+      },
+
+      /**
+       * Remove the link to the given section view
+       * @param {View} sectionView - The view to remove the link to
+       */
+      removeSectionLink: function (sectionView) {
+        // Switch to the default section the user is deleting the active section
+        if (sectionView == this.activeSection) {
+          this.switchSection();
         }
-      });
-    },
 
-    /**
-    * Remove the link to the given section view
-    * @param {View} sectionView - The view to remove the link to
-    */
-    removeSectionLink: function(sectionView){
-
-      // Switch to the default section the user is deleting the active section
-      if (sectionView == this.activeSection){
-        this.switchSection();
-      };
-
-      try{
-
-        var view = this;
-        //Find the section link associated with this section view
-        this.$(this.sectionLinksContainer).children().each(function(i, link){
-          if( $(link).data("view") == sectionView ){
-
-            //Remove the menu link
-            $(link).addClass("removing").find(".section-menu-link").remove();
-
-            //Destroy any popovers
-            $(link).popover("destroy");
-            $(link).find(".popover-this").popover("destroy");
-
-            //Hide the section name link with an animation
-            $(link).animate({width: "0px", overflow: "hidden"}, {
-              duration: 300,
-              complete: function(){
-                this.remove();
-                // If there's a page order option set on the model, update it
-                var pageOrder = view.model.get("pageOrder");
-                if(pageOrder && pageOrder.length){
-                  view.updatePageOrder();
-                }
+        try {
+          var view = this;
+          //Find the section link associated with this section view
+          this.$(this.sectionLinksContainer)
+            .children()
+            .each(function (i, link) {
+              if ($(link).data("view") == sectionView) {
+                //Remove the menu link
+                $(link)
+                  .addClass("removing")
+                  .find(".section-menu-link")
+                  .remove();
+
+                //Destroy any popovers
+                $(link).popover("destroy");
+                $(link).find(".popover-this").popover("destroy");
+
+                //Hide the section name link with an animation
+                $(link).animate(
+                  { width: "0px", overflow: "hidden" },
+                  {
+                    duration: 300,
+                    complete: function () {
+                      this.remove();
+                      // If there's a page order option set on the model, update it
+                      var pageOrder = view.model.get("pageOrder");
+                      if (pageOrder && pageOrder.length) {
+                        view.updatePageOrder();
+                      }
+                    },
+                  },
+                );
               }
             });
-          }
-        });
-      }
-      catch(e){
-        console.error(e);
-      }
-    },
-
-    /**
-    * Adds a section and tab to this view and the PortalModel
-    * @param {string} sectionType - The type of section to add
-    */
-    addSection: function(sectionType){
-
-      try{
-
-        //Create a new section to the Portal model
-        this.model.addSection(sectionType);
-
-        if(typeof sectionType == "string"){
-
-          switch( sectionType.toLowerCase() ){
-            case "data":
-              this.switchSection(this.dataView);
-              break;
-            case "metrics":
-              this.renderMetricsSection();
-              this.switchSection(this.metricsView);
-              break;
-            case "freeform":
-              // Set up page ordering if it isn't already. This allows us to add a new
-              // freeform page at the end of the list of tabs, instead of before Data and
-              // Metrics (the default before page ordering was enabled).
-              var pageOrder = this.model.get("pageOrder");
-              if (!pageOrder || !pageOrder.length) {
-                this.updatePageOrder();
-              }
-              // Get the section model that was just added
-              var newestSection = this.model.get("sections")[this.model.get("sections").length-1];
-              //Render the content section view for it
-              this.renderContentSection(newestSection, true);
-              //Switch to that new view
-              this.switchSection( this.getSectionByModel(newestSection) );
-              break;
-            case "members":
-              // TODO
-              // this.switchSection(this.getSectionByLabel("Members"));
-              break;
-          }
-
-          // If the section we just added is now one of two sections, re-enable
-          // the hide/delete button on the other section link.
-          this.toggleRemoveSectionOption();
-
-          this.editorView.showControls();
-
-          // Add the item to the the pageOrder option on the model, if it exists
-          var pageOrder = this.model.get("pageOrder");
-          if(pageOrder && pageOrder.length){
-            this.updatePageOrder();
-          }
-
-        }
-        else{
-          return;
+        } catch (e) {
+          console.error(e);
         }
-      }
-      catch(e){
-        console.error(e);
-      }
-    },
-
-    /**
-    * Removes a section and its tab from this view and the PortalModel.
-    * At least one of the parameters is required, but not both
-    * @param {Event} [e] - (optional) The click event on the Remove button
-    * @param {Element|jQuery} [sectionLink] - The link element of the section to be removed.
-    */
-    removeSection: function(e, sectionLink){
+      },
 
-      try{
-        if( !sectionLink || !sectionLink.length ) {
+      /**
+       * Adds a section and tab to this view and the PortalModel
+       * @param {string} sectionType - The type of section to add
+       */
+      addSection: function (sectionType) {
+        try {
+          //Create a new section to the Portal model
+          this.model.addSection(sectionType);
+
+          if (typeof sectionType == "string") {
+            switch (sectionType.toLowerCase()) {
+              case "data":
+                this.switchSection(this.dataView);
+                break;
+              case "metrics":
+                this.renderMetricsSection();
+                this.switchSection(this.metricsView);
+                break;
+              case "freeform":
+                // Set up page ordering if it isn't already. This allows us to add a new
+                // freeform page at the end of the list of tabs, instead of before Data and
+                // Metrics (the default before page ordering was enabled).
+                var pageOrder = this.model.get("pageOrder");
+                if (!pageOrder || !pageOrder.length) {
+                  this.updatePageOrder();
+                }
+                // Get the section model that was just added
+                var newestSection =
+                  this.model.get("sections")[
+                    this.model.get("sections").length - 1
+                  ];
+                //Render the content section view for it
+                this.renderContentSection(newestSection, true);
+                //Switch to that new view
+                this.switchSection(this.getSectionByModel(newestSection));
+                break;
+              case "members":
+                // TODO
+                // this.switchSection(this.getSectionByLabel("Members"));
+                break;
+            }
 
-          var clickedEl = $(e.target);
+            // If the section we just added is now one of two sections, re-enable
+            // the hide/delete button on the other section link.
+            this.toggleRemoveSectionOption();
 
-          //Get the PortalSection model for this remove button
-          var sectionLink = clickedEl.parents(this.sectionLinkContainer).first();
+            this.editorView.showControls();
 
-          //Exit if no section link was found
-          if( !sectionLink || !sectionLink.length ){
+            // Add the item to the the pageOrder option on the model, if it exists
+            var pageOrder = this.model.get("pageOrder");
+            if (pageOrder && pageOrder.length) {
+              this.updatePageOrder();
+            }
+          } else {
             return;
           }
+        } catch (e) {
+          console.error(e);
         }
-
-        //Get the section model and view
-        var sectionModel        = sectionLink.data("model"),
-            sectionView         = sectionLink.data("view"),
-            sectionType         = sectionLink.data("section-type"),
-            uniqueSectionLabel  = sectionView.uniqueSectionLabel,
-            sectionLabelIndex   = this.sectionLabels.indexOf(uniqueSectionLabel);
-
-        if( PortalSection.prototype.isPrototypeOf(sectionModel) ){
-          // Remove this section from the Portal
-          this.model.removeSection(sectionModel);
-          // Remove the section label from the list of unique section labels
-          if(sectionLabelIndex > -1){
-            this.sectionLabels.splice(sectionLabelIndex, 1);
-          }
-        }
-        else{
-          //Remove this section type from the model
-          this.model.removeSection(sectionType);
-        }
-
+      },
+
+      /**
+       * Removes a section and its tab from this view and the PortalModel.
+       * At least one of the parameters is required, but not both
+       * @param {Event} [e] - (optional) The click event on the Remove button
+       * @param {Element|jQuery} [sectionLink] - The link element of the section to be removed.
+       */
+      removeSection: function (e, sectionLink) {
         try {
+          if (!sectionLink || !sectionLink.length) {
+            var clickedEl = $(e.target);
 
-          //If no section view was found, exit now
-          if( !sectionView  ){
-            return;
-          }
-
-          //If this is not the Data section, remove the view, since Data sections can only be hidden
-          if( sectionType.toLowerCase() != "data" ){
-            // remove the sectionView
-            this.removeSectionLink(sectionView);
-
-            // remove the section view from the subviews array
-            this.subviews.splice($.inArray(sectionView, this.subviews), 1);
-
-            //Remove the view from the page
-            sectionView.$el.remove();
+            //Get the PortalSection model for this remove button
+            var sectionLink = clickedEl
+              .parents(this.sectionLinkContainer)
+              .first();
 
-            //Reset the active section, if the one that was removed is currently active
-            if( this.activeSection == sectionView ){
-              this.activeSection = undefined;
-
-              //Switch to the default section
-              this.switchSection();
+            //Exit if no section link was found
+            if (!sectionLink || !sectionLink.length) {
+              return;
             }
           }
 
+          //Get the section model and view
+          var sectionModel = sectionLink.data("model"),
+            sectionView = sectionLink.data("view"),
+            sectionType = sectionLink.data("section-type"),
+            uniqueSectionLabel = sectionView.uniqueSectionLabel,
+            sectionLabelIndex = this.sectionLabels.indexOf(uniqueSectionLabel);
+
+          if (PortalSection.prototype.isPrototypeOf(sectionModel)) {
+            // Remove this section from the Portal
+            this.model.removeSection(sectionModel);
+            // Remove the section label from the list of unique section labels
+            if (sectionLabelIndex > -1) {
+              this.sectionLabels.splice(sectionLabelIndex, 1);
+            }
+          } else {
+            //Remove this section type from the model
+            this.model.removeSection(sectionType);
+          }
 
-        } catch (error) {
-          console.error(error);
-        }
-
-        // If the section just removed was the second-to-last section, disable
-        // the hide/delete button on the last section link.
-        this.toggleRemoveSectionOption();
+          try {
+            //If no section view was found, exit now
+            if (!sectionView) {
+              return;
+            }
 
-        this.editorView.showControls();
+            //If this is not the Data section, remove the view, since Data sections can only be hidden
+            if (sectionType.toLowerCase() != "data") {
+              // remove the sectionView
+              this.removeSectionLink(sectionView);
 
-      }
-      catch(e){
-        console.error(e);
-        MetacatUI.appView.showAlert("The section could not be deleted. (" + e.message + ")", "alert-error");
-      }
+              // remove the section view from the subviews array
+              this.subviews.splice($.inArray(sectionView, this.subviews), 1);
 
-    },
+              //Remove the view from the page
+              sectionView.$el.remove();
 
-    /**
-    * Shows a previously-hidden section
-    * @param {Event} [e] - (optional) The click event on the Show button
-    */
-    showSection: function(e){
+              //Reset the active section, if the one that was removed is currently active
+              if (this.activeSection == sectionView) {
+                this.activeSection = undefined;
 
-      try{
+                //Switch to the default section
+                this.switchSection();
+              }
+            }
+          } catch (error) {
+            console.error(error);
+          }
 
-        //Get the PortalSection model for this show button
-        var sectionLink = $(e.target).parents(this.sectionLinkContainer),
-            section = sectionLink.data("model");
+          // If the section just removed was the second-to-last section, disable
+          // the hide/delete button on the last section link.
+          this.toggleRemoveSectionOption();
 
-        //If this section is not a PortalSection model, get the section type
-        if( !PortalSection.prototype.isPrototypeOf(section) ){
-          section = sectionLink.data("section-type");
+          this.editorView.showControls();
+        } catch (e) {
+          console.error(e);
+          MetacatUI.appView.showAlert(
+            "The section could not be deleted. (" + e.message + ")",
+            "alert-error",
+          );
         }
+      },
 
-        //If no section was found, exit now
-        if( !section ){
-          return;
-        }
+      /**
+       * Shows a previously-hidden section
+       * @param {Event} [e] - (optional) The click event on the Show button
+       */
+      showSection: function (e) {
+        try {
+          //Get the PortalSection model for this show button
+          var sectionLink = $(e.target).parents(this.sectionLinkContainer),
+            section = sectionLink.data("model");
 
-        //Mark this section as shown
-        this.model.addSection(section);
+          //If this section is not a PortalSection model, get the section type
+          if (!PortalSection.prototype.isPrototypeOf(section)) {
+            section = sectionLink.data("section-type");
+          }
 
-        // If the section we're now showing is now one of two sections, re-enable
-        // the hide/delete button on the other section link.
-        this.toggleRemoveSectionOption();
+          //If no section was found, exit now
+          if (!section) {
+            return;
+          }
 
-      }
-      catch(e){
-        console.error(e);
-        MetacatUI.appView.showAlert("The section could not be shown. (" + e.message + ")", "alert-error");
-      }
+          //Mark this section as shown
+          this.model.addSection(section);
 
-    },
+          // If the section we're now showing is now one of two sections, re-enable
+          // the hide/delete button on the other section link.
+          this.toggleRemoveSectionOption();
+        } catch (e) {
+          console.error(e);
+          MetacatUI.appView.showAlert(
+            "The section could not be shown. (" + e.message + ")",
+            "alert-error",
+          );
+        }
+      },
 
-    /**
-    * Renames a section in the tab in this view and in the PortalSectionModel
-    * @param {Event} [e] - (optional) The click event on the Rename button
-    */
-    renameSection: function(e){
-      try {
-        //Get the PortalSection model for this rename button
-        var sectionLink = $(e.target).parents(this.sectionLinkContainer),
+      /**
+       * Renames a section in the tab in this view and in the PortalSectionModel
+       * @param {Event} [e] - (optional) The click event on the Rename button
+       */
+      renameSection: function (e) {
+        try {
+          //Get the PortalSection model for this rename button
+          var sectionLink = $(e.target).parents(this.sectionLinkContainer),
             targetLink = sectionLink.children(this.sectionLinks),
             section = sectionLink.data("model");
 
-        // double-click events
-        if (e.type === "dblclick") {
-          // Continue editing tab-name on double click only for markdown sections
-          if($(sectionLink).data("view").type != "PortEditorMdSection"){
-            return;
+          // double-click events
+          if (e.type === "dblclick") {
+            // Continue editing tab-name on double click only for markdown sections
+            if ($(sectionLink).data("view").type != "PortEditorMdSection") {
+              return;
+            }
           }
-        }
 
-        // make the text editable
-        targetLink.attr("contenteditable", true);
+          // make the text editable
+          targetLink.attr("contenteditable", true);
 
-        // add focus to the text
-        targetLink.focus();
+          // add focus to the text
+          targetLink.focus();
 
-        //Select the text of the link
-        if (window.getSelection && window.document.createRange) {
+          //Select the text of the link
+          if (window.getSelection && window.document.createRange) {
             var selection = window.getSelection();
             var range = window.document.createRange();
-            range.selectNodeContents( targetLink[0] );
+            range.selectNodeContents(targetLink[0]);
             selection.removeAllRanges();
             selection.addRange(range);
-        } else if (window.document.body.createTextRange) {
+          } else if (window.document.body.createTextRange) {
             range = window.document.body.createTextRange();
-            range.moveToElementText( targetLink[0] );
+            range.moveToElementText(targetLink[0]);
             range.select();
+          }
+        } catch (error) {
+          console.error(error);
         }
+      },
+
+      /**
+       * Stops user from entering more than 50 characters, and shows a message
+       * if user tries to exceed the limit. Also stops a user from entering
+       * RETURN or TAB characters, and instead re-directs to updateName().
+       * In the case of the TAB key, the focus moves to the title field.
+       * @param {Event} e - The keyup or keydown event when the user types in the portal-section-link field
+       */
+      limitLabelInput: function (e) {
+        try {
+          // Character limit for the labels
+          var limit = 50;
+          var currentLabel = $(e.target).text();
+
+          // If the RETURN key is pressed
+          if (e.which == 13) {
+            // Don't allow character to be entered
+            e.preventDefault();
+            e.stopPropagation();
+            // Update name and exit function
+            this.updateName(e);
+            return;
+          }
 
-      } catch (error) {
-        console.error(error);
-      }
-
-    },
-
-    /**
-     * Stops user from entering more than 50 characters, and shows a message
-     * if user tries to exceed the limit. Also stops a user from entering
-     * RETURN or TAB characters, and instead re-directs to updateName().
-     * In the case of the TAB key, the focus moves to the title field.
-     * @param {Event} e - The keyup or keydown event when the user types in the portal-section-link field
-    */
-    limitLabelInput: function(e){
-
-      try{
-
-        // Character limit for the labels
-        var limit = 50;
-        var currentLabel = $(e.target).text();
-
-        // If the RETURN key is pressed
-        if(e.which == 13){
-          // Don't allow character to be entered
-          e.preventDefault();
-          e.stopPropagation();
-          // Update name and exit function
-          this.updateName(e);
-          return
-        }
-
-        // If the TAB key is pressed
-        if(e.which == 9){
-          // Don't allow character to be entered
-          e.preventDefault();
-          e.stopPropagation();
-          // Update name, change focus to title, and exit function
-          this.updateName(e);
-          $("textarea.title").focus();
-          return
-        }
+          // If the TAB key is pressed
+          if (e.which == 9) {
+            // Don't allow character to be entered
+            e.preventDefault();
+            e.stopPropagation();
+            // Update name, change focus to title, and exit function
+            this.updateName(e);
+            $("textarea.title").focus();
+            return;
+          }
 
-        // Keys that a user can use as normal, even if character limit is met
-        var allowedKeys = [
-          8,  // DELETE
-          35, // END
-          36, // HOME
-          37, // LEFT
-          38, // UP
-          39, // RIGHT
-          40, // DOWN
-          46,  // DEL
-          17   // CTRL
-        ];
-
-        // Stop addition of more characters and show message
-        if(
-          // If at or greater than limit and
-          currentLabel.length >= limit &&
-          // key isn't a special key and
-          !allowedKeys.includes(e.which) &&
-          // cmd key isn't held down and
-          !e.metaKey &&
-          // user doesn't have some of the text selected
-          !window.getSelection().toString().length
-        ){
-          // Don't allow character to be entered
-          e.preventDefault();
-          e.stopPropagation();
-          // Add a tooltip if one doesn't exist yet
-          if(!$(e.delegateTarget).find(".tooltip").length){
-            $(e.target).tooltip({
-              placement: "top",
-              trigger: "manual",
-              title: "Limit of " + limit + " characters or fewer"
-            });
+          // Keys that a user can use as normal, even if character limit is met
+          var allowedKeys = [
+            8, // DELETE
+            35, // END
+            36, // HOME
+            37, // LEFT
+            38, // UP
+            39, // RIGHT
+            40, // DOWN
+            46, // DEL
+            17, // CTRL
+          ];
+
+          // Stop addition of more characters and show message
+          if (
+            // If at or greater than limit and
+            currentLabel.length >= limit &&
+            // key isn't a special key and
+            !allowedKeys.includes(e.which) &&
+            // cmd key isn't held down and
+            !e.metaKey &&
+            // user doesn't have some of the text selected
+            !window.getSelection().toString().length
+          ) {
+            // Don't allow character to be entered
+            e.preventDefault();
+            e.stopPropagation();
+            // Add a tooltip if one doesn't exist yet
+            if (!$(e.delegateTarget).find(".tooltip").length) {
+              $(e.target).tooltip({
+                placement: "top",
+                trigger: "manual",
+                title: "Limit of " + limit + " characters or fewer",
+              });
+            }
+            // Show the tooltip
+            $(e.target).tooltip("show");
+            // If under the character limit, proceed as normal.
+          } else {
+            // Make sure there's no tooltip showing.
+            $(e.delegateTarget).find(".tooltip").remove();
           }
-          // Show the tooltip
-          $(e.target).tooltip('show');
-        // If under the character limit, proceed as normal.
-        } else {
-          // Make sure there's no tooltip showing.
-          $(e.delegateTarget).find(".tooltip").remove();
+        } catch (error) {
+          "Error limiting user input in label field, error message: " + error;
         }
-      }
-      catch(error){
-        "Error limiting user input in label field, error message: " + error
-      }
+      },
 
-    },
+      /**
+       * Update the section label
+       *
+       * @param e The event triggering this method
+       */
+      updateName: function (e) {
+        // Remove tooltip incase one was set by limitLabelInput function
+        $(e.delegateTarget).find(".tooltip").remove();
 
-    /**
-     * Update the section label
-     *
-     * @param e The event triggering this method
-     */
-    updateName: function(e) {
-
-      // Remove tooltip incase one was set by limitLabelInput function
-      $(e.delegateTarget).find(".tooltip").remove();
-
-      try {
-        //Get the PortalSection model for this rename button
-        var sectionLink   =   $(e.target).parents(this.sectionLinkContainer),
-            targetLink    =   sectionLink.find(this.sectionLinks),
-            sectionModel  =   sectionLink.data("model"),
-            sectionView   =   sectionLink.data("view"),
+        try {
+          //Get the PortalSection model for this rename button
+          var sectionLink = $(e.target).parents(this.sectionLinkContainer),
+            targetLink = sectionLink.find(this.sectionLinks),
+            sectionModel = sectionLink.data("model"),
+            sectionView = sectionLink.data("view"),
             // Clean up the typed in name so it's valid for XML
-            oldLabel      =   sectionModel.get("label"),
-            newLabel      =   this.model.cleanXMLText(targetLink.text().trim()),
-            pageOrder     =   this.model.get("pageOrder");
-
-        // Remove the content editable attribute
-        targetLink.attr("contenteditable", false);
-
-        // If this section is an object of PortalSection model, update the label.
-        if( sectionModel && PortalSection.prototype.isPrototypeOf(sectionModel) ){
-          // update the label on the model
-          sectionModel.set("label", newLabel);
-        }
-        else {
-          // TODO: handle the case for non-markdown sections
-        }
+            oldLabel = sectionModel.get("label"),
+            newLabel = this.model.cleanXMLText(targetLink.text().trim()),
+            pageOrder = this.model.get("pageOrder");
+
+          // Remove the content editable attribute
+          targetLink.attr("contenteditable", false);
+
+          // If this section is an object of PortalSection model, update the label.
+          if (
+            sectionModel &&
+            PortalSection.prototype.isPrototypeOf(sectionModel)
+          ) {
+            // update the label on the model
+            sectionModel.set("label", newLabel);
+          } else {
+            // TODO: handle the case for non-markdown sections
+          }
 
-        // Update the name set on the link element, since it's used for setting the pageOrder option
-        sectionLink.data("section-name", newLabel);
+          // Update the name set on the link element, since it's used for setting the pageOrder option
+          sectionLink.data("section-name", newLabel);
 
-        // Update the name in the pageOrder option, if it exists
-        if(pageOrder && pageOrder.length){
-          this.updatePageOrder();
-        }
+          // Update the name in the pageOrder option, if it exists
+          if (pageOrder && pageOrder.length) {
+            this.updatePageOrder();
+          }
 
-        // Update the array of unique section labels
+          // Update the array of unique section labels
 
-        // Get the position of the unique label in the list
-        var indexIDs = this.sectionLabels.indexOf(sectionView.uniqueSectionLabel),
+          // Get the position of the unique label in the list
+          var indexIDs = this.sectionLabels.indexOf(
+              sectionView.uniqueSectionLabel,
+            ),
             newUniqueLabel = "";
 
-        // Remove the old unique label so when we create a new unique label,
-        // we don't consider the label that we're replacing in determining uniqueness
-        if(indexIDs > -1){
-          this.sectionLabels.splice(indexIDs, 1);
-          newUniqueLabel = this.getUniqueSectionLabel(sectionModel);
-          this.sectionLabels.splice(indexIDs, 0, newUniqueLabel)
-        } else {
-          newUniqueLabel = this.getUniqueSectionLabel(sectionModel);
-          this.sectionLabels.push(newUniqueLabel);
-        }
-
-        // Update the label set on the view
-        sectionView.uniqueSectionLabel = newUniqueLabel;
-
-      } catch (error) {
-        console.error(error);
-      }
-    },
-
-    /**
-     * Using the "section-name" data attribute set on each section link,
-     * and the order that the links are displayed in the DOM, update the
-     * pageOrder option in the portal model.
-     */
-    updatePageOrder: function(){
-      try {
-        var view = this;
-        // Get the links as they are ordered in the UI
-        var links = view.el.querySelectorAll(view.sectionLinksContainer + view.sortableLinksSelector),
-        pageOrder = [];
-        _.each(links, function(link){
-          // Use the value set on section-name to re-order pages
-          var label = $(link).data("section-name");
-          if(label){ pageOrder.push(label) }
-        });
-        view.model.set("pageOrder", pageOrder);
-        view.editorView.showControls();
-      } catch (error) {
-        console.log("Error updating the portal page order, message: " + error)
-      }
-    },
+          // Remove the old unique label so when we create a new unique label,
+          // we don't consider the label that we're replacing in determining uniqueness
+          if (indexIDs > -1) {
+            this.sectionLabels.splice(indexIDs, 1);
+            newUniqueLabel = this.getUniqueSectionLabel(sectionModel);
+            this.sectionLabels.splice(indexIDs, 0, newUniqueLabel);
+          } else {
+            newUniqueLabel = this.getUniqueSectionLabel(sectionModel);
+            this.sectionLabels.push(newUniqueLabel);
+          }
 
-    /**
-    * Add a new unique label to the list of unique section labels
-    * (used the ensure that tab links and anchors are unique,
-    * otherwise, tab switching does not work)
-    */
-    updateSectionLabelsList: function(newLabel){
-      try {
-        if( !this.sectionLabels ){
-          this.sectionLabels = [];
+          // Update the label set on the view
+          sectionView.uniqueSectionLabel = newUniqueLabel;
+        } catch (error) {
+          console.error(error);
         }
-        this.sectionLabels.push(newLabel);
-      } catch (error) {
-        console.log("Error updating the list of unique section labels. Error message: " + error)
-      }
-    },
-
-    /**
-    * Shows a validation error message and adds error styling to the given elements
-    * @param {jQuery} elements - The elements to add error styling and messaging to
-    */
-    showValidation: function(elements){
-      try{
-        //Get the parent elements that have ids set
-        var sectionEls = elements.parents(this.sectionEls);
-
-        //See if there is a matching section link
-        for(var i=0; i<sectionEls.length; i++){
-
-          //Get the section view attached to the section element
-          var sectionView = sectionEls.data("view");
-
-          //If a section view was found,
-          if( sectionView ){
-            //Find the section link that links to this section view
-            var matchingLink = _.find($(this.sectionLinkContainer), function(link){
-              return $(link).data("view") == sectionView;
-            });
-
-            //Add the error class and display the error icon
-            if( matchingLink ){
-              $(matchingLink).addClass("error").find(".icon.error").show();
-              //Exit the loop
-              i=sectionEls.length+1;
+      },
+
+      /**
+       * Using the "section-name" data attribute set on each section link,
+       * and the order that the links are displayed in the DOM, update the
+       * pageOrder option in the portal model.
+       */
+      updatePageOrder: function () {
+        try {
+          var view = this;
+          // Get the links as they are ordered in the UI
+          var links = view.el.querySelectorAll(
+              view.sectionLinksContainer + view.sortableLinksSelector,
+            ),
+            pageOrder = [];
+          _.each(links, function (link) {
+            // Use the value set on section-name to re-order pages
+            var label = $(link).data("section-name");
+            if (label) {
+              pageOrder.push(label);
             }
+          });
+          view.model.set("pageOrder", pageOrder);
+          view.editorView.showControls();
+        } catch (error) {
+          console.log(
+            "Error updating the portal page order, message: " + error,
+          );
+        }
+      },
+
+      /**
+       * Add a new unique label to the list of unique section labels
+       * (used the ensure that tab links and anchors are unique,
+       * otherwise, tab switching does not work)
+       */
+      updateSectionLabelsList: function (newLabel) {
+        try {
+          if (!this.sectionLabels) {
+            this.sectionLabels = [];
           }
+          this.sectionLabels.push(newLabel);
+        } catch (error) {
+          console.log(
+            "Error updating the list of unique section labels. Error message: " +
+              error,
+          );
         }
-      }
-      catch(e){
-        console.error("Error showing validation message: ", e);
-      }
-    },
-
-    /**
-    * Closes all the popovers in this view
-    */
-    closePopovers: function(){
-      this.$(".popover-this").popover("hide");
-    },
+      },
 
-    /**
-     * This function is called when the app navigates away from this view.
-     * Any clean-up or housekeeping happens at this time.
-     */
-    onClose: function() {
+      /**
+       * Shows a validation error message and adds error styling to the given elements
+       * @param {jQuery} elements - The elements to add error styling and messaging to
+       */
+      showValidation: function (elements) {
+        try {
+          //Get the parent elements that have ids set
+          var sectionEls = elements.parents(this.sectionEls);
+
+          //See if there is a matching section link
+          for (var i = 0; i < sectionEls.length; i++) {
+            //Get the section view attached to the section element
+            var sectionView = sectionEls.data("view");
+
+            //If a section view was found,
+            if (sectionView) {
+              //Find the section link that links to this section view
+              var matchingLink = _.find(
+                $(this.sectionLinkContainer),
+                function (link) {
+                  return $(link).data("view") == sectionView;
+                },
+              );
+
+              //Add the error class and display the error icon
+              if (matchingLink) {
+                $(matchingLink).addClass("error").find(".icon.error").show();
+                //Exit the loop
+                i = sectionEls.length + 1;
+              }
+            }
+          }
+        } catch (e) {
+          console.error("Error showing validation message: ", e);
+        }
+      },
+
+      /**
+       * Closes all the popovers in this view
+       */
+      closePopovers: function () {
+        this.$(".popover-this").popover("hide");
+      },
+
+      /**
+       * This function is called when the app navigates away from this view.
+       * Any clean-up or housekeeping happens at this time.
+       */
+      onClose: function () {
         //Remove each subview from the DOM and remove listeners
         _.invoke(this.subviews, "remove");
 
@@ -1590,12 +1622,11 @@ 

Source: src/js/views/portals/editor/PortEditorSectionsVie //Remove the reference to the EditorView this.editorView = null; - } - - }); + }, + }, + ); return PortEditorSectionsView; - });

diff --git a/docs/docs/src_js_views_portals_editor_PortEditorSettingsView.js.html b/docs/docs/src_js_views_portals_editor_PortEditorSettingsView.js.html index 8e57a26cc..27072a0ec 100644 --- a/docs/docs/src_js_views_portals_editor_PortEditorSettingsView.js.html +++ b/docs/docs/src_js_views_portals_editor_PortEditorSettingsView.js.html @@ -44,351 +44,372 @@

Source: src/js/views/portals/editor/PortEditorSettingsVie
-
define(['underscore',
-        'jquery',
-        'backbone',
-        'models/portals/PortalSectionModel',
-        "views/portals/editor/PortEditorSectionView",
-        "views/portals/editor/PortEditorLogosView",
-        "text!templates/portals/editor/portEditorSettings.html"],
-function(_, $, Backbone, PortalSection, PortEditorSectionView, PortEditorLogosView,
-  Template){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/portals/PortalSectionModel",
+  "views/portals/editor/PortEditorSectionView",
+  "views/portals/editor/PortEditorLogosView",
+  "text!templates/portals/editor/portEditorSettings.html",
+], function (
+  _,
+  $,
+  Backbone,
+  PortalSection,
+  PortEditorSectionView,
+  PortEditorLogosView,
+  Template,
+) {
   /**
-  * @class PortEditorSettingsView
-  * @classcategory Views/Portals/Editor
-  * @extends PortEditorSectionView
-  */
+   * @class PortEditorSettingsView
+   * @classcategory Views/Portals/Editor
+   * @extends PortEditorSectionView
+   */
   var PortEditorSettingsView = PortEditorSectionView.extend(
-    /** @lends PortEditorSettingsView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "PortEditorSettings",
-
-    /**
-    * The display name for this Section
-    * @type {string}
-    */
-    uniqueSectionLabel: "Settings",
-
-    /**
-    * The type of section view this is
-    * @type {string}
-    */
-    sectionType: "settings",
-
-    /**
-    * The HTML classes to use for this view's element
-    * @type {string}
-    */
-    className: PortEditorSectionView.prototype.className + " port-editor-settings",
-
-    /**
-    * The id attribute of the view element
-    * @param {string}
-    */
-    id: "Settings",
-
-    /**
-    * The PortalModel that is being edited
-    * @type {Portal}
-    */
-    model: undefined,
-
-    /**
-    * A reference to the PortalEditorView
-    * @type {PortalEditorView}
-    */
-    editorView: undefined,
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    */
-    template: _.template(Template),
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * @type {Object}
-    */
-    events: {
-      "focusout .label-container input" : "showLabelValidation",
-      "click .change-label"             : "changeLabel",
-      "click .cancel-change-label"      : "cancelChangeLabel",
-      "click .ok-change-label"          : "okChangeLabel",
-      "keyup .label-container input"    : "removeLabelValidation"
-    },
-
-    /**
-    * Creates a new PortEditorSettingsView
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options){
-
-      try {
-        //Call the superclass initialize() function
-        PortEditorSectionView.prototype.initialize(options);
-      } catch (e) {
-        console.log("Error initializing the portal editor settings view. Error message: " + e);
-      }
-    },
-
-    /**
-    * Renders this view
-    */
-    render: function(){
-
-      try {
-        //Insert the template into the view
-        var portalTermSingular = MetacatUI.appModel.get("portalTermSingular");
-        this.$el.html(this.template({
-          label: this.model.get("label"),
-          description: this.model.get("description"),
-          portalTermPlural: MetacatUI.appModel.get("portalTermPlural"),
-          portalTermSingular: MetacatUI.appModel.get("portalTermSingular")
-        }));
-
-        //Render the PortEditorLogosView
-        var logosView = new PortEditorLogosView({
-          model: this.model,
-          editorView: this.editorView
-        });
-        logosView.render();
-        this.$(".logos-container").html(logosView.el).data("view", logosView);
-
-        //Save a reference to this view
-        this.$el.data("view", this);
-
-        // If it's a new model, it won't have a label (URL) yet. Display the label
-        // input field so the user doesn't miss it.
-        if (this.model.get("isNew")) {
-          this.changeLabel();
+    /** @lends PortEditorSettingsView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "PortEditorSettings",
+
+      /**
+       * The display name for this Section
+       * @type {string}
+       */
+      uniqueSectionLabel: "Settings",
+
+      /**
+       * The type of section view this is
+       * @type {string}
+       */
+      sectionType: "settings",
+
+      /**
+       * The HTML classes to use for this view's element
+       * @type {string}
+       */
+      className:
+        PortEditorSectionView.prototype.className + " port-editor-settings",
+
+      /**
+       * The id attribute of the view element
+       * @param {string}
+       */
+      id: "Settings",
+
+      /**
+       * The PortalModel that is being edited
+       * @type {Portal}
+       */
+      model: undefined,
+
+      /**
+       * A reference to the PortalEditorView
+       * @type {PortalEditorView}
+       */
+      editorView: undefined,
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      template: _.template(Template),
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * @type {Object}
+       */
+      events: {
+        "focusout .label-container input": "showLabelValidation",
+        "click .change-label": "changeLabel",
+        "click .cancel-change-label": "cancelChangeLabel",
+        "click .ok-change-label": "okChangeLabel",
+        "keyup .label-container input": "removeLabelValidation",
+      },
+
+      /**
+       * Creates a new PortEditorSettingsView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          //Call the superclass initialize() function
+          PortEditorSectionView.prototype.initialize(options);
+        } catch (e) {
+          console.log(
+            "Error initializing the portal editor settings view. Error message: " +
+              e,
+          );
         }
-
-      } catch (e) {
-        console.log("Error rendering the portal editor settings view. Error message: "+ e);
-      }
-
-    },
-
-    /**
-     * Removes help text and css formatting that indicates error or success after label/URL validation.
-     *
-     *  @param {Event} e - The keyup or focusout event
-     */
-    removeLabelValidation: function(e){
-
-      try {
-        var container = this.$(".label-container"),
-            messageEl = $(container).find('.notification');
-
-        // Remove input formating if there was any
-        messageEl.html("");
-        container.removeClass("error");
-        container.removeClass("success");
-        container.find(".error").removeClass("error");
-        container.find(".success").removeClass("success");
-
-        if(!this.model.get("isNew")){
-          // Ensure that the OK button is showing, may be hidden if a previous
-          // attempt to change the label resulted in an error
-          this.$(".ok-change-label").show();
+      },
+
+      /**
+       * Renders this view
+       */
+      render: function () {
+        try {
+          //Insert the template into the view
+          var portalTermSingular = MetacatUI.appModel.get("portalTermSingular");
+          this.$el.html(
+            this.template({
+              label: this.model.get("label"),
+              description: this.model.get("description"),
+              portalTermPlural: MetacatUI.appModel.get("portalTermPlural"),
+              portalTermSingular: MetacatUI.appModel.get("portalTermSingular"),
+            }),
+          );
+
+          //Render the PortEditorLogosView
+          var logosView = new PortEditorLogosView({
+            model: this.model,
+            editorView: this.editorView,
+          });
+          logosView.render();
+          this.$(".logos-container").html(logosView.el).data("view", logosView);
+
+          //Save a reference to this view
+          this.$el.data("view", this);
+
+          // If it's a new model, it won't have a label (URL) yet. Display the label
+          // input field so the user doesn't miss it.
+          if (this.model.get("isNew")) {
+            this.changeLabel();
+          }
+        } catch (e) {
+          console.log(
+            "Error rendering the portal editor settings view. Error message: " +
+              e,
+          );
         }
-      } catch (error) {
-        console.log("Error removing label validation, error message: " + error);
-      }
-
-    },
-
-
-    /**
-     * showLabelValidationError - add css formatting and hide OK button when there are errors in label validation.
-     *
-     * @param {Event} e - The keyup or focusout event
-     */
-    showLabelValidationError: function(e){
-
-      try {
-        var container = this.$(".label-container"),
-            input = container.find('input'),
-            messageEl = container.find('.notification'),
-            okButton = container.find('.ok-change-label');
-
-        messageEl.addClass("error");
-        input.addClass("error");
-        okButton.hide();
-      } catch (error) {
-        console.log("Error showing label validation error, message: " + error);
-      }
-
-    },
-
-    /**
-     * Initiates validatation of the newly inputed label (a URL component).
-     * Listens for a response from the model, then displays help text based on
-     * whether the new label was valid or not.
-     *
-     *  @param {Event} e - The focusout event
-     */
-    showLabelValidation: function(e){
-
-      try{
-        var container = this.$(".label-container"),
-            input = container.find('input'),
-            messageEl = container.find('.notification'),
-            value = this.model.cleanXMLText(input.val()),
-            model = this.model;
-
-        //If the label is unchanged, remove the validation messaging and exit
-        if( value == this.model.get("originalLabel") ){
-          this.removeLabelValidation(e);
-          return;
+      },
+
+      /**
+       * Removes help text and css formatting that indicates error or success after label/URL validation.
+       *
+       *  @param {Event} e - The keyup or focusout event
+       */
+      removeLabelValidation: function (e) {
+        try {
+          var container = this.$(".label-container"),
+            messageEl = $(container).find(".notification");
+
+          // Remove input formating if there was any
+          messageEl.html("");
+          container.removeClass("error");
+          container.removeClass("success");
+          container.find(".error").removeClass("error");
+          container.find(".success").removeClass("success");
+
+          if (!this.model.get("isNew")) {
+            // Ensure that the OK button is showing, may be hidden if a previous
+            // attempt to change the label resulted in an error
+            this.$(".ok-change-label").show();
+          }
+        } catch (error) {
+          console.log(
+            "Error removing label validation, error message: " + error,
+          );
         }
-
-        //If there is an error checking the validity, display a message
-        this.listenToOnce(this.model, "errorValidatingLabel", function(){
-          this.removeLabelValidation(e);
-          var email = MetacatUI.appModel.get('emailContact');
-          messageEl.html("There was a problem checking the availablity of this URL. " +
-                         "Please try again or <a href='mailto:" + email + "'> contact us at " +
-                         email + "</a>.");
-          this.showLabelValidationError(e);
-        });
-
-        // Validate the label string
-        var error = this.model.validateLabel(value);
-
-        // If there is an error, display it and exit
-        if( error ){
-          this.removeLabelValidation(e);
-          this.showLabelValidationError(e);
-          messageEl.html(error);
-          return;
+      },
+
+      /**
+       * showLabelValidationError - add css formatting and hide OK button when there are errors in label validation.
+       *
+       * @param {Event} e - The keyup or focusout event
+       */
+      showLabelValidationError: function (e) {
+        try {
+          var container = this.$(".label-container"),
+            input = container.find("input"),
+            messageEl = container.find(".notification"),
+            okButton = container.find(".ok-change-label");
+
+          messageEl.addClass("error");
+          input.addClass("error");
+          okButton.hide();
+        } catch (error) {
+          console.log(
+            "Error showing label validation error, message: " + error,
+          );
         }
+      },
+
+      /**
+       * Initiates validatation of the newly inputed label (a URL component).
+       * Listens for a response from the model, then displays help text based on
+       * whether the new label was valid or not.
+       *
+       *  @param {Event} e - The focusout event
+       */
+      showLabelValidation: function (e) {
+        try {
+          var container = this.$(".label-container"),
+            input = container.find("input"),
+            messageEl = container.find(".notification"),
+            value = this.model.cleanXMLText(input.val()),
+            model = this.model;
 
-        // If there are no validation errors, check label availability
-
-        // Success
-        this.listenToOnce(this.model, "labelAvailable", function(){
-          this.removeLabelValidation(e);
-          messageEl.html("<i class='icon-check'></i> This URL is available")
-                   .addClass("success");
-          // Make sure the OK button is enabled
-          if(!this.model.isNew()){
-            this.$(".ok-change-label").show();
+          //If the label is unchanged, remove the validation messaging and exit
+          if (value == this.model.get("originalLabel")) {
+            this.removeLabelValidation(e);
+            return;
           }
-        });
-
-        // Error: label taken
-        this.listenToOnce(this.model, "labelTaken", function(){
-          this.removeLabelValidation(e);
-          this.showLabelValidationError(e);
-          messageEl.html("This URL is already taken, please try something else");
 
-          //Manually add the validation error message since this check is done outside of the validate() function
-          if( typeof this.model.validationError == "object" && this.model.validationError !== null ){
-            this.model.validationError.label = "This URL is already taken, please try something else";
+          //If there is an error checking the validity, display a message
+          this.listenToOnce(this.model, "errorValidatingLabel", function () {
+            this.removeLabelValidation(e);
+            var email = MetacatUI.appModel.get("emailContact");
+            messageEl.html(
+              "There was a problem checking the availablity of this URL. " +
+                "Please try again or <a href='mailto:" +
+                email +
+                "'> contact us at " +
+                email +
+                "</a>.",
+            );
+            this.showLabelValidationError(e);
+          });
+
+          // Validate the label string
+          var error = this.model.validateLabel(value);
+
+          // If there is an error, display it and exit
+          if (error) {
+            this.removeLabelValidation(e);
+            this.showLabelValidationError(e);
+            messageEl.html(error);
+            return;
           }
-          else{
-            this.model.validationError = {
-              label: "This URL is already taken, please try something else"
+
+          // If there are no validation errors, check label availability
+
+          // Success
+          this.listenToOnce(this.model, "labelAvailable", function () {
+            this.removeLabelValidation(e);
+            messageEl
+              .html("<i class='icon-check'></i> This URL is available")
+              .addClass("success");
+            // Make sure the OK button is enabled
+            if (!this.model.isNew()) {
+              this.$(".ok-change-label").show();
             }
+          });
+
+          // Error: label taken
+          this.listenToOnce(this.model, "labelTaken", function () {
+            this.removeLabelValidation(e);
+            this.showLabelValidationError(e);
+            messageEl.html(
+              "This URL is already taken, please try something else",
+            );
+
+            //Manually add the validation error message since this check is done outside of the validate() function
+            if (
+              typeof this.model.validationError == "object" &&
+              this.model.validationError !== null
+            ) {
+              this.model.validationError.label =
+                "This URL is already taken, please try something else";
+            } else {
+              this.model.validationError = {
+                label: "This URL is already taken, please try something else",
+              };
+            }
+          });
+
+          // Show 'checking URL' message
+          messageEl.html(
+            "<i class='icon-spinner icon-spin icon-large loading icon'></i> " +
+              "Checking if URL is available",
+          );
+
+          // Check label availability
+          this.model.checkLabelAvailability(value);
+        } catch (error) {
+          console.log("Error validating the label, error message: " + error);
+        }
+      },
+
+      /**
+       * Makes the portal label editable whenever the `change url` button is clicked
+       */
+      changeLabel: function () {
+        try {
+          //Get the label at this point in time
+          this.model.set("latestLabel", this.model.get("label"));
+
+          //Hide the label display and Change button
+          this.$(".display-label, .change-label").hide();
+          //Show the input and controls
+          this.$(".label-container").show();
+
+          // If the model is new, hide the Cancel and Ok buttons.
+          if (this.model.get("isNew")) {
+            this.$(".ok-change-label").hide();
+            this.$(".cancel-change-label").hide();
           }
-        });
-
-        // Show 'checking URL' message
-        messageEl.html(
-          "<i class='icon-spinner icon-spin icon-large loading icon'></i> "+
-          "Checking if URL is available"
-        );
-
-        // Check label availability
-        this.model.checkLabelAvailability(value);
-      }
-      catch(error){
-        console.log("Error validating the label, error message: " + error);
-      }
-    },
-
-    /**
-     * Makes the portal label editable whenever the `change url` button is clicked
-     */
-    changeLabel: function(){
-      try {
-        //Get the label at this point in time
-        this.model.set("latestLabel", this.model.get("label"));
-
-        //Hide the label display and Change button
-        this.$(".display-label, .change-label").hide();
-        //Show the input and controls
-        this.$(".label-container").show();
-
-        // If the model is new, hide the Cancel and Ok buttons.
-        if (this.model.get("isNew")) {
-          this.$(".ok-change-label").hide();
-          this.$(".cancel-change-label").hide();
+        } catch (e) {
+          console.log("Error changing label, error message: " + e);
         }
-      } catch (e) {
-        console.log("Error changing label, error message: " + e);
-      }
-    },
-
-    /**
-     * Cancels changing the portal label
-     */
-    cancelChangeLabel: function(){
-      try {
-        //Reset the label
-        this.model.set("label", this.model.get("latestLabel"));
-        this.$(".label-container input").val(this.model.get("label"));
-
-        //Validate the label
-        this.showLabelValidation();
-
-        //Show the label display and Change button
-        this.$(".display-label, .change-label").show();
-        // Ensure that the OK button is showing, may be hidden if a previous
-        // attempt to change the label resulted in an error
-        this.$(".ok-change-label").show();
-        //Hide the input and controls
-        this.$(".label-container").hide();
-      } catch (e) {
-        console.log("Error cancelling the changes to label, error message: " + e);
-      }
-    },
-
-    /**
-     * Shows the portal label as saved
-     */
-    okChangeLabel: function(){
-      try {
-        //Show the label display and Change button
-        this.$(".display-label, .change-label").show();
-        //Hide the input and controls
-        this.$(".label-container").hide();
-
-        //If there is a validation error with the label, revert it back
-        if( this.model.validationError && this.model.validationError.label ){
+      },
+
+      /**
+       * Cancels changing the portal label
+       */
+      cancelChangeLabel: function () {
+        try {
+          //Reset the label
           this.model.set("label", this.model.get("latestLabel"));
           this.$(".label-container input").val(this.model.get("label"));
-        }
-        else{
-          this.$(".display-label-value").text(this.model.get("label"));
-        }
 
-        //Validate the label
-        this.showLabelValidation();
-      } catch (e) {
-        console.log("Error showing the portal label as saved, error message: " + e);
-      }
-    }
+          //Validate the label
+          this.showLabelValidation();
 
-  });
+          //Show the label display and Change button
+          this.$(".display-label, .change-label").show();
+          // Ensure that the OK button is showing, may be hidden if a previous
+          // attempt to change the label resulted in an error
+          this.$(".ok-change-label").show();
+          //Hide the input and controls
+          this.$(".label-container").hide();
+        } catch (e) {
+          console.log(
+            "Error cancelling the changes to label, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * Shows the portal label as saved
+       */
+      okChangeLabel: function () {
+        try {
+          //Show the label display and Change button
+          this.$(".display-label, .change-label").show();
+          //Hide the input and controls
+          this.$(".label-container").hide();
+
+          //If there is a validation error with the label, revert it back
+          if (this.model.validationError && this.model.validationError.label) {
+            this.model.set("label", this.model.get("latestLabel"));
+            this.$(".label-container input").val(this.model.get("label"));
+          } else {
+            this.$(".display-label-value").text(this.model.get("label"));
+          }
 
-  return PortEditorSettingsView;
+          //Validate the label
+          this.showLabelValidation();
+        } catch (e) {
+          console.log(
+            "Error showing the portal label as saved, error message: " + e,
+          );
+        }
+      },
+    },
+  );
 
+  return PortEditorSettingsView;
 });
 
diff --git a/docs/docs/src_js_views_portals_editor_PortalEditorView.js.html b/docs/docs/src_js_views_portals_editor_PortalEditorView.js.html index ca891559c..802149c52 100644 --- a/docs/docs/src_js_views_portals_editor_PortalEditorView.js.html +++ b/docs/docs/src_js_views_portals_editor_PortalEditorView.js.html @@ -44,181 +44,197 @@

Source: src/js/views/portals/editor/PortalEditorView.js
-
define(['underscore',
-        'jquery',
-        'backbone',
-        'models/portals/PortalModel',
-        "models/portals/PortalImage",
-        "collections/Filters",
-        'views/EditorView',
-        "views/SignInView",
-        "views/portals/editor/PortEditorSectionsView",
-        "views/portals/editor/PortEditorImageView",
-        "text!templates/loading.html",
-        "text!templates/portals/editor/portalEditor.html",
-        "text!templates/portals/editor/portalEditorSubmitMessage.html",
-        "text!templates/portals/editor/portalLoginPage.html"
-      ],
-function(_, $, Backbone, Portal, PortalImage, Filters, EditorView, SignInView,
-  PortEditorSectionsView, ImageEdit, LoadingTemplate, Template,
-  portalEditorSubmitMessageTemplate, LoginTemplate){
-
+            
define([
+  "underscore",
+  "jquery",
+  "backbone",
+  "models/portals/PortalModel",
+  "models/portals/PortalImage",
+  "collections/Filters",
+  "views/EditorView",
+  "views/SignInView",
+  "views/portals/editor/PortEditorSectionsView",
+  "views/portals/editor/PortEditorImageView",
+  "text!templates/loading.html",
+  "text!templates/portals/editor/portalEditor.html",
+  "text!templates/portals/editor/portalEditorSubmitMessage.html",
+  "text!templates/portals/editor/portalLoginPage.html",
+], function (
+  _,
+  $,
+  Backbone,
+  Portal,
+  PortalImage,
+  Filters,
+  EditorView,
+  SignInView,
+  PortEditorSectionsView,
+  ImageEdit,
+  LoadingTemplate,
+  Template,
+  portalEditorSubmitMessageTemplate,
+  LoginTemplate,
+) {
   /**
-  * @class PortalEditorView
-  * @classdesc A view of a form for creating and editing DataONE Portal documents
-  * @classcategory Views/Portals/Editor
-  * @name PortalEditorView
-  * @extends EditorView
-  * @constructs
-  */
+   * @class PortalEditorView
+   * @classdesc A view of a form for creating and editing DataONE Portal documents
+   * @classcategory Views/Portals/Editor
+   * @name PortalEditorView
+   * @extends EditorView
+   * @constructs
+   */
   var PortalEditorView = EditorView.extend(
-    /** @lends PortalEditorView.prototype */{
-
-    /**
-    * The type of View this is
-    * @type {string}
-    */
-    type: "PortalEditor",
-
-    /**
-    * The short name OR pid for the portal
-    * @type {string}
-    */
-    portalIdentifier: "",
-
-    /**
-    * The PortalModel that is being edited
-    * @type {Portal}
-    */
-    model: undefined,
-
-    /**
-    * The currently active editor section. e.g. Data, Metrics, Settings, etc.
-    * @type {string}
-    */
-    activeSectionLabel: "",
-
-    /**
-    * When a new portal is being created, this is the label of the section that will be active when the editor first renders
-    * @type {string}
-    */
-    newPortalActiveSectionLabel: (MetacatUI.appModel.get("portalDefaults") ? MetacatUI.appModel.get("portalDefaults").newPortalActiveSectionLabel : "") || "Settings",
-
-    /**
-    * References to templates for this view. HTML files are converted to Underscore.js templates
-    */
-    template: _.template(Template),
-    loadingTemplate: _.template(LoadingTemplate),
-    loginTemplate: _.template(LoginTemplate),
-    // Over-ride the default editor submit message template (which is currently
-    // used by the metadata editor) with the portal editor version
-    editorSubmitMessageTemplate: _.template(portalEditorSubmitMessageTemplate),
-
-    /**
-    * An array of Backbone Views that are contained in this view.
-    * @type {Backbone.View[]}
-    */
-    subviews: [],
-
-    /**
-    * A reference to the PortEditorSectionsView for this instance of the PortEditorView
-    * @type {PortEditorSectionsView}
-    */
-    sectionsView: null,
-
-    /**
-    * The text to use in the editor submit button
-    * @type {string}
-    */
-    submitButtonText: "Save",
-
-    /**
-    * A jQuery selector for the element that the PortEditorSectionsView should be inserted into
-    * @type {string}
-    */
-    portEditSectionsContainer: ".port-editor-sections-container",
-
-    /**
-    * A jQuery selector for the element that the portal logo image uploader
-    * should be inserted into
-    * @type {string}
-    */
-    portEditLogoContainer: ".logo-editor-container",
-
-    /**
-    * A jQuery selector for links to view this portal
-    * @type {string}
-    */
-    viewPortalLinks: ".view-portal-link",
-
-    /**
-    * A temporary name to use for portals when they are first created but don't have a label yet.
-    * This name should only be used in views, and never set on the model so it doesn't risk getting
-    * serialized and saved.
-    * @type {string}
-    */
-    newPortalTempName: "new",
-
-    /**
-    * The events this view will listen to and the associated function to call.
-    * This view will inherit events from the parent class, EditorView.
-    * @type {Object}
-    */
-    events: _.extend(EditorView.prototype.events, {
-      "focusout .basic-text"                  : "updateBasicText",
-      "click .section-links-toggle-container" : "toggleSectionLinks"
-    }),
-
-    /**
-    * Is executed when a new PortalEditorView is created
-    * @param {Object} options - A literal object with options to pass to the view
-    */
-    initialize: function(options){
-
-      EditorView.prototype.initialize.call(this, options);
-
-      //Reset arrays and objects set on this View, otherwise they will be shared across intances, causing errors
-      this.subviews = new Array();
-      this.sectionsView = null;
-
-      if(typeof options == "object"){
-        // initializing the PortalEditorView properties
-        this.portalIdentifier = options.portalIdentifier ? options.portalIdentifier : undefined;
-        this.activeSectionLabel = options.activeSectionLabel || "";
-      }
-
-    },
-
-    /**
-    * Renders the PortalEditorView
-    */
-    render: function(){
-
-      //Execute the superclass render() function, which will add some basic Editor functionality
-      EditorView.prototype.render.call(this);
-
-      $("body").addClass("Portal");
-
-      // Display a spinner to indicate loading until model is created.
-      this.$el.html(this.loadingTemplate({
-        msg: "Retrieving portal details..."
-      }));
-
-      //Create the model
-      this.createModel();
-
-      // An existing portal should have a portalIdentifier already set
-      // from the router, that does not equal the newPortalTempName ("new"),
-      // plus a seriesId or label set during createModel()
-      if (
-        (this.model.get("seriesId") || this.model.get("label"))
-        &&
-        (this.portalIdentifier && this.portalIdentifier != this.newPortalTempName)
-      ){
+    /** @lends PortalEditorView.prototype */ {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "PortalEditor",
+
+      /**
+       * The short name OR pid for the portal
+       * @type {string}
+       */
+      portalIdentifier: "",
+
+      /**
+       * The PortalModel that is being edited
+       * @type {Portal}
+       */
+      model: undefined,
+
+      /**
+       * The currently active editor section. e.g. Data, Metrics, Settings, etc.
+       * @type {string}
+       */
+      activeSectionLabel: "",
+
+      /**
+       * When a new portal is being created, this is the label of the section that will be active when the editor first renders
+       * @type {string}
+       */
+      newPortalActiveSectionLabel:
+        (MetacatUI.appModel.get("portalDefaults")
+          ? MetacatUI.appModel.get("portalDefaults").newPortalActiveSectionLabel
+          : "") || "Settings",
+
+      /**
+       * References to templates for this view. HTML files are converted to Underscore.js templates
+       */
+      template: _.template(Template),
+      loadingTemplate: _.template(LoadingTemplate),
+      loginTemplate: _.template(LoginTemplate),
+      // Over-ride the default editor submit message template (which is currently
+      // used by the metadata editor) with the portal editor version
+      editorSubmitMessageTemplate: _.template(
+        portalEditorSubmitMessageTemplate,
+      ),
+
+      /**
+       * An array of Backbone Views that are contained in this view.
+       * @type {Backbone.View[]}
+       */
+      subviews: [],
+
+      /**
+       * A reference to the PortEditorSectionsView for this instance of the PortEditorView
+       * @type {PortEditorSectionsView}
+       */
+      sectionsView: null,
+
+      /**
+       * The text to use in the editor submit button
+       * @type {string}
+       */
+      submitButtonText: "Save",
+
+      /**
+       * A jQuery selector for the element that the PortEditorSectionsView should be inserted into
+       * @type {string}
+       */
+      portEditSectionsContainer: ".port-editor-sections-container",
+
+      /**
+       * A jQuery selector for the element that the portal logo image uploader
+       * should be inserted into
+       * @type {string}
+       */
+      portEditLogoContainer: ".logo-editor-container",
+
+      /**
+       * A jQuery selector for links to view this portal
+       * @type {string}
+       */
+      viewPortalLinks: ".view-portal-link",
+
+      /**
+       * A temporary name to use for portals when they are first created but don't have a label yet.
+       * This name should only be used in views, and never set on the model so it doesn't risk getting
+       * serialized and saved.
+       * @type {string}
+       */
+      newPortalTempName: "new",
+
+      /**
+       * The events this view will listen to and the associated function to call.
+       * This view will inherit events from the parent class, EditorView.
+       * @type {Object}
+       */
+      events: _.extend(EditorView.prototype.events, {
+        "focusout .basic-text": "updateBasicText",
+        "click .section-links-toggle-container": "toggleSectionLinks",
+      }),
+
+      /**
+       * Is executed when a new PortalEditorView is created
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        EditorView.prototype.initialize.call(this, options);
+
+        //Reset arrays and objects set on this View, otherwise they will be shared across intances, causing errors
+        this.subviews = new Array();
+        this.sectionsView = null;
+
+        if (typeof options == "object") {
+          // initializing the PortalEditorView properties
+          this.portalIdentifier = options.portalIdentifier
+            ? options.portalIdentifier
+            : undefined;
+          this.activeSectionLabel = options.activeSectionLabel || "";
+        }
+      },
+
+      /**
+       * Renders the PortalEditorView
+       */
+      render: function () {
+        //Execute the superclass render() function, which will add some basic Editor functionality
+        EditorView.prototype.render.call(this);
+
+        $("body").addClass("Portal");
+
+        // Display a spinner to indicate loading until model is created.
+        this.$el.html(
+          this.loadingTemplate({
+            msg: "Retrieving portal details...",
+          }),
+        );
+
+        //Create the model
+        this.createModel();
+
+        // An existing portal should have a portalIdentifier already set
+        // from the router, that does not equal the newPortalTempName ("new"),
+        // plus a seriesId or label set during createModel()
+        if (
+          (this.model.get("seriesId") || this.model.get("label")) &&
+          this.portalIdentifier &&
+          this.portalIdentifier != this.newPortalTempName
+        ) {
           var view = this;
 
-          this.listenToOnce(this.model, "change:isAuthorized", function(){
-
+          this.listenToOnce(this.model, "change:isAuthorized", function () {
             if (this.model.get("isAuthorized")) {
               // When an existing model has been synced render the results
               view.stopListening(view.model, "sync", view.renderPortalEditor);
@@ -235,788 +251,920 @@ 

Source: src/js/views/portals/editor/PortalEditorView.jsYou're one step away from the portal builder</strong><br>Start by signing in with your ORCID" - } - - this.$el.html(this.loginTemplate({ - title: title, - portalInfoLink: MetacatUI.appModel.get("portalInfoURL"), - portalImageSrc: MetacatUI.root + "/img/portals/portal-data-page-example.png", - altText: "Screen shot of a portal data page for a climate research lab. The page shows a search bar, customized filters, and a map of the the geographic area the data covers." - })); - - }, - - /** - * The DataONE Plus Subscription if fetched from Bookkeeper and the status of the - * Subscription is rendered on the page. - * Subviews in this view should have their own renderSubscriptionInfo() function - * that inserts subscription details into the subview. - */ - renderSubscriptionInfo: function(){ - if( MetacatUI.appModel.get("enableBookkeeperServices") ){ - - if( MetacatUI.appUserModel.get("loggedIn") && MetacatUI.appUserModel.get("dataoneSubscription") ){ - //Show the free trial message for this portal, if the subscription is in a free trial - var subscription = MetacatUI.appUserModel.get("dataoneSubscription"), - isFreeTrial = false; - - //If the Subscription is in free trial mode - if( subscription && subscription.isTrialing() ){ - - if( MetacatUI.appModel.get("dataonePlusPreviewMode") ){ - //If this portal is not in the configured list of Plus portals - var trialExceptions = MetacatUI.appModel.get("dataonePlusPreviewPortals"); - isFreeTrial = !_.findWhere(trialExceptions, { seriesId: this.model.get("seriesId") }); - } - else{ - isFreeTrial = true; - } - - if( isFreeTrial ){ - //Show a free trial message in the editor footer - var freeTrialMessage = "This " + MetacatUI.appModel.get("portalTermSingular") + " is a free preview of " + MetacatUI.appModel.get("dataonePlusName"); - var messageEl = $(document.createElement("span")) - .addClass("free-trial-message") - .text(freeTrialMessage) - .prepend( $(document.createElement("i")).addClass("dataone-plus-icon-container") ); - this.$("#editor-footer").prepend(messageEl); - - // Update the label element to randomly generated label - // And disable the input - var labelEL = $('.label-input-text'); - labelEL.val(this.model.get("label")); - - //When the Portal Model label is changed, update the input - this.listenTo(this.model, "change:label", function(){ - $('.label-input-text').val(this.model.get("label")); - }); - - labelEL.attr("disabled", "disabled"); - //Remove the "Change URL" button that toggles the label input - this.$(".btn.change-label").remove(); + remove: true, + }, + ); - // Show edit label message if the edit button is disabled - var editLabelMessage = "Create a custom " + MetacatUI.appModel.get("portalTermSingular") + " name for the URL when your free preview of " + - "<i class='dataone-plus-icon-container'></i>" + MetacatUI.appModel.get("dataonePlusName") + " ends."; - var messageContainer = this.$(".label-container .notification").html(editLabelMessage).addClass("free-trial"); + //Hide the saving styling + this.hideSaving(); + } + }, + + /** + * Shows a validation error message and adds error styling to the given elements + * @param {jQuery} elements - The elements to add error styling and messaging to + * @param {string} errorMsg - The error message to display + */ + showValidationMessage: function (elements, errorMsg) { + //Show the error message + elements.filter(".notification").addClass("error").text(errorMsg); + + //Add the error class to inputs + var inputs = elements.filter("textarea, input").addClass("error"); + + //Show the validation message in the portal sections + if (this.sectionsView) { + this.sectionsView.showValidation(elements); + } + }, + + /** + * Removes all the validation error styling and messaging from this view + */ + removeValidation: function () { + EditorView.prototype.removeValidation.call(this); + this.$( + ".section-link-container.error, input.error, textarea.error", + ).removeClass("error"); + }, + + /** + * Show Sign In buttons + */ + showSignIn: function () { + // Messsage if the user is trying to edit an existing portal + var title = "Sign in with your ORCID to edit this portal"; + // Message to create a portal if the portal is new + if (this.model.get("isNew")) { + title = + "<strong>You're one step away from the portal builder</strong><br>Start by signing in with your ORCID"; + } - if( !messageContainer.is(":visible") ){ - messageContainer.detach().appendTo( this.$(".change-label-container") ); + this.$el.html( + this.loginTemplate({ + title: title, + portalInfoLink: MetacatUI.appModel.get("portalInfoURL"), + portalImageSrc: + MetacatUI.root + "/img/portals/portal-data-page-example.png", + altText: + "Screen shot of a portal data page for a climate research lab. The page shows a search bar, customized filters, and a map of the the geographic area the data covers.", + }), + ); + }, + + /** + * The DataONE Plus Subscription if fetched from Bookkeeper and the status of the + * Subscription is rendered on the page. + * Subviews in this view should have their own renderSubscriptionInfo() function + * that inserts subscription details into the subview. + */ + renderSubscriptionInfo: function () { + if (MetacatUI.appModel.get("enableBookkeeperServices")) { + if ( + MetacatUI.appUserModel.get("loggedIn") && + MetacatUI.appUserModel.get("dataoneSubscription") + ) { + //Show the free trial message for this portal, if the subscription is in a free trial + var subscription = MetacatUI.appUserModel.get( + "dataoneSubscription", + ), + isFreeTrial = false; + + //If the Subscription is in free trial mode + if (subscription && subscription.isTrialing()) { + if (MetacatUI.appModel.get("dataonePlusPreviewMode")) { + //If this portal is not in the configured list of Plus portals + var trialExceptions = MetacatUI.appModel.get( + "dataonePlusPreviewPortals", + ); + isFreeTrial = !_.findWhere(trialExceptions, { + seriesId: this.model.get("seriesId"), + }); + } else { + isFreeTrial = true; } - //Insert the DataONE Plus icon - var viewRef = this; - require(["text!templates/dataonePlusIcon.html"], function(iconTemplate){ - viewRef.$(".dataone-plus-icon-container").html(iconTemplate); - }); + if (isFreeTrial) { + //Show a free trial message in the editor footer + var freeTrialMessage = + "This " + + MetacatUI.appModel.get("portalTermSingular") + + " is a free preview of " + + MetacatUI.appModel.get("dataonePlusName"); + var messageEl = $(document.createElement("span")) + .addClass("free-trial-message") + .text(freeTrialMessage) + .prepend( + $(document.createElement("i")).addClass( + "dataone-plus-icon-container", + ), + ); + this.$("#editor-footer").prepend(messageEl); + + // Update the label element to randomly generated label + // And disable the input + var labelEL = $(".label-input-text"); + labelEL.val(this.model.get("label")); + + //When the Portal Model label is changed, update the input + this.listenTo(this.model, "change:label", function () { + $(".label-input-text").val(this.model.get("label")); + }); + + labelEL.attr("disabled", "disabled"); + //Remove the "Change URL" button that toggles the label input + this.$(".btn.change-label").remove(); + + // Show edit label message if the edit button is disabled + var editLabelMessage = + "Create a custom " + + MetacatUI.appModel.get("portalTermSingular") + + " name for the URL when your free preview of " + + "<i class='dataone-plus-icon-container'></i>" + + MetacatUI.appModel.get("dataonePlusName") + + " ends."; + var messageContainer = this.$(".label-container .notification") + .html(editLabelMessage) + .addClass("free-trial"); + + if (!messageContainer.is(":visible")) { + messageContainer + .detach() + .appendTo(this.$(".change-label-container")); + } + //Insert the DataONE Plus icon + var viewRef = this; + require(["text!templates/dataonePlusIcon.html"], function ( + iconTemplate, + ) { + viewRef.$(".dataone-plus-icon-container").html(iconTemplate); + }); + } } + } else { + this.listenTo( + MetacatUI.appUserModel, + "change:dataoneSubscription", + this.renderSubscriptionInfo, + ); } } - else{ - this.listenTo( MetacatUI.appUserModel, "change:dataoneSubscription", this.renderSubscriptionInfo ); + }, + + /** + * @inheritdoc + */ + isAccessPolicyEditEnabled: function () { + if (!MetacatUI.appModel.get("allowAccessPolicyChanges")) { + return false; } - } - }, - - /** - * @inheritdoc - */ - isAccessPolicyEditEnabled: function(){ - - if( !MetacatUI.appModel.get("allowAccessPolicyChanges") ){ - return false; - } - - if( !MetacatUI.appModel.get("allowAccessPolicyChangesPortals") ){ - return false; - } - - let limitedTo = MetacatUI.appModel.get("allowAccessPolicyChangesPortalsForSubjects"); - if( Array.isArray(limitedTo) && limitedTo.length ){ - - return _.intersection(limitedTo, MetacatUI.appUserModel.get("allIdentitiesAndGroups")).length > 0; - - } - else{ - return true; - } - - }, - - /** - * If the given portal doesn't exist, display a Not Found message. - */ - showNotFound: function(){ - - this.hideLoading(); - - var notFoundMessage = $(document.createElement("p")).text("The " + MetacatUI.appModel.get("portalTermSingular") + " "); - notFoundMessage.append( $(document.createElement("span")).text(this.model.get("label") || this.portalIdentifier) ) - .append( $(document.createElement("span")).text(" doesn't exist.") ); + if (!MetacatUI.appModel.get("allowAccessPolicyChangesPortals")) { + return false; + } - MetacatUI.appView.showAlert(notFoundMessage, "alert-error non-fixed", this.$el, undefined, { remove: true }); - }, + let limitedTo = MetacatUI.appModel.get( + "allowAccessPolicyChangesPortalsForSubjects", + ); + if (Array.isArray(limitedTo) && limitedTo.length) { + return ( + _.intersection( + limitedTo, + MetacatUI.appUserModel.get("allIdentitiesAndGroups"), + ).length > 0 + ); + } else { + return true; + } + }, - /** - * This function is called whenever the window is scrolled. - */ - handleScroll: function() { + /** + * If the given portal doesn't exist, display a Not Found message. + */ + showNotFound: function () { + this.hideLoading(); + var notFoundMessage = $(document.createElement("p")).text( + "The " + MetacatUI.appModel.get("portalTermSingular") + " ", + ); + notFoundMessage + .append( + $(document.createElement("span")).text( + this.model.get("label") || this.portalIdentifier, + ), + ) + .append($(document.createElement("span")).text(" doesn't exist.")); + + MetacatUI.appView.showAlert( + notFoundMessage, + "alert-error non-fixed", + this.$el, + undefined, + { remove: true }, + ); + }, + + /** + * This function is called whenever the window is scrolled. + */ + handleScroll: function () { try { - var menu = $(".section-links-toggle-container")[0], editorFooter = this.$("#editor-footer")[0], editorFooterHeight = editorFooter ? editorFooter.offsetHeight : 0, menuHeight = menu ? menu.offsetHeight : 0, - hiddenHeight = (menuHeight * -1) + editorFooterHeight, + hiddenHeight = menuHeight * -1 + editorFooterHeight, currentScrollPos = window.pageYOffset; - if(!menu){ - return + if (!menu) { + return; } - if(MetacatUI.appView.prevScrollpos >= currentScrollPos) { + if (MetacatUI.appView.prevScrollpos >= currentScrollPos) { // when scrolling upward menu.style.bottom = editorFooterHeight + "px"; } else { @@ -1024,42 +1172,41 @@

Source: src/js/views/portals/editor/PortalEditorView.js

diff --git a/docs/docs/src_js_views_projects_ProjectView.js.html b/docs/docs/src_js_views_projects_ProjectView.js.html index 965a89977..692d9ecd2 100644 --- a/docs/docs/src_js_views_projects_ProjectView.js.html +++ b/docs/docs/src_js_views_projects_ProjectView.js.html @@ -45,109 +45,120 @@

Source: src/js/views/projects/ProjectView.js

define([
-        'jquery',
-        'underscore',
-        'backbone',
-        'text!templates/projectInfo.html', 'models/projects/Project',
-        'collections/ProjectList'],
-    function ($, _, Backbone, template, Project, ProjectList) {
-        /**
-         * @class ProjectView
-         * @classdesc    This is a base view for projects list loading. It is structured
-         * on the premise that a project gets selected to be having its details viewed. The template associated
-         * with this view has a placeholder to render the projects with the desired way.
-         * @classcategory Views/Projects
-         * @extends Backbone.View
-         * @since 2.22.0
-         */
-        var ProjectView = Backbone.View.extend(
-            /** @lends ProjectView.prototype */{
-            el: "#Content",
-            template: _.template(template),
-            projectList: undefined, // Set default list if not using projectsApiUrl
-            selectedProject: undefined,
-            load: undefined,
-            events: {
-                "change #projects": "handleSelectProject"
-            },
-
-            initialize: function (options) {
-                this.getProjectsList()
-
-
-                // After projectList fetch call is successful, set the default selected project to the first project
-                // received from the projectsList then render to load the select element.
-                this.listenTo(this.projectList, 'sync', this.setSelectedProject);
-                this.listenTo(this.projectList, 'sync', this.render);
-
-                // In case the token was not loaded already, get the projects after it gets loaded.
-                this.listenTo(MetacatUI.appUserModel, 'change:token', this.getProjectsList);
-            },
-
-            /**
-             *
-             * @returns {ProjectView}
-             */
-            render: function () {
-                // If a project selection logic is implemented, pass down the selection.
-                if(this.selectedProject !== undefined) {
-                    this.$el.html(this.template({
-                        projectList: this.projectList,
-                        selectedProject: this.selectedProject
-                    }));
-                } else {
-                    this.$el.html(this.template({
-                        projectList: this.projectList,
-                    }));
-                }
-                return this;
-            },
-
-            /**
-             * Handles the change event for selecting a project in the dropdown and then render.
-             * @param {Event} e
-             * @since 2.22.0
-             */
-            handleSelectProject: function (e) {
-                // Set the selectedProject based on the selected project id from the select element.
-                this.selectedProject = this.projectList.findWhere({id: e.target.value});
-                this.render();
-                return;
-            },
-
-            /**
-             * Call back to set the selectedProject
-             * This is used as a callback to only set the current project on the success of the fetch call.
-             *  @since 2.22.0
-             */
-            setSelectedProject: function () {
-                if (this.selectedProject === undefined)
-                    this.selectedProject = this.projectList.at(0)
-                this.render()
-            },
-
-            /**
-             * Call back to initialize the ProjectsList
-             * This is used as a callback so that the fetch would happen after the change:token event gets loaded.
-             *  @since 2.22.0
-             */
-            getProjectsList: function (){
-                // Note that if the projectsApiUrl config is not set, projectsList will fall to the default set.
-                if(MetacatUI.appModel.get("projectsApiUrl")){
-                    // Get the projects for the user
-                    this.projectList = new ProjectList({
-                        authToken: MetacatUI.appUserModel.get("token"),
-                        urlBase: MetacatUI.appModel.get("projectsApiUrl")
-                    })
-                    this.projectList.fetch({
-                        parse: true,
-                        data: {can_manage: true}
-                    })
-                }
-            }
+  "jquery",
+  "underscore",
+  "backbone",
+  "text!templates/projectInfo.html",
+  "models/projects/Project",
+  "collections/ProjectList",
+], function ($, _, Backbone, template, Project, ProjectList) {
+  /**
+   * @class ProjectView
+   * @classdesc    This is a base view for projects list loading. It is structured
+   * on the premise that a project gets selected to be having its details viewed. The template associated
+   * with this view has a placeholder to render the projects with the desired way.
+   * @classcategory Views/Projects
+   * @extends Backbone.View
+   * @since 2.22.0
+   */
+  var ProjectView = Backbone.View.extend(
+    /** @lends ProjectView.prototype */ {
+      el: "#Content",
+      template: _.template(template),
+      projectList: undefined, // Set default list if not using projectsApiUrl
+      selectedProject: undefined,
+      load: undefined,
+      events: {
+        "change #projects": "handleSelectProject",
+      },
+
+      initialize: function (options) {
+        this.getProjectsList();
+
+        // After projectList fetch call is successful, set the default selected project to the first project
+        // received from the projectsList then render to load the select element.
+        this.listenTo(this.projectList, "sync", this.setSelectedProject);
+        this.listenTo(this.projectList, "sync", this.render);
+
+        // In case the token was not loaded already, get the projects after it gets loaded.
+        this.listenTo(
+          MetacatUI.appUserModel,
+          "change:token",
+          this.getProjectsList,
+        );
+      },
+
+      /**
+       *
+       * @returns {ProjectView}
+       */
+      render: function () {
+        // If a project selection logic is implemented, pass down the selection.
+        if (this.selectedProject !== undefined) {
+          this.$el.html(
+            this.template({
+              projectList: this.projectList,
+              selectedProject: this.selectedProject,
+            }),
+          );
+        } else {
+          this.$el.html(
+            this.template({
+              projectList: this.projectList,
+            }),
+          );
+        }
+        return this;
+      },
+
+      /**
+       * Handles the change event for selecting a project in the dropdown and then render.
+       * @param {Event} e
+       * @since 2.22.0
+       */
+      handleSelectProject: function (e) {
+        // Set the selectedProject based on the selected project id from the select element.
+        this.selectedProject = this.projectList.findWhere({
+          id: e.target.value,
         });
-        return ProjectView;
-    });
+        this.render();
+        return;
+      },
+
+      /**
+       * Call back to set the selectedProject
+       * This is used as a callback to only set the current project on the success of the fetch call.
+       *  @since 2.22.0
+       */
+      setSelectedProject: function () {
+        if (this.selectedProject === undefined)
+          this.selectedProject = this.projectList.at(0);
+        this.render();
+      },
+
+      /**
+       * Call back to initialize the ProjectsList
+       * This is used as a callback so that the fetch would happen after the change:token event gets loaded.
+       *  @since 2.22.0
+       */
+      getProjectsList: function () {
+        // Note that if the projectsApiUrl config is not set, projectsList will fall to the default set.
+        if (MetacatUI.appModel.get("projectsApiUrl")) {
+          // Get the projects for the user
+          this.projectList = new ProjectList({
+            authToken: MetacatUI.appUserModel.get("token"),
+            urlBase: MetacatUI.appModel.get("projectsApiUrl"),
+          });
+          this.projectList.fetch({
+            parse: true,
+            data: { can_manage: true },
+          });
+        }
+      },
+    },
+  );
+  return ProjectView;
+});
 
diff --git a/docs/docs/src_js_views_queryBuilder_QueryBuilderView.js.html b/docs/docs/src_js_views_queryBuilder_QueryBuilderView.js.html index 62e59bf6e..1125d1717 100644 --- a/docs/docs/src_js_views_queryBuilder_QueryBuilderView.js.html +++ b/docs/docs/src_js_views_queryBuilder_QueryBuilderView.js.html @@ -44,455 +44,495 @@

Source: src/js/views/queryBuilder/QueryBuilderView.js

-
define(["jquery",
-    "underscore",
-    "backbone",
-    "collections/Filters",
-    "collections/queryFields/QueryFields",
-    "views/searchSelect/SearchableSelectView",
-    "views/queryBuilder/QueryRuleView",
-    "text!templates/queryBuilder/queryBuilder.html"
-  ],
-  function($, _, Backbone, Filters, QueryFields, SearchableSelect, QueryRule, Template) {
-
-    /**
-     * @class QueryBuilderView
-     * @classdesc A view that provides a UI for users to construct a complex search
-     * through the DataONE Solr index
-     * @classcategory Views/QueryBuilder
-     * @screenshot views/QueryBuilderView.png
-     * @extends Backbone.View
-     * @constructor
-     * @since 2.14.0
-     */
-    var QueryBuilderView = Backbone.View.extend(
-      /** @lends QueryBuilderView.prototype */
-      {
-
-        /**
-        * The type of View this is
-        * @type {string}
-        */
-        type: "QueryBuilderView",
-
-        /**
-         * The HTML class names for this view element
-         * @type {string}
-         */
-        className: "query-builder",
-
-        /**
-         * A JQuery selector for the element in the template that will contain the query
-         * rules
-         * @type {string}
-         */
-        rulesContainerSelector: ".rules-container",
-
-        /**
-         * An ID for the element in the template that a user should click to add a new
-         * rule. A unique ID will be appended to this ID, and the ID will be added to the
-         * template.
-         * @type {string}
-         * @since 2.17.0
-         */
-        addRuleButtonID: "add-rule-",
-
-        /**
-         * An ID for the element in the template that a user should click to add a new
-         * rule group. A unique ID will be appended to this ID, and the ID will be added
-         * to the template.
-         * @type {string}
-         * @since 2.17.0
-         */
-        addRuleGroupButtonID: "add-rule-group-",
-
-        /**
-         * A JQuery selector for the element in the template that will contain the input
-         * allowing a user to switch the exclude attribute from "include" to "exclude"
-         * (i.e. to switch between exclude:false and exclude:true in the filterGroup
-         * model.)
-         * @type {string}
-         * @since 2.17.0
-         */
-        excludeInputSelector: ".exclude-input",
-
-        /**
-         * A JQuery selector for the element in the template that will contain the input
-         * allowing a user to switch the operator from "all" to "any" (i.e. to switch
-         * between operator:"AND" and exclude:"OR" in the filterGroup model.)
-         * @type {string}
-         * @since 2.17.0
-         */
-        operatorInputSelector: ".operator-input",
-
-        /**
-         * The maximum number of levels nested Rule Groups (i.e. nested FilterGroup
-         * models) that a user is permitted to *build* in the Query Builder. If a
-         * Portal/Collection document is loaded into the Query Builder that has more than
-         * the maximum allowable nested levels, those levels will still be displayed. This
-         * only prevents the "Add Rule Group" button from being shown.
-         * @type {number}
-         * @since 2.17.0
-         */
-        nestedLevelsAllowed: 1,
-
-        /**
-         * An array of hex color codes used to help distinguish between different rules
-         * @type {string[]}
-         */
-        ruleColorPalette: ["#44AA99", "#137733", "#c9a538", "#CC6677", "#882355",
-          "#AA4499","#332288"
-        ],
-
-        /**
-         * Query fields to exclude in the metadata field selector of each Query Rule. This
-         * is a list of field names that exist in the query service index (i.e. Solr), but
-         * which should be hidden in the Query Builder
-         * @type {string[]}
-         */
-        excludeFields: [],
-
-        /**        
-         * Query fields to exclude in the metadata field selector for any Query Rules that
-         * are in nested Query Builders (i.e. in nested Filter Groups). This is a list of
-         * field names that exist in the query service index (i.e. Solr), but which should
-         * be hidden in nested Query Builders
-         * @type {string[]}
-         */
-        nestedExcludeFields: [],
-
-        /**
-         * Query fields that do not exist in the query service index, but which we would
-         * like to show as options in the Query Builder field input.
-         * 
-         * @type {SpecialField[]}
-         *
-         * @since 2.15.0
-         */
-        specialFields: [],
-
-        /**
-        * A Filters collection that stores filters to be edited with this Query Builder,
-        * e.g. the definitionFilters in a Collection or Portal model. If a filterGroup is
-        * set, then collection doesn't necessarily need to be set, as the Filters
-        * collection from within the FilterGroup model will automatically be set on view.
-        * @type {Filters}
-        */
-        collection: null,
-
-        /**
-        * The FilterGroup model that stores the filters, the exclude attribute, and the
-        * group operator to be edited with this Query Builder. This does not need to be
-        * set; just a Filters collection can be set on the view instead, but then there
-        * will be no input to switch between the include & exclude and any & all, since
-        * these are the exclude and operator attributes on the filterGroup model.
-        * @type {FilterGroup}
-        * @since 2.17.0
-        */
-        filterGroup: null,
-
-        /**
-         * The primary HTML template for this view
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * events - A function that specifies a set of DOM events that will be bound to
-         * methods on your View through Backbone.delegateEvents.
-         * @see {@link https://backbonejs.org/#View-events}
-         *
-         * @return {Object}  The events hash
-         */
-        events: function(){
-          try {
-            var events = {};
-            var addRuleAction = "click #" + this.addRuleButtonID + this.cid;
-            events[addRuleAction] = "addQueryRule"
-            var addRuleGroupAction = "click #" + this.addRuleGroupButtonID + this.cid;
-            events[addRuleGroupAction] = "addQueryRuleGroup"
-            return events
-          } catch (e) {
-            console.error("Failed to specify events for  the Query Builder View," +
-              " error message: " + e);
-          }
-        },
-
-        /**
-         * The list of QueryRuleViews that are contained within this queryBuilder
-         * @type {QueryRuleView[]}
-         */
-        rules: [],
-
-        /**
-         * Creates a new QueryBuilderView
-         * @param {Object} options - A literal object with options to pass to the view
-         */
-        initialize: function(options) {
-
-          try {
-
-            // Get all the options and apply them to this view
-            if (typeof options == "object") {
-              var optionKeys = Object.keys(options);
-              _.each(optionKeys, function(key, i) {
+            
define([
+  "jquery",
+  "underscore",
+  "backbone",
+  "collections/Filters",
+  "collections/queryFields/QueryFields",
+  "views/searchSelect/SearchableSelectView",
+  "views/queryBuilder/QueryRuleView",
+  "text!templates/queryBuilder/queryBuilder.html",
+], function (
+  $,
+  _,
+  Backbone,
+  Filters,
+  QueryFields,
+  SearchableSelect,
+  QueryRule,
+  Template,
+) {
+  /**
+   * @class QueryBuilderView
+   * @classdesc A view that provides a UI for users to construct a complex search
+   * through the DataONE Solr index
+   * @classcategory Views/QueryBuilder
+   * @screenshot views/QueryBuilderView.png
+   * @extends Backbone.View
+   * @constructor
+   * @since 2.14.0
+   */
+  var QueryBuilderView = Backbone.View.extend(
+    /** @lends QueryBuilderView.prototype */
+    {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "QueryBuilderView",
+
+      /**
+       * The HTML class names for this view element
+       * @type {string}
+       */
+      className: "query-builder",
+
+      /**
+       * A JQuery selector for the element in the template that will contain the query
+       * rules
+       * @type {string}
+       */
+      rulesContainerSelector: ".rules-container",
+
+      /**
+       * An ID for the element in the template that a user should click to add a new
+       * rule. A unique ID will be appended to this ID, and the ID will be added to the
+       * template.
+       * @type {string}
+       * @since 2.17.0
+       */
+      addRuleButtonID: "add-rule-",
+
+      /**
+       * An ID for the element in the template that a user should click to add a new
+       * rule group. A unique ID will be appended to this ID, and the ID will be added
+       * to the template.
+       * @type {string}
+       * @since 2.17.0
+       */
+      addRuleGroupButtonID: "add-rule-group-",
+
+      /**
+       * A JQuery selector for the element in the template that will contain the input
+       * allowing a user to switch the exclude attribute from "include" to "exclude"
+       * (i.e. to switch between exclude:false and exclude:true in the filterGroup
+       * model.)
+       * @type {string}
+       * @since 2.17.0
+       */
+      excludeInputSelector: ".exclude-input",
+
+      /**
+       * A JQuery selector for the element in the template that will contain the input
+       * allowing a user to switch the operator from "all" to "any" (i.e. to switch
+       * between operator:"AND" and exclude:"OR" in the filterGroup model.)
+       * @type {string}
+       * @since 2.17.0
+       */
+      operatorInputSelector: ".operator-input",
+
+      /**
+       * The maximum number of levels nested Rule Groups (i.e. nested FilterGroup
+       * models) that a user is permitted to *build* in the Query Builder. If a
+       * Portal/Collection document is loaded into the Query Builder that has more than
+       * the maximum allowable nested levels, those levels will still be displayed. This
+       * only prevents the "Add Rule Group" button from being shown.
+       * @type {number}
+       * @since 2.17.0
+       */
+      nestedLevelsAllowed: 1,
+
+      /**
+       * An array of hex color codes used to help distinguish between different rules
+       * @type {string[]}
+       */
+      ruleColorPalette: [
+        "#44AA99",
+        "#137733",
+        "#c9a538",
+        "#CC6677",
+        "#882355",
+        "#AA4499",
+        "#332288",
+      ],
+
+      /**
+       * Query fields to exclude in the metadata field selector of each Query Rule. This
+       * is a list of field names that exist in the query service index (i.e. Solr), but
+       * which should be hidden in the Query Builder
+       * @type {string[]}
+       */
+      excludeFields: [],
+
+      /**
+       * Query fields to exclude in the metadata field selector for any Query Rules that
+       * are in nested Query Builders (i.e. in nested Filter Groups). This is a list of
+       * field names that exist in the query service index (i.e. Solr), but which should
+       * be hidden in nested Query Builders
+       * @type {string[]}
+       */
+      nestedExcludeFields: [],
+
+      /**
+       * Query fields that do not exist in the query service index, but which we would
+       * like to show as options in the Query Builder field input.
+       *
+       * @type {SpecialField[]}
+       *
+       * @since 2.15.0
+       */
+      specialFields: [],
+
+      /**
+       * A Filters collection that stores filters to be edited with this Query Builder,
+       * e.g. the definitionFilters in a Collection or Portal model. If a filterGroup is
+       * set, then collection doesn't necessarily need to be set, as the Filters
+       * collection from within the FilterGroup model will automatically be set on view.
+       * @type {Filters}
+       */
+      collection: null,
+
+      /**
+       * The FilterGroup model that stores the filters, the exclude attribute, and the
+       * group operator to be edited with this Query Builder. This does not need to be
+       * set; just a Filters collection can be set on the view instead, but then there
+       * will be no input to switch between the include & exclude and any & all, since
+       * these are the exclude and operator attributes on the filterGroup model.
+       * @type {FilterGroup}
+       * @since 2.17.0
+       */
+      filterGroup: null,
+
+      /**
+       * The primary HTML template for this view
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * events - A function that specifies a set of DOM events that will be bound to
+       * methods on your View through Backbone.delegateEvents.
+       * @see {@link https://backbonejs.org/#View-events}
+       *
+       * @return {Object}  The events hash
+       */
+      events: function () {
+        try {
+          var events = {};
+          var addRuleAction = "click #" + this.addRuleButtonID + this.cid;
+          events[addRuleAction] = "addQueryRule";
+          var addRuleGroupAction =
+            "click #" + this.addRuleGroupButtonID + this.cid;
+          events[addRuleGroupAction] = "addQueryRuleGroup";
+          return events;
+        } catch (e) {
+          console.error(
+            "Failed to specify events for  the Query Builder View," +
+              " error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * The list of QueryRuleViews that are contained within this queryBuilder
+       * @type {QueryRuleView[]}
+       */
+      rules: [],
+
+      /**
+       * Creates a new QueryBuilderView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            var optionKeys = Object.keys(options);
+            _.each(
+              optionKeys,
+              function (key, i) {
                 this[key] = options[key];
-              }, this);
-            }
-
-            // If neither a Filters collection nor a FilterGroup model is provided in the
-            // options for this view, then create a new FilterGroup model and set it on
-            // the view.
-            if(!this.collection && !this.filterGroup){
-              this.filterGroup = new FilterGroup()
-            }
-
-            // If there is a FilterGroup model set, but no Filters collection, then use
-            // the Filters from within the FilterGroup model as the Filters collection.
-            if(!this.collection && this.filterGroup){
-              this.collection = this.filterGroup.get("filters")
-            }
-
-          } catch (e) {
-            console.error(
-              "Failed to initialize the Query Builder view, error message:", e
+              },
+              this,
             );
           }
-        },
-
-        /**
-         * render - Render the view
-         *
-         * @return {QueryBuilder}  Returns the view
-         */
-        render: function() {
-
-          try {
-
-            // Ensure the query fields are cached for the Query Field Select View and the
-            // Query Rule View
-            if (
-              typeof MetacatUI.queryFields === "undefined" ||
-              MetacatUI.queryFields.length === 0
-            ) {
-              MetacatUI.queryFields = new QueryFields();
-              this.listenToOnce(MetacatUI.queryFields, "sync", this.render)
-              MetacatUI.queryFields.fetch();
-              return
-            }
-
-            // Insert the template into the view
-            this.$el.html(this.template({
+
+          // If neither a Filters collection nor a FilterGroup model is provided in the
+          // options for this view, then create a new FilterGroup model and set it on
+          // the view.
+          if (!this.collection && !this.filterGroup) {
+            this.filterGroup = new FilterGroup();
+          }
+
+          // If there is a FilterGroup model set, but no Filters collection, then use
+          // the Filters from within the FilterGroup model as the Filters collection.
+          if (!this.collection && this.filterGroup) {
+            this.collection = this.filterGroup.get("filters");
+          }
+        } catch (e) {
+          console.error(
+            "Failed to initialize the Query Builder view, error message:",
+            e,
+          );
+        }
+      },
+
+      /**
+       * render - Render the view
+       *
+       * @return {QueryBuilder}  Returns the view
+       */
+      render: function () {
+        try {
+          // Ensure the query fields are cached for the Query Field Select View and the
+          // Query Rule View
+          if (
+            typeof MetacatUI.queryFields === "undefined" ||
+            MetacatUI.queryFields.length === 0
+          ) {
+            MetacatUI.queryFields = new QueryFields();
+            this.listenToOnce(MetacatUI.queryFields, "sync", this.render);
+            MetacatUI.queryFields.fetch();
+            return;
+          }
+
+          // Insert the template into the view
+          this.$el.html(
+            this.template({
               addRuleButtonID: this.addRuleButtonID + this.cid,
               addRuleGroupButtonID: this.addRuleGroupButtonID + this.cid,
-            }));
-
-            // Nested Query Builders are used to display nested filterGroup models.
-            // They need to be styled slightly different from the parent Query Builder.
-            if(this.parentRule){
-              this.$el.addClass("nested")
-            }
-
-            // Remove the rule group button ID if no more nested Query Builders are
-            // allowed.
-            if(
-              typeof this.nestedLevelsAllowed == "number" &&
-              this.nestedLevelsAllowed < 1
-            ){
-              this.$el.find("#" + this.addRuleGroupButtonID + this.cid).remove()
-            };
-
-            // Save the rules container element to the view before we add any nested
-            // QueryBuilders (nested FilterGroups), since their rules container uses the
-            // same selector.
-            this.rulesContainer = this.$el.find(this.rulesContainerSelector);
-
-            // If there is a FilterGroup model set on this view (not just a Filters
-            // collection) then render the inputs that allow a user to edit the "exclude"
-            // and "operator" attributes
-            if(this.filterGroup){
-              this.renderExcludeOperatorInputs();
-            }
-
-            // Add a row for each rule that exists already in the model
-            if(
-              this.collection && this.collection.models &&
-              this.collection.models.length
-            ){
-              this.collection.models.forEach(function(model){
-                this.addQueryRule(model)
-              }, this);
-            }
-            // Render a new Query Rule at the end
-            this.addQueryRule();
-
-            return this;
-
-          } catch (e) {
-            console.error("Failed to render a Query Builder view, error message: ", e);
+            }),
+          );
+
+          // Nested Query Builders are used to display nested filterGroup models.
+          // They need to be styled slightly different from the parent Query Builder.
+          if (this.parentRule) {
+            this.$el.addClass("nested");
           }
-        },
-        
-        /**
-         * Insert two inputs: one that allows the user to edit the "exclude" attribute in
-         * the FilterGroup model by selecting either "include" or "exclude"; and a second
-         * that allows the user to edit the "operator" attribute in the FilterGroup model
-         * by selecting between "all" and "any".
-         * @since 2.17.0
-         */
-        renderExcludeOperatorInputs: function(){
-
-          try {
-
-            if(!this.filterGroup){
-              console.log("A filterGroup model is required to edit the exclude and " +
-                "operator attributes in a Query Builder View.");
-              return
-            }
-
-            // Select the elements in the template where the two inputs should be inserted
-            var excludeContainer = this.$el.find(this.excludeInputSelector);
-            var operatorContainer = this.$el.find(this.operatorInputSelector);
-            // Create the exclude input
-            var excludeInput = new SearchableSelect({
-              options: [
-                { label: "Include",
-                  value: "false",
-                  description: "Include all datasets with metadata that matches the rules" +
-                    " that are set below."
-                },
-                { label: "Exclude",
-                  value: "true",
-                  description: "Match any dataset except those with metadata that match" +
-                    " the rules that are set below"
-                }
-              ],
-              allowMulti: false,
-              allowAdditions: false,
-              inputLabel: "",
-              selected: [this.filterGroup.get("exclude").toString()],
-              clearable: false,
-            });
-            // Create the operator input
-            var operatorInput = new SearchableSelect({
-              options: [
-                { label: "all",
-                  value: "AND",
-                  description: "For a dataset to match, it must have metadata that " +
-                    "matches every rule set below."
-                },
-                { label: "any",
-                  value: "OR",
-                  description: "For a dataset to match, its metadata only needs to " +
-                  "match one of the rules set below."
-                }
-              ],
-              allowMulti: false,
-              allowAdditions: false,
-              inputLabel: "",
-              selected: [this.filterGroup.get("operator")],
-              clearable: false,
-            })
-            // Update the FilterGroup model when the user changes the operator or exclude
-            // options. newValues will always be an Array, but since these inputs don't
-            // allow multiple selections (allowMulti: false), then there will only ever be
-            // one value.
-            this.stopListening(excludeInput)
-            this.listenTo(excludeInput, "changeSelection", function(newValues){
-              // Convert the string (necessary to be used as a value in SearchableSelect)
-              // to a boolean. It should be "true" or "false".
-              var newExclude = newValues[0] == "true";
-              this.filterGroup.set("exclude", newExclude);
-            });
-            this.stopListening(operatorInput)
-            this.listenTo(operatorInput, "changeSelection", function(newValues){
-              this.filterGroup.set("operator", newValues[0]);
-            });
-            // Render the inputs and insert them into the view. Replace the default text
-            // within the containers otherwise.
-            excludeContainer.html(excludeInput.render().el);
-            operatorContainer.html(operatorInput.render().el);
-          } catch (error) {
-            console.log("There was a problem rendering the exclude and operator " +
-            "inputs in a QueryBuilderView, error details: " + error);
+
+          // Remove the rule group button ID if no more nested Query Builders are
+          // allowed.
+          if (
+            typeof this.nestedLevelsAllowed == "number" &&
+            this.nestedLevelsAllowed < 1
+          ) {
+            this.$el.find("#" + this.addRuleGroupButtonID + this.cid).remove();
           }
-        },
-
-        /**
-         * Appends a new row (Query Rule View) to the end of the Query Builder
-         *
-         * @param {Filter|FilterGroup} filterModel The Filter model or FilterGroup model
-         * for which to create a rule. If none is provided, then a Filter group model
-         * will be created and added to the collection.
-         */
-        addQueryRule: function(filterModel){
-          try {
-
-            // Ensure that the object passed to this function is a filter. When the "add
-            // rule" button is clicked, the Event object is passed to this function
-            // instead. If no filter model is provided, assume that this is a new rule
-            if(!filterModel || (filterModel && !/filter/i.test(filterModel.type))){
-              filterModel = this.collection.add({
-                nodeName: "filter",
-                operator: "OR",
-                fieldsOperator: "OR"
-              });
-            }
-
-            // Don't show invisible rules
-            if(filterModel.get("isInvisible")){
-              return
-            }
-
-            // insert QueryRuleView
-            var rule = new QueryRule({
-              model: filterModel,
-              ruleColorPalette: this.ruleColorPalette,
-              excludeFields: this.excludeFields,
-              nestedExcludeFields: this.nestedExcludeFields,
-              specialFields: this.specialFields,
-              parentRule: this.parentRule,
-              nestedLevelsAllowed: this.nestedLevelsAllowed,
-            });
 
-            // Insert and render the rule
-            this.rulesContainer.append(rule.el);
-            rule.render();
-            // Add the rule to the list of rule sub-views
-            // TODO: is this really needed? are they removed when rule removed?
-            this.rules.push(rule);
+          // Save the rules container element to the view before we add any nested
+          // QueryBuilders (nested FilterGroups), since their rules container uses the
+          // same selector.
+          this.rulesContainer = this.$el.find(this.rulesContainerSelector);
+
+          // If there is a FilterGroup model set on this view (not just a Filters
+          // collection) then render the inputs that allow a user to edit the "exclude"
+          // and "operator" attributes
+          if (this.filterGroup) {
+            this.renderExcludeOperatorInputs();
+          }
 
-          } catch (e) {
-            console.error("Error adding a Query Rule, error message:", e);
+          // Add a row for each rule that exists already in the model
+          if (
+            this.collection &&
+            this.collection.models &&
+            this.collection.models.length
+          ) {
+            this.collection.models.forEach(function (model) {
+              this.addQueryRule(model);
+            }, this);
           }
-        },
-
-        /**
-         * Exactly the same as {@link QueryBuilderView#addQueryRule}, except that if no
-         * model is provided to this function, then a FilterGroup model will be created
-         * instead of a Filter model.
-         * @param  {FilterGroup} filterGroupModel
-         */
-        addQueryRuleGroup: function(filterGroupModel){
-          try {
-            // Ensure that the object passed to this function is a filter. When the "add
-            // rule" button is clicked, the Event object is passed to this function
-            // instead. If no filter model is provided, assume that this is a new rule
-            if(!filterGroupModel || (filterGroupModel && filterGroupModel.type != "FilterGroup")){
-              filterGroupModel = this.collection.add({
-                filterType: "FilterGroup",
-              });
-            };
-            this.addQueryRule(filterGroupModel)
-          } catch (error) {
-            console.log("Error adding a Query Rule Group in a Query Builder View. " +
-            "Error details: " + error);
+          // Render a new Query Rule at the end
+          this.addQueryRule();
+
+          return this;
+        } catch (e) {
+          console.error(
+            "Failed to render a Query Builder view, error message: ",
+            e,
+          );
+        }
+      },
+
+      /**
+       * Insert two inputs: one that allows the user to edit the "exclude" attribute in
+       * the FilterGroup model by selecting either "include" or "exclude"; and a second
+       * that allows the user to edit the "operator" attribute in the FilterGroup model
+       * by selecting between "all" and "any".
+       * @since 2.17.0
+       */
+      renderExcludeOperatorInputs: function () {
+        try {
+          if (!this.filterGroup) {
+            console.log(
+              "A filterGroup model is required to edit the exclude and " +
+                "operator attributes in a Query Builder View.",
+            );
+            return;
           }
-        },
 
-      });
-      return QueryBuilderView;
-  });
+          // Select the elements in the template where the two inputs should be inserted
+          var excludeContainer = this.$el.find(this.excludeInputSelector);
+          var operatorContainer = this.$el.find(this.operatorInputSelector);
+          // Create the exclude input
+          var excludeInput = new SearchableSelect({
+            options: [
+              {
+                label: "Include",
+                value: "false",
+                description:
+                  "Include all datasets with metadata that matches the rules" +
+                  " that are set below.",
+              },
+              {
+                label: "Exclude",
+                value: "true",
+                description:
+                  "Match any dataset except those with metadata that match" +
+                  " the rules that are set below",
+              },
+            ],
+            allowMulti: false,
+            allowAdditions: false,
+            inputLabel: "",
+            selected: [this.filterGroup.get("exclude").toString()],
+            clearable: false,
+          });
+          // Create the operator input
+          var operatorInput = new SearchableSelect({
+            options: [
+              {
+                label: "all",
+                value: "AND",
+                description:
+                  "For a dataset to match, it must have metadata that " +
+                  "matches every rule set below.",
+              },
+              {
+                label: "any",
+                value: "OR",
+                description:
+                  "For a dataset to match, its metadata only needs to " +
+                  "match one of the rules set below.",
+              },
+            ],
+            allowMulti: false,
+            allowAdditions: false,
+            inputLabel: "",
+            selected: [this.filterGroup.get("operator")],
+            clearable: false,
+          });
+          // Update the FilterGroup model when the user changes the operator or exclude
+          // options. newValues will always be an Array, but since these inputs don't
+          // allow multiple selections (allowMulti: false), then there will only ever be
+          // one value.
+          this.stopListening(excludeInput);
+          this.listenTo(excludeInput, "changeSelection", function (newValues) {
+            // Convert the string (necessary to be used as a value in SearchableSelect)
+            // to a boolean. It should be "true" or "false".
+            var newExclude = newValues[0] == "true";
+            this.filterGroup.set("exclude", newExclude);
+          });
+          this.stopListening(operatorInput);
+          this.listenTo(operatorInput, "changeSelection", function (newValues) {
+            this.filterGroup.set("operator", newValues[0]);
+          });
+          // Render the inputs and insert them into the view. Replace the default text
+          // within the containers otherwise.
+          excludeContainer.html(excludeInput.render().el);
+          operatorContainer.html(operatorInput.render().el);
+        } catch (error) {
+          console.log(
+            "There was a problem rendering the exclude and operator " +
+              "inputs in a QueryBuilderView, error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Appends a new row (Query Rule View) to the end of the Query Builder
+       *
+       * @param {Filter|FilterGroup} filterModel The Filter model or FilterGroup model
+       * for which to create a rule. If none is provided, then a Filter group model
+       * will be created and added to the collection.
+       */
+      addQueryRule: function (filterModel) {
+        try {
+          // Ensure that the object passed to this function is a filter. When the "add
+          // rule" button is clicked, the Event object is passed to this function
+          // instead. If no filter model is provided, assume that this is a new rule
+          if (
+            !filterModel ||
+            (filterModel && !/filter/i.test(filterModel.type))
+          ) {
+            filterModel = this.collection.add({
+              nodeName: "filter",
+              operator: "OR",
+              fieldsOperator: "OR",
+            });
+          }
+
+          // Don't show invisible rules
+          if (filterModel.get("isInvisible")) {
+            return;
+          }
+
+          // insert QueryRuleView
+          var rule = new QueryRule({
+            model: filterModel,
+            ruleColorPalette: this.ruleColorPalette,
+            excludeFields: this.excludeFields,
+            nestedExcludeFields: this.nestedExcludeFields,
+            specialFields: this.specialFields,
+            parentRule: this.parentRule,
+            nestedLevelsAllowed: this.nestedLevelsAllowed,
+          });
+
+          // Insert and render the rule
+          this.rulesContainer.append(rule.el);
+          rule.render();
+          // Add the rule to the list of rule sub-views
+          // TODO: is this really needed? are they removed when rule removed?
+          this.rules.push(rule);
+        } catch (e) {
+          console.error("Error adding a Query Rule, error message:", e);
+        }
+      },
+
+      /**
+       * Exactly the same as {@link QueryBuilderView#addQueryRule}, except that if no
+       * model is provided to this function, then a FilterGroup model will be created
+       * instead of a Filter model.
+       * @param  {FilterGroup} filterGroupModel
+       */
+      addQueryRuleGroup: function (filterGroupModel) {
+        try {
+          // Ensure that the object passed to this function is a filter. When the "add
+          // rule" button is clicked, the Event object is passed to this function
+          // instead. If no filter model is provided, assume that this is a new rule
+          if (
+            !filterGroupModel ||
+            (filterGroupModel && filterGroupModel.type != "FilterGroup")
+          ) {
+            filterGroupModel = this.collection.add({
+              filterType: "FilterGroup",
+            });
+          }
+          this.addQueryRule(filterGroupModel);
+        } catch (error) {
+          console.log(
+            "Error adding a Query Rule Group in a Query Builder View. " +
+              "Error details: " +
+              error,
+          );
+        }
+      },
+    },
+  );
+  return QueryBuilderView;
+});
 

diff --git a/docs/docs/src_js_views_queryBuilder_QueryRuleView.js.html b/docs/docs/src_js_views_queryBuilder_QueryRuleView.js.html index 176597a65..4969f9a1c 100644 --- a/docs/docs/src_js_views_queryBuilder_QueryRuleView.js.html +++ b/docs/docs/src_js_views_queryBuilder_QueryRuleView.js.html @@ -59,1703 +59,1806 @@

Source: src/js/views/queryBuilder/QueryRuleView.js

"models/filters/Filter", "models/filters/BooleanFilter", "models/filters/NumericFilter", - "models/filters/DateFilter" -], - function ( - $, _, Backbone, SearchableSelect, QueryFieldSelect, NodeSelect, AccountSelect, - NumericFilterView, DateFilterView, ObjectFormatSelect, AnnotationFilter, - Filter, BooleanFilter, NumericFilter, DateFilter - ) { - - /** - * @class QueryRuleView - * @classdesc A view that provides an UI for a user to construct a single filter that - * is part of a complex query - * @classcategory Views/QueryBuilder - * @screenshot views/QueryRuleView.png - * @extends Backbone.View - * @constructor - * @since 2.14.0 - */ - return Backbone.View.extend( - /** @lends QueryRuleView.prototype */ - { - /** - * The type of View this is - * @type {string} - */ - type: "QueryRule", - - /** - * The HTML class names for this view element - * @type {string} - */ - className: "query-rule", - - /** - * The class to add to the rule number and other information on the left - * @type {string} - */ - ruleInfoClass: "rule-info", - - /** - * The class to add to the field select element - * @type {string} - */ - fieldsClass: "field", - - /** - * The class to add to the operator select element - * @type {string} - */ - operatorClass: "operator", - - /** - * The class to add to the value select element - * @type {string} - */ - valuesClass: "value", - - /** - * The class to add to the element that a user should click to remove a rule. - * @type {string} - */ - removeClass: "remove-rule", - - /** - * An ID for the element that a user should click to remove a rule. A unique ID - * will be appended to this ID, and the ID will be added to the template. - * @type {string} - */ - removeRuleID: "remove-rule-", - - /** - * The maximum number of levels of nested Rule Groups (i.e. nested FilterGroup - * models) that a user is permitted to build in the Query Builder that contains - * this rule. This value should be passed to the rule by the parent Query Builder. - * This value minus one will be passed on to any child Query Builders (those that - * render nested FilterGroup models). - * @type {number} - * @since 2.17.0 - */ - nestedLevelsAllowed: 1, - - /** - * An array of hex color codes used to help distinguish between different rules. - * If this is a nested Query Rule, and the rule should inherit its colour from - * the parent Query Rule, then set ruleColorPalette to "inherit". - * @type {string[]|string} - */ - ruleColorPalette: ["#44AA99", "#137733", "#c9a538", "#CC6677", "#882355", - "#AA4499", "#332288"], - - /** - * Search index fields to exclude in the metadata field selector - * @type {string[]} - */ - excludeFields: [], - - /** - * Query fields to exclude in the metadata field selector for any Query Rules that - * are in nested Query Builders (i.e. in nested Filter Groups). This is a list of - * field names that exist in the query service index (i.e. Solr), but which should - * be hidden in nested Query Builders - * @type {string[]} - */ - nestedExcludeFields: [], - - /** - * A single Filter model that is part of a Filters collection, such as the - * definition filters for a Collection or Portal or the filters for a Search - * model. The Filter model must be part of a Filters collection (i.e. there must - * be a model.collection property) - * @type {Filter|BooleanFilter|NumericFilter|DateFilter|FilterGroup} - */ - model: undefined, - - /** - * A function that creates and returns the Backbone events object. - * @return {Object} Returns a Backbone events object - */ - events: function () { - var events = {}; - var removeID = "#" + this.removeRuleID + this.cid; - events["click " + removeID] = "removeSelf"; - events["mouseover " + removeID] = "previewRemove"; - events["mouseout " + removeID] = "previewRemove"; - return events + "models/filters/DateFilter", +], function ( + $, + _, + Backbone, + SearchableSelect, + QueryFieldSelect, + NodeSelect, + AccountSelect, + NumericFilterView, + DateFilterView, + ObjectFormatSelect, + AnnotationFilter, + Filter, + BooleanFilter, + NumericFilter, + DateFilter, +) { + /** + * @class QueryRuleView + * @classdesc A view that provides an UI for a user to construct a single filter that + * is part of a complex query + * @classcategory Views/QueryBuilder + * @screenshot views/QueryRuleView.png + * @extends Backbone.View + * @constructor + * @since 2.14.0 + */ + return Backbone.View.extend( + /** @lends QueryRuleView.prototype */ + { + /** + * The type of View this is + * @type {string} + */ + type: "QueryRule", + + /** + * The HTML class names for this view element + * @type {string} + */ + className: "query-rule", + + /** + * The class to add to the rule number and other information on the left + * @type {string} + */ + ruleInfoClass: "rule-info", + + /** + * The class to add to the field select element + * @type {string} + */ + fieldsClass: "field", + + /** + * The class to add to the operator select element + * @type {string} + */ + operatorClass: "operator", + + /** + * The class to add to the value select element + * @type {string} + */ + valuesClass: "value", + + /** + * The class to add to the element that a user should click to remove a rule. + * @type {string} + */ + removeClass: "remove-rule", + + /** + * An ID for the element that a user should click to remove a rule. A unique ID + * will be appended to this ID, and the ID will be added to the template. + * @type {string} + */ + removeRuleID: "remove-rule-", + + /** + * The maximum number of levels of nested Rule Groups (i.e. nested FilterGroup + * models) that a user is permitted to build in the Query Builder that contains + * this rule. This value should be passed to the rule by the parent Query Builder. + * This value minus one will be passed on to any child Query Builders (those that + * render nested FilterGroup models). + * @type {number} + * @since 2.17.0 + */ + nestedLevelsAllowed: 1, + + /** + * An array of hex color codes used to help distinguish between different rules. + * If this is a nested Query Rule, and the rule should inherit its colour from + * the parent Query Rule, then set ruleColorPalette to "inherit". + * @type {string[]|string} + */ + ruleColorPalette: [ + "#44AA99", + "#137733", + "#c9a538", + "#CC6677", + "#882355", + "#AA4499", + "#332288", + ], + + /** + * Search index fields to exclude in the metadata field selector + * @type {string[]} + */ + excludeFields: [], + + /** + * Query fields to exclude in the metadata field selector for any Query Rules that + * are in nested Query Builders (i.e. in nested Filter Groups). This is a list of + * field names that exist in the query service index (i.e. Solr), but which should + * be hidden in nested Query Builders + * @type {string[]} + */ + nestedExcludeFields: [], + + /** + * A single Filter model that is part of a Filters collection, such as the + * definition filters for a Collection or Portal or the filters for a Search + * model. The Filter model must be part of a Filters collection (i.e. there must + * be a model.collection property) + * @type {Filter|BooleanFilter|NumericFilter|DateFilter|FilterGroup} + */ + model: undefined, + + /** + * A function that creates and returns the Backbone events object. + * @return {Object} Returns a Backbone events object + */ + events: function () { + var events = {}; + var removeID = "#" + this.removeRuleID + this.cid; + events["click " + removeID] = "removeSelf"; + events["mouseover " + removeID] = "previewRemove"; + events["mouseout " + removeID] = "previewRemove"; + return events; + }, + + /** + * A list of additional fields which are not retrieved from the query API, but + * which should be added to the list of options. This can be used to add + * abstracted fields which are a combination of multiple query fields, or to add a + * duplicate field that has a different label. These special fields are passed on + * to {@link QueryFieldSelectView#addFields}. + * + * @type {SpecialField[]} + * + * @since 2.15.0 + */ + specialFields: [ + { + name: "documents-special-field", + fields: ["documents"], + label: "Contains Data Files", + description: + "Limit results to packages that include data files. Without" + + " this rule, results may include packages with metadata but no data.", + category: "General", + values: ["*"], }, - - /** - * A list of additional fields which are not retrieved from the query API, but - * which should be added to the list of options. This can be used to add - * abstracted fields which are a combination of multiple query fields, or to add a - * duplicate field that has a different label. These special fields are passed on - * to {@link QueryFieldSelectView#addFields}. - * - * @type {SpecialField[]} - * - * @since 2.15.0 - */ - specialFields: [ - { - name: "documents-special-field", - fields: ["documents"], - label: "Contains Data Files", - description: "Limit results to packages that include data files. Without" + - " this rule, results may include packages with metadata but no data.", - category: "General", - values: ["*"] - }, - { - name: "year-data-collection", - fields: ["beginDate", "endDate"], - label: "Year of Data Collection", - description: "The temporal range of content described by the metadata", - category: "Dates" - } - ], - - /** - * An operator option is an object that lists the properties of one of the - * operators that will be displayed to the user in the Query Rule "operator" - * dropdown list. The operator properties are used to pre-select the correct - * operator based on attributes in the associated - * {@link Filter#defaults Filter model}, as well as to update the Filter model - * when a user selects a new operator. Operators can set the exclude and - * matchSubstring properties of the model, and sometimes the values as well. - * Either the types property OR the fields property must be set, not both. - * - * @typedef {Object} OperatorOption - * @property {string} label - The label to display to the user - * @property {string} icon - An icon that represents the operator - * @property {boolean} matchSubstring - Whether the matchSubstring attribute is - * true or false in the filter model that matches this operator - * @property {boolean} exclude - Whether the exclude attribute is true or false in - * the filter model that matches this operator - * @property {boolean} hasMax - Whether the filter model that matches this - * operator must have a max attribute - * @property {boolean} hasMin - Whether the filter model that matches this - * operator must have a min attribute - * @property {string[]} values - For this operator to work as desired, the values - * that should be set in the filter (e.g. ["true"] for the operator "is true") - * @property {string[]} [types] - The node names of the filters that this operator - * is used for (e.g. "filter", "booleanFilter") - * @property {string[]} [fields] - The query field names of the filters that this - * operator is used for. If this is used for a - * {@link QueryRuleView#specialFields special field}, then list the special field - * name (id), and not the real query field names. If this fields property is set, - * then the types property will be ignored. (i.e. fields is more specific than - * types.) - */ - - /** - * The list of operators that will be available in the dropdown list that connects - * the query fields to the values. Each operator must be unique. - * - * @type {OperatorOption[]} - */ - operatorOptions: [ - { - label: "is true", - description: "The data package includes data files (and not only metadata)", - icon: "ok-circle", - matchSubstring: false, - exclude: false, - values: ["*"], - fields: ["documents-special-field"] - }, - { - label: "is false", - description: "The data package only contains metadata; it contains no data files.", - icon: "ban-circle", - matchSubstring: false, - exclude: true, - values: ["*"], - fields: ["documents-special-field"] - }, - { - label: "equals", - description: "The text in the metadata field is an exact match to the" + - " selected value", - icon: "equal", - matchSubstring: false, - exclude: false, - types: ["filter"] - }, - { - label: "does not equal", - description: "The text in the metadata field is anything except an exact" + - " match to the selected value", - icon: "not-equal", - matchSubstring: false, - exclude: true, - types: ["filter"] - }, - { - label: "contains", - description: "The text in the metadata field matches or contains the words" + - " or phrase selected", - icon: "ok-circle", - matchSubstring: true, - exclude: false, - types: ["filter"] - }, - { - label: "does not contain", - description: "The words or phrase selected are not contained within the" + - " metadata field", - icon: "ban-circle", - matchSubstring: true, - exclude: true, - types: ["filter"] - }, - { - label: "is empty", - description: "The metadata field contains no text or value", - icon: "circle-blank", - matchSubstring: false, - exclude: true, - values: ["*"], - types: ["filter"] - }, - { - label: "is not empty", - description: "The metadata field is filled in with any text at all", - icon: "circle", - matchSubstring: false, - exclude: false, - values: ["*"], - types: ["filter"] - }, - { - label: "is true", - description: "The metadata field is set to true", - icon: "ok-circle", - matchSubstring: false, - exclude: false, - values: [true], - types: ["booleanFilter"] - }, - { - label: "is false", - description: "The metadata field is set to false", - icon: "ban-circle", - matchSubstring: false, - exclude: false, - values: [false], - types: ["booleanFilter"] - }, - { - label: "is between", - description: "The metadata field is a value between the range selected" + - " (inclusive of both values)", - icon: "resize-horizontal", - matchSubstring: false, - exclude: false, - hasMin: true, - hasMax: true, - types: ["numericFilter", "dateFilter"] - }, - { - label: "is less than or equal to", - description: "The metadata field is a number less than the value selected", - icon: "less-than-or-eq", - matchSubstring: false, - exclude: false, - hasMin: false, - hasMax: true, - types: ["numericFilter"] - }, - { - label: "is greater than or equal to", - description: "The metadata field is a number greater than the value selected", - icon: "greater-than-or-eq", - matchSubstring: false, - exclude: false, - hasMin: true, - hasMax: false, - types: ["numericFilter"] - }, - { - label: "is exactly", - description: "The metadata field exactly equals the value selected", - icon: "equal", - matchSubstring: false, - exclude: false, - hasMin: false, - hasMax: false, - types: ["numericFilter"] - }, - // TODO: The dateFilter model & view need to be updated for these to work: - // { - // label: "is during or before", icon: "less-than-or-eq", matchSubstring: - // false, exclude: false, hasMin: false, hasMax: true, types: ["dateFilter"] - // }, - // { - // label: "is during or after", icon: "greater-than", matchSubstring: false, - // exclude: false, hasMin: true, hasMax: false, types: ["dateFilter"] - // }, - // { - // label: "is in the year", icon: "equal", matchSubstring: false, exclude: - // false, hasMin: false, hasMax: false, types: ["dateFilter"] - // } - ], - - - /** - * The third input in each Query Rule is where the user enters a value, minimum, - * or maximum for the filter model. Different types of values are appropriate for - * different solr query fields, and so we display different interfaces depending - * on the type and category of the selected query fields. A Value Input Option - * object defines a of interface to show for a given type and category. - * - * @typedef {Object} ValueInputOption - * @property {string[]} filterTypes - An array of one or more filter types that - * are allowed for this interface. If none are provided then any filter type is - * allowed. Filter types are one of the four keys defined in - * {@link QueryField#filterTypesMap}. - * @property {string[]} categories - An array of one or more categories that are - * allowed for this interface. These strings must exactly match the categories - * provided in QueryField.categoriesMap(). If none are provided then any category - * is allowed. - * @property {string[]} queryFields - Specific names of fields that are allowed in - * this interface. If none are provided, then any query fields are allowed that - * match the other properties. If this value select should be used for a - * {@link QueryRuleView#specialFields special field}, then use the name (id) of - * the special field, not the actual query fields that it represents. - * @property {string} label - If the interface does not include a label (e.g. - * number filter), include a string to display here. - * @property {function} uiFunction - A function that returns the UI view to use - * with all appropriate options set. The function will be called with this view as - * the context. - */ - - /** - * This list defines which type of value input to show depending on filter type, - * category, and query fields. The value input options are ordered from *most* - * specific to *least*, since the first match will be selected. The filter model - * must match either the queryFields, or both the filterTypes AND the categories - * for a UI to be selected. - * @type {ValueInputOption[]} - */ - valueSelectUImap: [ - // serviceCoupling field - { - queryFields: ["serviceCoupling"], - uiFunction: function () { - return new SearchableSelect({ - options: [ - { - label: "tight", - description: "Tight coupled service work only on the data described" + - " by this metadata document." - }, - { - label: "mixed", - description: "Mixed coupling means service works on data described" + - " by this metadata document but may work on other data." - }, - { - label: "loose", - description: "Loose coupling means service works on any data." - } - ], - allowMulti: true, - allowAdditions: false, - inputLabel: "Select a coupling", - selected: this.model.get("values"), - separatorText: this.model.get("operator") - }) - } + { + name: "year-data-collection", + fields: ["beginDate", "endDate"], + label: "Year of Data Collection", + description: + "The temporal range of content described by the metadata", + category: "Dates", + }, + ], + + /** + * An operator option is an object that lists the properties of one of the + * operators that will be displayed to the user in the Query Rule "operator" + * dropdown list. The operator properties are used to pre-select the correct + * operator based on attributes in the associated + * {@link Filter#defaults Filter model}, as well as to update the Filter model + * when a user selects a new operator. Operators can set the exclude and + * matchSubstring properties of the model, and sometimes the values as well. + * Either the types property OR the fields property must be set, not both. + * + * @typedef {Object} OperatorOption + * @property {string} label - The label to display to the user + * @property {string} icon - An icon that represents the operator + * @property {boolean} matchSubstring - Whether the matchSubstring attribute is + * true or false in the filter model that matches this operator + * @property {boolean} exclude - Whether the exclude attribute is true or false in + * the filter model that matches this operator + * @property {boolean} hasMax - Whether the filter model that matches this + * operator must have a max attribute + * @property {boolean} hasMin - Whether the filter model that matches this + * operator must have a min attribute + * @property {string[]} values - For this operator to work as desired, the values + * that should be set in the filter (e.g. ["true"] for the operator "is true") + * @property {string[]} [types] - The node names of the filters that this operator + * is used for (e.g. "filter", "booleanFilter") + * @property {string[]} [fields] - The query field names of the filters that this + * operator is used for. If this is used for a + * {@link QueryRuleView#specialFields special field}, then list the special field + * name (id), and not the real query field names. If this fields property is set, + * then the types property will be ignored. (i.e. fields is more specific than + * types.) + */ + + /** + * The list of operators that will be available in the dropdown list that connects + * the query fields to the values. Each operator must be unique. + * + * @type {OperatorOption[]} + */ + operatorOptions: [ + { + label: "is true", + description: + "The data package includes data files (and not only metadata)", + icon: "ok-circle", + matchSubstring: false, + exclude: false, + values: ["*"], + fields: ["documents-special-field"], + }, + { + label: "is false", + description: + "The data package only contains metadata; it contains no data files.", + icon: "ban-circle", + matchSubstring: false, + exclude: true, + values: ["*"], + fields: ["documents-special-field"], + }, + { + label: "equals", + description: + "The text in the metadata field is an exact match to the" + + " selected value", + icon: "equal", + matchSubstring: false, + exclude: false, + types: ["filter"], + }, + { + label: "does not equal", + description: + "The text in the metadata field is anything except an exact" + + " match to the selected value", + icon: "not-equal", + matchSubstring: false, + exclude: true, + types: ["filter"], + }, + { + label: "contains", + description: + "The text in the metadata field matches or contains the words" + + " or phrase selected", + icon: "ok-circle", + matchSubstring: true, + exclude: false, + types: ["filter"], + }, + { + label: "does not contain", + description: + "The words or phrase selected are not contained within the" + + " metadata field", + icon: "ban-circle", + matchSubstring: true, + exclude: true, + types: ["filter"], + }, + { + label: "is empty", + description: "The metadata field contains no text or value", + icon: "circle-blank", + matchSubstring: false, + exclude: true, + values: ["*"], + types: ["filter"], + }, + { + label: "is not empty", + description: "The metadata field is filled in with any text at all", + icon: "circle", + matchSubstring: false, + exclude: false, + values: ["*"], + types: ["filter"], + }, + { + label: "is true", + description: "The metadata field is set to true", + icon: "ok-circle", + matchSubstring: false, + exclude: false, + values: [true], + types: ["booleanFilter"], + }, + { + label: "is false", + description: "The metadata field is set to false", + icon: "ban-circle", + matchSubstring: false, + exclude: false, + values: [false], + types: ["booleanFilter"], + }, + { + label: "is between", + description: + "The metadata field is a value between the range selected" + + " (inclusive of both values)", + icon: "resize-horizontal", + matchSubstring: false, + exclude: false, + hasMin: true, + hasMax: true, + types: ["numericFilter", "dateFilter"], + }, + { + label: "is less than or equal to", + description: + "The metadata field is a number less than the value selected", + icon: "less-than-or-eq", + matchSubstring: false, + exclude: false, + hasMin: false, + hasMax: true, + types: ["numericFilter"], + }, + { + label: "is greater than or equal to", + description: + "The metadata field is a number greater than the value selected", + icon: "greater-than-or-eq", + matchSubstring: false, + exclude: false, + hasMin: true, + hasMax: false, + types: ["numericFilter"], + }, + { + label: "is exactly", + description: "The metadata field exactly equals the value selected", + icon: "equal", + matchSubstring: false, + exclude: false, + hasMin: false, + hasMax: false, + types: ["numericFilter"], + }, + // TODO: The dateFilter model & view need to be updated for these to work: + // { + // label: "is during or before", icon: "less-than-or-eq", matchSubstring: + // false, exclude: false, hasMin: false, hasMax: true, types: ["dateFilter"] + // }, + // { + // label: "is during or after", icon: "greater-than", matchSubstring: false, + // exclude: false, hasMin: true, hasMax: false, types: ["dateFilter"] + // }, + // { + // label: "is in the year", icon: "equal", matchSubstring: false, exclude: + // false, hasMin: false, hasMax: false, types: ["dateFilter"] + // } + ], + + /** + * The third input in each Query Rule is where the user enters a value, minimum, + * or maximum for the filter model. Different types of values are appropriate for + * different solr query fields, and so we display different interfaces depending + * on the type and category of the selected query fields. A Value Input Option + * object defines a of interface to show for a given type and category. + * + * @typedef {Object} ValueInputOption + * @property {string[]} filterTypes - An array of one or more filter types that + * are allowed for this interface. If none are provided then any filter type is + * allowed. Filter types are one of the four keys defined in + * {@link QueryField#filterTypesMap}. + * @property {string[]} categories - An array of one or more categories that are + * allowed for this interface. These strings must exactly match the categories + * provided in QueryField.categoriesMap(). If none are provided then any category + * is allowed. + * @property {string[]} queryFields - Specific names of fields that are allowed in + * this interface. If none are provided, then any query fields are allowed that + * match the other properties. If this value select should be used for a + * {@link QueryRuleView#specialFields special field}, then use the name (id) of + * the special field, not the actual query fields that it represents. + * @property {string} label - If the interface does not include a label (e.g. + * number filter), include a string to display here. + * @property {function} uiFunction - A function that returns the UI view to use + * with all appropriate options set. The function will be called with this view as + * the context. + */ + + /** + * This list defines which type of value input to show depending on filter type, + * category, and query fields. The value input options are ordered from *most* + * specific to *least*, since the first match will be selected. The filter model + * must match either the queryFields, or both the filterTypes AND the categories + * for a UI to be selected. + * @type {ValueInputOption[]} + */ + valueSelectUImap: [ + // serviceCoupling field + { + queryFields: ["serviceCoupling"], + uiFunction: function () { + return new SearchableSelect({ + options: [ + { + label: "tight", + description: + "Tight coupled service work only on the data described" + + " by this metadata document.", + }, + { + label: "mixed", + description: + "Mixed coupling means service works on data described" + + " by this metadata document but may work on other data.", + }, + { + label: "loose", + description: + "Loose coupling means service works on any data.", + }, + ], + allowMulti: true, + allowAdditions: false, + inputLabel: "Select a coupling", + selected: this.model.get("values"), + separatorText: this.model.get("operator"), + }); }, - // Metadata format IDs - { - queryFields: ["formatId"], - uiFunction: function () { - return new ObjectFormatSelect({ - selected: this.model.get("values"), - separatorText: this.model.get("operator") - }) - } + }, + // Metadata format IDs + { + queryFields: ["formatId"], + uiFunction: function () { + return new ObjectFormatSelect({ + selected: this.model.get("values"), + separatorText: this.model.get("operator"), + }); }, - // Semantic annotation picker - { - queryFields: ["sem_annotation"], - uiFunction: function () { - // A bioportalAPIKey is required for the Annotation Filter UI - if (MetacatUI.appModel.get("bioportalAPIKey")) { - return new AnnotationFilter({ - selected: this.model.get("values").slice(), - separatorText: this.model.get("operator"), - multiselect: true, - inputLabel: "Type a value", - }); - // If there's no API key, render the default UI (the last in this list) - } else { - return this.valueSelectUImap.slice(-1)[0].uiFunction.call(this); - } + }, + // Semantic annotation picker + { + queryFields: ["sem_annotation"], + uiFunction: function () { + // A bioportalAPIKey is required for the Annotation Filter UI + if (MetacatUI.appModel.get("bioportalAPIKey")) { + return new AnnotationFilter({ + selected: this.model.get("values").slice(), + separatorText: this.model.get("operator"), + multiselect: true, + inputLabel: "Type a value", + }); + // If there's no API key, render the default UI (the last in this list) + } else { + return this.valueSelectUImap.slice(-1)[0].uiFunction.call(this); } }, - // User/Organization account ID lookup - { - queryFields: ["writePermission", "readPermission", "changePermission", "rightsHolder", "submitter"], - uiFunction: function () { - return new AccountSelect({ - selected: this.model.get("values"), - separatorText: this.model.get("operator") - }); - }, + }, + // User/Organization account ID lookup + { + queryFields: [ + "writePermission", + "readPermission", + "changePermission", + "rightsHolder", + "submitter", + ], + uiFunction: function () { + return new AccountSelect({ + selected: this.model.get("values"), + separatorText: this.model.get("operator"), + }); }, - // Repository picker for fields that need a member node ID - { - filterTypes: ["filter"], - queryFields: ["blockedReplicationMN", "preferredReplicationMN", "replicaMN", - "authoritativeMN", "datasource"], - uiFunction: function () { - return new NodeSelect({ - selected: this.model.get("values"), - separatorText: this.model.get("operator") - }) - } + }, + // Repository picker for fields that need a member node ID + { + filterTypes: ["filter"], + queryFields: [ + "blockedReplicationMN", + "preferredReplicationMN", + "replicaMN", + "authoritativeMN", + "datasource", + ], + uiFunction: function () { + return new NodeSelect({ + selected: this.model.get("values"), + separatorText: this.model.get("operator"), + }); }, - // Any numeric fields don't fit one of the above options - { - filterTypes: ["numericFilter"], - label: "Choose a value", - uiFunction: function () { - return new NumericFilterView({ - model: this.model, - showButton: false, - separatorText: this.model.get("operator") - }) - } + }, + // Any numeric fields don't fit one of the above options + { + filterTypes: ["numericFilter"], + label: "Choose a value", + uiFunction: function () { + return new NumericFilterView({ + model: this.model, + showButton: false, + separatorText: this.model.get("operator"), + }); }, - // Any date fields that don't fit one of the above options - { - filterTypes: ["dateFilter"], - label: "Choose a year", - uiFunction: function () { - return new DateFilterView({ - model: this.model, - separatorText: this.model.get("operator") - }) - } + }, + // Any date fields that don't fit one of the above options + { + filterTypes: ["dateFilter"], + label: "Choose a year", + uiFunction: function () { + return new DateFilterView({ + model: this.model, + separatorText: this.model.get("operator"), + }); }, - // The last is the default value selection UI - { - uiFunction: function () { - return new SearchableSelect({ - options: [], - allowMulti: true, - allowAdditions: true, - inputLabel: "Type a value", - selected: this.model.get("values"), - separatorText: this.model.get("operator") - }) - } - } - ], - - /** - * Creates a new QueryRuleView - * @param {Object} options - A literal object with options to pass to the view - */ - initialize: function (options) { - try { - - // Get all the options and apply them to this view - if (typeof options == "object") { - var optionKeys = Object.keys(options); - _.each(optionKeys, function (key, i) { - this[key] = options[key]; - }, this); - } - - // If no model is provided in the options, we cannot render this view. A - // filter model cannot be created, because it must be part of a collection. - if (!this.model || !this.model.collection) { - console.error("error: A Filter model that's part of a Filters collection" - + " is required to initialize a Query Rule view.") - return - } - - // The model may be removed during the save process if it's empty. Remove this - // Rule Group view when that happens. - this.stopListening(this.model, "remove"); - this.listenTo(this.model, "remove", function () { - this.removeSelf(); + }, + // The last is the default value selection UI + { + uiFunction: function () { + return new SearchableSelect({ + options: [], + allowMulti: true, + allowAdditions: true, + inputLabel: "Type a value", + selected: this.model.get("values"), + separatorText: this.model.get("operator"), }); - - } catch (e) { - console.log("Failed to initialize a Query Builder View, error message:", e); - } + }, }, - - /** - * render - Render the view - * - * @return {QueryRule} Returns the view - */ - render: function () { - - try { - - // Add the Rule number. - // TODO: Also add the number of datasets related to rule - this.addRuleInfo(); - this.stopListening(this.model.collection, "remove"); - this.listenTo(this.model.collection, "remove", this.updateRuleInfo); - // Nested rules should also listen for changes in Filters of their parent Rule - if(this.parentRule){ - this.stopListening(this.parentRule.model.collection, "remove"); - this.listenTo(this.parentRule.model.collection, "remove", this.updateRuleInfo); - } - - // The remove button is needed for both FilterGroups and other Filter models - this.addRemoveButton(); - - // Render nested filter group views as another Query Builder. - if(this.model.type == "FilterGroup"){ - - this.$el.addClass("rule-group"); - - // We must initialize a QueryBuilderView using the inline require syntax to - // avoid the problem of circular dependencies. QueryRuleView requires - // QueryBuilderView, and QueryBuilderView requires QueryRuleView. For more - // info, see https://requirejs.org/docs/api.html#circular - var QueryBuilderView = require('views/queryBuilder/QueryBuilderView'); - - // The default - nestedLevelsAllowed = 1 - // If we are adding a query builer, then it is a nested level. Subtract one - // from the total levels allowed. - if(typeof this.nestedLevelsAllowed == "number"){ - nestedLevelsAllowed = this.nestedLevelsAllowed - 1 - } - - // If there is a special list of fields to exclude in nested Query Builders - // (i.e. in nested FilterGroup models), then pass this list on as the - // excludeFields list in the child QueryBuilder - var excludeFields = this.excludeFields; - if(this.nestedExcludeFields && Array.isArray(this.nestedExcludeFields)){ - excludeFields = this.nestedExcludeFields - } - - // Insert QueryRuleView - var ruleGroup = new QueryBuilderView({ - filterGroup: this.model, - // Nested Query Rules have the same color as their parent rule - ruleColorPalette: "inherit", - excludeFields: excludeFields, - specialFields: this.specialFields, - parentRule: this, - nestedLevelsAllowed: nestedLevelsAllowed - }); - this.el.append(ruleGroup.el); - ruleGroup.render(); - } else { - // For any other filter type... Add a metadata selector field whether the - // rule is new or has already been created - this.addFieldSelect(); - - // Operator field and value field Add an operator input only for already - // existing filters (For new filters, a metadata field needs to be selected - // first) - if ( - this.model.get("fields") && - this.model.get("fields").length - ) { - this.addOperatorSelect(); - this.addValueSelect(); - } - } - - - - return this; - - } catch (e) { - console.error("Error rendering the query Rule View, error message: ", e); + ], + + /** + * Creates a new QueryRuleView + * @param {Object} options - A literal object with options to pass to the view + */ + initialize: function (options) { + try { + // Get all the options and apply them to this view + if (typeof options == "object") { + var optionKeys = Object.keys(options); + _.each( + optionKeys, + function (key, i) { + this[key] = options[key]; + }, + this, + ); } - }, - /** - * Insert container for the color-coded rule numbering. - */ - addRuleInfo: function () { - try { - this.$indexEl = $(document.createElement("span")); - this.$ruleInfoEl = $(document.createElement("div")) - .addClass(this.ruleInfoClass); - this.$ruleInfoEl.append(this.$indexEl); - - this.$el.append(this.$ruleInfoEl); - this.updateRuleInfo(); - } catch (error) { - console.log( - "Error adding rule info container for a Query Rule, details: " + error + // If no model is provided in the options, we cannot render this view. A + // filter model cannot be created, because it must be part of a collection. + if (!this.model || !this.model.collection) { + console.error( + "error: A Filter model that's part of a Filters collection" + + " is required to initialize a Query Rule view.", ); + return; } - }, - /** - * Selects a color from the - * {@link QueryRuleView#ruleColorPalette rule colour palette array}, given an - * index. If the index is greater than the length of the palette, then the palette - * is effectively repeated until long enough (i.e. colours will be recycled). If - * no index in provided, the first colour in the palette will be selected. - * - * @param {number} [index=0] - The position of the rule within the Filters - * collection. - * @param {string} [defaultColor="#57b39c"] - A default colour to use in case - * there is problem with this function (hex color code beginning with '#'). - * @return {string} - Returns a hex color code string - */ - getPaletteColor: function (index = 0, defaultColor = "#57b39c") { - try { - // Allow the rule to inherit it's color from the parent rule within which it's - // nested - if(this.ruleColorPalette == "inherit"){ - return null - } - if (!this.ruleColorPalette || !this.ruleColorPalette.length) { - return defaultColor; - } - var numCols = this.ruleColorPalette.length; - if ((index + 1) > numCols) { - var n = Math.floor(index / numCols); - index = index - (numCols * n); - } - return this.ruleColorPalette[index]; - } catch (error) { - console.log( - "Error getting a color for a Query Rule, using the default colour" - + " instead. Error details: " + error + // The model may be removed during the save process if it's empty. Remove this + // Rule Group view when that happens. + this.stopListening(this.model, "remove"); + this.listenTo(this.model, "remove", function () { + this.removeSelf(); + }); + } catch (e) { + console.log( + "Failed to initialize a Query Builder View, error message:", + e, + ); + } + }, + + /** + * render - Render the view + * + * @return {QueryRule} Returns the view + */ + render: function () { + try { + // Add the Rule number. + // TODO: Also add the number of datasets related to rule + this.addRuleInfo(); + this.stopListening(this.model.collection, "remove"); + this.listenTo(this.model.collection, "remove", this.updateRuleInfo); + // Nested rules should also listen for changes in Filters of their parent Rule + if (this.parentRule) { + this.stopListening(this.parentRule.model.collection, "remove"); + this.listenTo( + this.parentRule.model.collection, + "remove", + this.updateRuleInfo, ); - return defaultColor; } - }, - /** - * Adds or updates the color-coded Query Rule information displayed to the user. - * This needs to be run when rules are added or removed. Rule information includes - * the rule number, but may one day also display information such as the number of - * results that there are for this individual rule. - */ - updateRuleInfo: function () { - try { - - // Rules are numbered in the order in which they appear in the Filters - // collection, excluding any invisible filter models. Rules nested in Rule - // Groups (within Filter Models) get numbered 3A, 3B, etc. - var letter = "" - var index = "" - // If this is a filter model nested in a filter group - if(this.parentRule){ - index = this.parentRule.ruleNumber; - var letterIndex = this.model.collection.visibleIndexOf(this.model); - if(typeof letterIndex === "number"){ - letter = String.fromCharCode(94 + letterIndex + 3).toUpperCase(); - } - // For top-level filter models - } else { - index = this.model.collection.visibleIndexOf(this.model); + // The remove button is needed for both FilterGroups and other Filter models + this.addRemoveButton(); + + // Render nested filter group views as another Query Builder. + if (this.model.type == "FilterGroup") { + this.$el.addClass("rule-group"); + + // We must initialize a QueryBuilderView using the inline require syntax to + // avoid the problem of circular dependencies. QueryRuleView requires + // QueryBuilderView, and QueryBuilderView requires QueryRuleView. For more + // info, see https://requirejs.org/docs/api.html#circular + var QueryBuilderView = require("views/queryBuilder/QueryBuilderView"); + + // The default + nestedLevelsAllowed = 1; + // If we are adding a query builer, then it is a nested level. Subtract one + // from the total levels allowed. + if (typeof this.nestedLevelsAllowed == "number") { + nestedLevelsAllowed = this.nestedLevelsAllowed - 1; } - if(typeof index == "number"){ - index = index + 1; + // If there is a special list of fields to exclude in nested Query Builders + // (i.e. in nested FilterGroup models), then pass this list on as the + // excludeFields list in the child QueryBuilder + var excludeFields = this.excludeFields; + if ( + this.nestedExcludeFields && + Array.isArray(this.nestedExcludeFields) + ) { + excludeFields = this.nestedExcludeFields; } - var ruleNumber = index + letter; - - // Set the rule number of the parent view to be accessed by any nested child - // rules - this.ruleNumber = ruleNumber; - - // if(this.model.type == "FilterGroup") - if (ruleNumber && ruleNumber.length) { - this.$indexEl.text("Rule " + ruleNumber); - } else { - this.$indexEl.text(""); - return - } - var color = this.getPaletteColor(index); - if (color) { - this.el.style.setProperty('--rule-color', color); + // Insert QueryRuleView + var ruleGroup = new QueryBuilderView({ + filterGroup: this.model, + // Nested Query Rules have the same color as their parent rule + ruleColorPalette: "inherit", + excludeFields: excludeFields, + specialFields: this.specialFields, + parentRule: this, + nestedLevelsAllowed: nestedLevelsAllowed, + }); + this.el.append(ruleGroup.el); + ruleGroup.render(); + } else { + // For any other filter type... Add a metadata selector field whether the + // rule is new or has already been created + this.addFieldSelect(); + + // Operator field and value field Add an operator input only for already + // existing filters (For new filters, a metadata field needs to be selected + // first) + if (this.model.get("fields") && this.model.get("fields").length) { + this.addOperatorSelect(); + this.addValueSelect(); } - } catch (error) { - console.log( - "Error updating the rule numbering for a Query Rule. Details: " + error - ); } - }, - /** - * addRemoveButton - Create and insert the button to remove the Query Rule - */ - addRemoveButton: function () { - try { - var removeButton = $( - "<i id='" + this.removeRuleID + this.cid + - "' class='" + this.removeClass + - " icon icon-remove' title='Remove this Query Rule'></i>" - ); - this.el.append(removeButton[0]); - } catch (e) { - console.error("Failed to create a remove button for a Query Rule, error details: " + e); + return this; + } catch (e) { + console.error( + "Error rendering the query Rule View, error message: ", + e, + ); + } + }, + + /** + * Insert container for the color-coded rule numbering. + */ + addRuleInfo: function () { + try { + this.$indexEl = $(document.createElement("span")); + this.$ruleInfoEl = $(document.createElement("div")).addClass( + this.ruleInfoClass, + ); + this.$ruleInfoEl.append(this.$indexEl); + + this.$el.append(this.$ruleInfoEl); + this.updateRuleInfo(); + } catch (error) { + console.log( + "Error adding rule info container for a Query Rule, details: " + + error, + ); + } + }, + + /** + * Selects a color from the + * {@link QueryRuleView#ruleColorPalette rule colour palette array}, given an + * index. If the index is greater than the length of the palette, then the palette + * is effectively repeated until long enough (i.e. colours will be recycled). If + * no index in provided, the first colour in the palette will be selected. + * + * @param {number} [index=0] - The position of the rule within the Filters + * collection. + * @param {string} [defaultColor="#57b39c"] - A default colour to use in case + * there is problem with this function (hex color code beginning with '#'). + * @return {string} - Returns a hex color code string + */ + getPaletteColor: function (index = 0, defaultColor = "#57b39c") { + try { + // Allow the rule to inherit it's color from the parent rule within which it's + // nested + if (this.ruleColorPalette == "inherit") { + return null; } - }, - - /** - * Determines whether the filter model that this rule renders matches one of the - * {@link QueryRuleView#specialFields special fields} set on this view. If it - * does, returns the first special field object that matches. For a filter model - * to match to one of the special fields, it must contain all of the fields listed - * in the special field's "fields" property. If the special field has an array set - * for "values", then the model's values must also exactly match the special - * field's values. - * - * @param {string[]} [fields] - Optionally set a list of query fields to search - * with. If not set, then the fields that are set on the view's filter model are - * used. - * @returns {SpecialField|null} - The matching special field, or null if no match - * was found. - * - * @since 2.15.0 - */ - getSpecialField: function(fields){ - - // Get information about the filter model (or used the fields passed to this - // function) - var selectedFields = fields || this.model.get("fields"); - var selectedFields = _.clone(selectedFields); - var selectedValues = this.model.get("values"); - - if(!this.specialFields || !Array.isArray(this.specialFields)){ - return null - } - - var matchingSpecialField = _.find(this.specialFields, function(specialField){ - - var fieldsMatch = false, - mustMatchValues = false, - valuesMatch = false; - - // If *all* the fields in the fields array are present in the list - // of fields that the special field represents, then count this as a match. - var commonFields = _.intersection(specialField.fields, selectedFields); - if(commonFields.length === specialField.fields.length){ - fieldsMatch = true - } - - // The selected value must *exactly match* if one is set in the special - // field - if(specialField.values){ - mustMatchValues = true; - valuesMatch = _.isEqual(specialField.values, selectedValues) - } - - return fieldsMatch && ( - !mustMatchValues || (mustMatchValues && valuesMatch) - ) - - }, this); - - // If this model matches one of the special fields, render it differently - return matchingSpecialField || null - }, - - /** - * Takes a list of query field names, checks if the model matches any of the - * special fields, and if it does, returns the list of fields with the actual - * field names replaced with the - * {@link QueryRuleView#specialFields special field name}. This function is the - * opposite of {@link QueryRuleView#convertFromSpecialFields} - * @param {string[]} fields - The list of field names to convert - * @returns {string[]} - The converted list of field names. If there were no - * special fields detected, or if there's an error, then then the field names are - * returned unchanged. - * - * @param {string[]} fields - The list of fields to convert to special fields, if - * the model matches any of the special field objects - * @returns {string[]} - Returns the list of fields with actual query field names - * replaced with special field names, if any match - * - * @since 2.15.0 - */ - convertToSpecialFields: function(fields){ - - try { - - var fields = _.clone(fields); - - // Insert the special field name at the same position as the associated - // query fields that we will remove - var replaceWithSpecialField = function(fields, specialField){ - if(specialField){ - position = _.findIndex(fields, function(selectedField){ - return specialField.fields.includes(selectedField); - }, this); - fields.splice(position, 0, specialField.name); - fields = _.difference(fields, specialField.fields); - } - return fields - } - - - // If the user selected a special field, make sure we convert those first - if( this.selectedSpecialFields && this.selectedSpecialFields.length ){ - this.selectedSpecialFields.forEach(function(specialFiend){ - fields = replaceWithSpecialField(fields, specialFiend) - }, this); + if (!this.ruleColorPalette || !this.ruleColorPalette.length) { + return defaultColor; + } + var numCols = this.ruleColorPalette.length; + if (index + 1 > numCols) { + var n = Math.floor(index / numCols); + index = index - numCols * n; + } + return this.ruleColorPalette[index]; + } catch (error) { + console.log( + "Error getting a color for a Query Rule, using the default colour" + + " instead. Error details: " + + error, + ); + return defaultColor; + } + }, + + /** + * Adds or updates the color-coded Query Rule information displayed to the user. + * This needs to be run when rules are added or removed. Rule information includes + * the rule number, but may one day also display information such as the number of + * results that there are for this individual rule. + */ + updateRuleInfo: function () { + try { + // Rules are numbered in the order in which they appear in the Filters + // collection, excluding any invisible filter models. Rules nested in Rule + // Groups (within Filter Models) get numbered 3A, 3B, etc. + var letter = ""; + var index = ""; + // If this is a filter model nested in a filter group + if (this.parentRule) { + index = this.parentRule.ruleNumber; + var letterIndex = this.model.collection.visibleIndexOf(this.model); + if (typeof letterIndex === "number") { + letter = String.fromCharCode(94 + letterIndex + 3).toUpperCase(); } + // For top-level filter models + } else { + index = this.model.collection.visibleIndexOf(this.model); + } - // Search for remaining special fields given the fields and model values - var matchingSpecialField = this.getSpecialField(fields); + if (typeof index == "number") { + index = index + 1; + } - // There may be more than one special field in the list of fields... - while(matchingSpecialField !== null){ - fields = replaceWithSpecialField(fields, matchingSpecialField) - // Check if there are more special fields remaining - matchingSpecialField = this.getSpecialField(fields); + var ruleNumber = index + letter; - } + // Set the rule number of the parent view to be accessed by any nested child + // rules + this.ruleNumber = ruleNumber; - return fields; - - } catch (error) { - console.log( - "Error converting query field names to special field names in" + - " a Query Rule View. Returning the list of fields unchanged." + - " Error details : " + error - ); - return fields + // if(this.model.type == "FilterGroup") + if (ruleNumber && ruleNumber.length) { + this.$indexEl.text("Rule " + ruleNumber); + } else { + this.$indexEl.text(""); + return; } - - }, - - /** - * Takes a list of query field names and checks if it contains any of the - * {@link QueryRuleView#specialFields special field names}. Returns the list with - * the special field names replaced with the actual field names that those special - * fields represent. Stores the name of each special field name removed in an - * array set on the view's selectedSpecialFields property. selectedSpecialFields - * is cleared each time this function runs. This function is the opposite of - * {@link QueryRuleView#convertToSpecialFields} - * @param {string[]} fields] - The list of field names to convert - * @returns {string[]} - The converted list of field names. If there were no - * special fields detected, or if there's an error, then then the field names are - * returned unchanged. - * - * @param {string[]} fields - The list of fields to convert to actual query - * service index fields - * @returns {string[]} - Returns the list of fields with any special field - * replaced with real fields from the query service index - * - * @since 2.15.0 - */ - convertFromSpecialFields: function(fields){ - try { - this.selectedSpecialFields = []; - if(this.specialFields){ - this.specialFields.forEach(function(specialField){ - var index = fields.indexOf(specialField.name); - if(index >= 0){ - // Keep a record that the user selected a special field (useful in the - // case that the special field is just a duplicate of another field) - this.selectedSpecialFields.push(specialField); - fields.splice.apply(fields, [index, 1].concat(specialField.fields)); - } - }, this); - } - return fields - } catch (error) { - console.log( - "Error converting special query fields to query fields that" + - " exist in the index in a Query Rule View. Returning the fields" + - " unchanged. Error details: " + error - ); - return fields + var color = this.getPaletteColor(index); + if (color) { + this.el.style.setProperty("--rule-color", color); } - }, - - /** - * Create and insert an input that allows the user to select a metadata field to - * query - */ - addFieldSelect: function () { - - try { - - // Check whether the filter model set on this view contains query fields - // and values that match one of the special rules. If it does, - // convert the list of field names to special field to pass on to the - // Query Field Select View. - var selectedFields = _.clone(this.model.get("fields")); - var selectedFields = this.convertToSpecialFields(selectedFields); - - this.fieldSelect = new QueryFieldSelect({ - selected: selectedFields, - excludeFields: this.excludeFields, - addFields: this.specialFields, - separatorText: this.model.get("fieldsOperator"), - }); - this.fieldSelect.$el.addClass(this.fieldsClass); - this.el.append(this.fieldSelect.el); - this.fieldSelect.render(); - - // Update the model when the fieldsOperator changes - this.stopListening( - this.fieldSelect, - 'separatorChanged' - ); - this.listenTo( - this.fieldSelect, - 'separatorChanged', - function(newOperator){ - this.model.set("fieldsOperator", newOperator) - } - ); - // Update model when the selected fields change - this.stopListening( - this.fieldSelect, - 'changeSelection' - ); - this.listenTo( - this.fieldSelect, - 'changeSelection', - this.handleFieldChange + } catch (error) { + console.log( + "Error updating the rule numbering for a Query Rule. Details: " + + error, + ); + } + }, + + /** + * addRemoveButton - Create and insert the button to remove the Query Rule + */ + addRemoveButton: function () { + try { + var removeButton = $( + "<i id='" + + this.removeRuleID + + this.cid + + "' class='" + + this.removeClass + + " icon icon-remove' title='Remove this Query Rule'></i>", + ); + this.el.append(removeButton[0]); + } catch (e) { + console.error( + "Failed to create a remove button for a Query Rule, error details: " + + e, + ); + } + }, + + /** + * Determines whether the filter model that this rule renders matches one of the + * {@link QueryRuleView#specialFields special fields} set on this view. If it + * does, returns the first special field object that matches. For a filter model + * to match to one of the special fields, it must contain all of the fields listed + * in the special field's "fields" property. If the special field has an array set + * for "values", then the model's values must also exactly match the special + * field's values. + * + * @param {string[]} [fields] - Optionally set a list of query fields to search + * with. If not set, then the fields that are set on the view's filter model are + * used. + * @returns {SpecialField|null} - The matching special field, or null if no match + * was found. + * + * @since 2.15.0 + */ + getSpecialField: function (fields) { + // Get information about the filter model (or used the fields passed to this + // function) + var selectedFields = fields || this.model.get("fields"); + var selectedFields = _.clone(selectedFields); + var selectedValues = this.model.get("values"); + + if (!this.specialFields || !Array.isArray(this.specialFields)) { + return null; + } + + var matchingSpecialField = _.find( + this.specialFields, + function (specialField) { + var fieldsMatch = false, + mustMatchValues = false, + valuesMatch = false; + + // If *all* the fields in the fields array are present in the list + // of fields that the special field represents, then count this as a match. + var commonFields = _.intersection( + specialField.fields, + selectedFields, ); + if (commonFields.length === specialField.fields.length) { + fieldsMatch = true; + } - } catch (e) { - console.error("Error adding a metadata selector input in the Query Rule" - + " View, error message:", e); - } - }, + // The selected value must *exactly match* if one is set in the special + // field + if (specialField.values) { + mustMatchValues = true; + valuesMatch = _.isEqual(specialField.values, selectedValues); + } - /** - * handleFieldChange - Called when the Query Field Select View triggers a change - * event. Updates the model with the new fields, and if required, - * 1) converts the filter model to a different type based on the types of fields - * selected, 2) updates the operator select and the value select - * - * @param {string[]} newFields The list of new query fields that were selected - */ - handleFieldChange: function (newFields) { - - try { - - // Uncomment the following chunk to clear operator & values when the field - // input is cleared. - // if(!newFields || newFields.length === 0 || newFields[0] === ""){ - // if(this.operatorSelect){ - // this.operatorSelect.changeSelection([""]); - // } - // this.model.set("fields", this.model.defaults().fields); - // return - // } - - // Get the selected operator before the field changed - var opBefore = this.getSelectedOperator(); - - // If any of the new fields are special fields, replace them with the - // actual query fields before setting them in the model... - newFields = this.convertFromSpecialFields(newFields); - - // Get the current type of filter and required type given the newly selected - // fields - var typeBefore = this.model.get("nodeName"), - typeAfter = MetacatUI.queryFields.getRequiredFilterType(newFields); - - // If the type has changed, then replace the model with one of the correct - // type, update the value and operator inputs, and do nothing else - if (typeBefore != typeAfter) { - this.model = this.model.collection.replaceModel( - this.model, - { filterType: typeAfter, fields: newFields } + return ( + fieldsMatch && + (!mustMatchValues || (mustMatchValues && valuesMatch)) + ); + }, + this, + ); + + // If this model matches one of the special fields, render it differently + return matchingSpecialField || null; + }, + + /** + * Takes a list of query field names, checks if the model matches any of the + * special fields, and if it does, returns the list of fields with the actual + * field names replaced with the + * {@link QueryRuleView#specialFields special field name}. This function is the + * opposite of {@link QueryRuleView#convertFromSpecialFields} + * @param {string[]} fields - The list of field names to convert + * @returns {string[]} - The converted list of field names. If there were no + * special fields detected, or if there's an error, then then the field names are + * returned unchanged. + * + * @param {string[]} fields - The list of fields to convert to special fields, if + * the model matches any of the special field objects + * @returns {string[]} - Returns the list of fields with actual query field names + * replaced with special field names, if any match + * + * @since 2.15.0 + */ + convertToSpecialFields: function (fields) { + try { + var fields = _.clone(fields); + + // Insert the special field name at the same position as the associated + // query fields that we will remove + var replaceWithSpecialField = function (fields, specialField) { + if (specialField) { + position = _.findIndex( + fields, + function (selectedField) { + return specialField.fields.includes(selectedField); + }, + this, ); - this.removeInput("value") - this.removeInput("operator") - this.addOperatorSelect(""); - return + fields.splice(position, 0, specialField.name); + fields = _.difference(fields, specialField.fields); } + return fields; + }; - // If the filter model type is the same, and the operator options are the same - // for the selected fields, then update the model - this.model.set("fields", newFields); - - // Get the selected operator now that we've updated the model with new fields - var opAfter = this.getSelectedOperator(); + // If the user selected a special field, make sure we convert those first + if (this.selectedSpecialFields && this.selectedSpecialFields.length) { + this.selectedSpecialFields.forEach(function (specialFiend) { + fields = replaceWithSpecialField(fields, specialFiend); + }, this); + } - // Add an empty operator input field, if there isn't one - if (!this.operatorSelect) { - this.addOperatorSelect(""); - // If the operator options have changed, refresh the operator input - } else if (opAfter !== opBefore){ - this.removeInput("operator"); - // Make sure that we overwrite any values that don't apply to the new options. - this.handleOperatorChange([""]); - this.addOperatorSelect(""); - return - } + // Search for remaining special fields given the fields and model values + var matchingSpecialField = this.getSpecialField(fields); - // Refresh the value select in case a different value input is required for - // the new fields - if (this.valueSelect) { - this.removeInput("value"); - this.addValueSelect(); - } + // There may be more than one special field in the list of fields... + while (matchingSpecialField !== null) { + fields = replaceWithSpecialField(fields, matchingSpecialField); + // Check if there are more special fields remaining + matchingSpecialField = this.getSpecialField(fields); + } - } catch (e) { - console.error("Failed to handle query field change in the Query Rule View," + - " error message: " + e); + return fields; + } catch (error) { + console.log( + "Error converting query field names to special field names in" + + " a Query Rule View. Returning the list of fields unchanged." + + " Error details : " + + error, + ); + return fields; + } + }, + + /** + * Takes a list of query field names and checks if it contains any of the + * {@link QueryRuleView#specialFields special field names}. Returns the list with + * the special field names replaced with the actual field names that those special + * fields represent. Stores the name of each special field name removed in an + * array set on the view's selectedSpecialFields property. selectedSpecialFields + * is cleared each time this function runs. This function is the opposite of + * {@link QueryRuleView#convertToSpecialFields} + * @param {string[]} fields] - The list of field names to convert + * @returns {string[]} - The converted list of field names. If there were no + * special fields detected, or if there's an error, then then the field names are + * returned unchanged. + * + * @param {string[]} fields - The list of fields to convert to actual query + * service index fields + * @returns {string[]} - Returns the list of fields with any special field + * replaced with real fields from the query service index + * + * @since 2.15.0 + */ + convertFromSpecialFields: function (fields) { + try { + this.selectedSpecialFields = []; + if (this.specialFields) { + this.specialFields.forEach(function (specialField) { + var index = fields.indexOf(specialField.name); + if (index >= 0) { + // Keep a record that the user selected a special field (useful in the + // case that the special field is just a duplicate of another field) + this.selectedSpecialFields.push(specialField); + fields.splice.apply( + fields, + [index, 1].concat(specialField.fields), + ); + } + }, this); } + return fields; + } catch (error) { + console.log( + "Error converting special query fields to query fields that" + + " exist in the index in a Query Rule View. Returning the fields" + + " unchanged. Error details: " + + error, + ); + return fields; + } + }, + + /** + * Create and insert an input that allows the user to select a metadata field to + * query + */ + addFieldSelect: function () { + try { + // Check whether the filter model set on this view contains query fields + // and values that match one of the special rules. If it does, + // convert the list of field names to special field to pass on to the + // Query Field Select View. + var selectedFields = _.clone(this.model.get("fields")); + var selectedFields = this.convertToSpecialFields(selectedFields); + + this.fieldSelect = new QueryFieldSelect({ + selected: selectedFields, + excludeFields: this.excludeFields, + addFields: this.specialFields, + separatorText: this.model.get("fieldsOperator"), + }); + this.fieldSelect.$el.addClass(this.fieldsClass); + this.el.append(this.fieldSelect.el); + this.fieldSelect.render(); + + // Update the model when the fieldsOperator changes + this.stopListening(this.fieldSelect, "separatorChanged"); + this.listenTo( + this.fieldSelect, + "separatorChanged", + function (newOperator) { + this.model.set("fieldsOperator", newOperator); + }, + ); + // Update model when the selected fields change + this.stopListening(this.fieldSelect, "changeSelection"); + this.listenTo( + this.fieldSelect, + "changeSelection", + this.handleFieldChange, + ); + } catch (e) { + console.error( + "Error adding a metadata selector input in the Query Rule" + + " View, error message:", + e, + ); + } + }, + + /** + * handleFieldChange - Called when the Query Field Select View triggers a change + * event. Updates the model with the new fields, and if required, + * 1) converts the filter model to a different type based on the types of fields + * selected, 2) updates the operator select and the value select + * + * @param {string[]} newFields The list of new query fields that were selected + */ + handleFieldChange: function (newFields) { + try { + // Uncomment the following chunk to clear operator & values when the field + // input is cleared. + // if(!newFields || newFields.length === 0 || newFields[0] === ""){ + // if(this.operatorSelect){ + // this.operatorSelect.changeSelection([""]); + // } + // this.model.set("fields", this.model.defaults().fields); + // return + // } - }, + // Get the selected operator before the field changed + var opBefore = this.getSelectedOperator(); - /** - * Create and insert an input field where the user can select an operator for the - * given rule. Operators will vary depending on filter model type. - * - * @param {string} selectedOperator - optional. The label of an operator to - * pre-select. Set to an empty string to render an empty operator selector. - */ - addOperatorSelect: function (selectedOperator) { - try { - - var view = this; - var operatorError = false; - - var options = this.getOperatorOptions(); - - // Identify the selected operator for existing models - if (typeof selectedOperator !== "string") { - selectedOperator = this.getSelectedOperator(); - // If there was no operator found, then this is probably an unsupported - // combination of exclude + matchSubstring + filterType - if (selectedOperator === "") { - operatorError = true; - } - } + // If any of the new fields are special fields, replace them with the + // actual query fields before setting them in the model... + newFields = this.convertFromSpecialFields(newFields); - if (selectedOperator === "") { - selectedOperator = [] - } else { - selectedOperator = [selectedOperator] - } + // Get the current type of filter and required type given the newly selected + // fields + var typeBefore = this.model.get("nodeName"), + typeAfter = MetacatUI.queryFields.getRequiredFilterType(newFields); - this.operatorSelect = new SearchableSelect({ - options: options, - allowMulti: false, - inputLabel: "Select an operator", - clearable: false, - placeholderText: "Select an operator", - selected: selectedOperator + // If the type has changed, then replace the model with one of the correct + // type, update the value and operator inputs, and do nothing else + if (typeBefore != typeAfter) { + this.model = this.model.collection.replaceModel(this.model, { + filterType: typeAfter, + fields: newFields, }); - this.operatorSelect.$el.addClass(this.operatorClass); - this.el.append(this.operatorSelect.el); - - if (operatorError) { - view.listenToOnce(view.operatorSelect, "postRender", function () { - view.operatorSelect.showMessage( - "Please select a valid operator", - "error", - true - ) - }) - } + this.removeInput("value"); + this.removeInput("operator"); + this.addOperatorSelect(""); + return; + } - this.operatorSelect.render(); + // If the filter model type is the same, and the operator options are the same + // for the selected fields, then update the model + this.model.set("fields", newFields); - // Update model when the values change - this.stopListening( - this.operatorSelect, - 'changeSelection' - ); - this.listenTo( - this.operatorSelect, - 'changeSelection', - this.handleOperatorChange - ); + // Get the selected operator now that we've updated the model with new fields + var opAfter = this.getSelectedOperator(); - } catch (e) { - console.error("Error adding an operator selector input in the Query Rule " + - "View, error message:", e); + // Add an empty operator input field, if there isn't one + if (!this.operatorSelect) { + this.addOperatorSelect(""); + // If the operator options have changed, refresh the operator input + } else if (opAfter !== opBefore) { + this.removeInput("operator"); + // Make sure that we overwrite any values that don't apply to the new options. + this.handleOperatorChange([""]); + this.addOperatorSelect(""); + return; } - }, - /** - * handleOperatorChange - When the operator selection is changed, update the model - * and re-set the value UI when required - * - * @param {string[]} newOperatorLabel The new operator label within an array, - * e.g. ["is greater than"] - */ - handleOperatorChange: function (newOperatorLabel) { - - try { - - var view = this; - - if (!newOperatorLabel || newOperatorLabel[0] == "") { - var modelDefaults = this.model.defaults(); - this.model.set({ - min: modelDefaults.min, - max: modelDefaults.max, - values: modelDefaults.values - }) - this.removeInput("value"); - return; + // Refresh the value select in case a different value input is required for + // the new fields + if (this.valueSelect) { + this.removeInput("value"); + this.addValueSelect(); + } + } catch (e) { + console.error( + "Failed to handle query field change in the Query Rule View," + + " error message: " + + e, + ); + } + }, + + /** + * Create and insert an input field where the user can select an operator for the + * given rule. Operators will vary depending on filter model type. + * + * @param {string} selectedOperator - optional. The label of an operator to + * pre-select. Set to an empty string to render an empty operator selector. + */ + addOperatorSelect: function (selectedOperator) { + try { + var view = this; + var operatorError = false; + + var options = this.getOperatorOptions(); + + // Identify the selected operator for existing models + if (typeof selectedOperator !== "string") { + selectedOperator = this.getSelectedOperator(); + // If there was no operator found, then this is probably an unsupported + // combination of exclude + matchSubstring + filterType + if (selectedOperator === "") { + operatorError = true; } + } - // Get the properties of the newly selected operator. The newOperatorLabel - // will be an array with one value. Select only from the available options, - // since there may be multiple options with the same label in - // this.operatorOptions. - var options = this.getOperatorOptions(); - var operator = _.findWhere( options, { label: newOperatorLabel[0] }); - - // Gather information about which values are currently set on the model, and - // which are required - var // Type - type = view.model.get("nodeName"), - isNumeric = ["dateFilter", "numericFilter"].includes(type), - isRange = operator.hasMin && operator.hasMax, - - // Values - modelValues = this.model.get("values"), - modelHasValues = modelValues ? modelValues && modelValues.length : false, - modelFirstValue = modelHasValues ? modelValues[0] : null, - modelValueInt = parseInt(modelFirstValue) ? parseInt(modelFirstValue) : null, - needsValue = isNumeric && !modelValueInt && !operator.hasMin && !operator.hasMax, - - // Min - modelMin = this.model.get("min"), - modelHasMin = modelMin === 0 || modelMin, - needsMin = operator.hasMin && !modelHasMin, - - // Max - modelMax = this.model.get("max"), - modelHasMax = modelMax === 0 || modelMax, - needsMax = operator.hasMax && !modelHasMax; - - // Some operator options include a specific value to be set on the model. For - // example, "is not empty", should set the model value to the "*" wildcard. - // For operators with these specific value requirements, update the filter - // model value and remove the value select input. - if (operator.values && operator.values.length) { - this.removeInput("value"); - this.model.set("values", operator.values); - // If the operator does not have a default value, then ensure that there is - // a value select available. - } else { - if (!this.valueSelect) { - this.model.set("values", view.model.defaults().values); - this.addValueSelect(); - } - } + if (selectedOperator === "") { + selectedOperator = []; + } else { + selectedOperator = [selectedOperator]; + } - // Update the model with true or false for matchSubstring and exclude - ["matchSubstring", "exclude"].forEach((prop, i) => { - if (typeof operator[prop] !== "undefined") { - view.model.set(prop, operator[prop]); - } else { - view.model.set(prop, view.model.defaults()[prop]); - } + this.operatorSelect = new SearchableSelect({ + options: options, + allowMulti: false, + inputLabel: "Select an operator", + clearable: false, + placeholderText: "Select an operator", + selected: selectedOperator, + }); + this.operatorSelect.$el.addClass(this.operatorClass); + this.el.append(this.operatorSelect.el); + + if (operatorError) { + view.listenToOnce(view.operatorSelect, "postRender", function () { + view.operatorSelect.showMessage( + "Please select a valid operator", + "error", + true, + ); }); + } - // Set min & max values as required by the operator - // TODO - test this strategy with dates... - - // Add a minimum value if one is needed - if (needsMin) { - // Search for the min in the values, then in the max - if (modelValueInt || modelValueInt === 0) { - this.model.set("min", modelValueInt) - } else if (modelHasMax) { - this.model.set("min", modelMax) - } else { - this.model.set("min", 0) - } - } - - // Add a maximum value if one is needed - if (needsMax) { - // Search for the min in the values, then in the max - if (modelValueInt || modelValueInt === 0) { - this.model.set("max", modelValueInt) - } else if (modelHasMin) { - this.model.set("max", modelMin) - } else { - this.model.set("max", 0) - } - } + this.operatorSelect.render(); + + // Update model when the values change + this.stopListening(this.operatorSelect, "changeSelection"); + this.listenTo( + this.operatorSelect, + "changeSelection", + this.handleOperatorChange, + ); + } catch (e) { + console.error( + "Error adding an operator selector input in the Query Rule " + + "View, error message:", + e, + ); + } + }, + + /** + * handleOperatorChange - When the operator selection is changed, update the model + * and re-set the value UI when required + * + * @param {string[]} newOperatorLabel The new operator label within an array, + * e.g. ["is greater than"] + */ + handleOperatorChange: function (newOperatorLabel) { + try { + var view = this; + + if (!newOperatorLabel || newOperatorLabel[0] == "") { + var modelDefaults = this.model.defaults(); + this.model.set({ + min: modelDefaults.min, + max: modelDefaults.max, + values: modelDefaults.values, + }); + this.removeInput("value"); + return; + } - // Add a value if one is needed - if (needsValue) { - if (modelHasMin) { - this.model.set("values", [modelMin]) - } else if (modelHasMax) { - this.model.set("values", [modelMax]) - } else { - this.model.set("values", [0]) - } + // Get the properties of the newly selected operator. The newOperatorLabel + // will be an array with one value. Select only from the available options, + // since there may be multiple options with the same label in + // this.operatorOptions. + var options = this.getOperatorOptions(); + var operator = _.findWhere(options, { label: newOperatorLabel[0] }); + + // Gather information about which values are currently set on the model, and + // which are required + var // Type + type = view.model.get("nodeName"), + isNumeric = ["dateFilter", "numericFilter"].includes(type), + isRange = operator.hasMin && operator.hasMax, + // Values + modelValues = this.model.get("values"), + modelHasValues = modelValues + ? modelValues && modelValues.length + : false, + modelFirstValue = modelHasValues ? modelValues[0] : null, + modelValueInt = parseInt(modelFirstValue) + ? parseInt(modelFirstValue) + : null, + needsValue = + isNumeric && + !modelValueInt && + !operator.hasMin && + !operator.hasMax, + // Min + modelMin = this.model.get("min"), + modelHasMin = modelMin === 0 || modelMin, + needsMin = operator.hasMin && !modelHasMin, + // Max + modelMax = this.model.get("max"), + modelHasMax = modelMax === 0 || modelMax, + needsMax = operator.hasMax && !modelHasMax; + + // Some operator options include a specific value to be set on the model. For + // example, "is not empty", should set the model value to the "*" wildcard. + // For operators with these specific value requirements, update the filter + // model value and remove the value select input. + if (operator.values && operator.values.length) { + this.removeInput("value"); + this.model.set("values", operator.values); + // If the operator does not have a default value, then ensure that there is + // a value select available. + } else { + if (!this.valueSelect) { + this.model.set("values", view.model.defaults().values); + this.addValueSelect(); } + } - // Remove the minimum and max if they should not be included in the filter - if (modelHasMax && !operator.hasMax) { - this.model.set("max", this.model.defaults().max) + // Update the model with true or false for matchSubstring and exclude + ["matchSubstring", "exclude"].forEach((prop, i) => { + if (typeof operator[prop] !== "undefined") { + view.model.set(prop, operator[prop]); + } else { + view.model.set(prop, view.model.defaults()[prop]); } - if (modelHasMin && !operator.hasMin) { - this.model.set("min", this.model.defaults().min) + }); + + // Set min & max values as required by the operator + // TODO - test this strategy with dates... + + // Add a minimum value if one is needed + if (needsMin) { + // Search for the min in the values, then in the max + if (modelValueInt || modelValueInt === 0) { + this.model.set("min", modelValueInt); + } else if (modelHasMax) { + this.model.set("min", modelMax); + } else { + this.model.set("min", 0); } + } - if (isRange) { - this.model.set("range", true) + // Add a maximum value if one is needed + if (needsMax) { + // Search for the min in the values, then in the max + if (modelValueInt || modelValueInt === 0) { + this.model.set("max", modelValueInt); + } else if (modelHasMin) { + this.model.set("max", modelMin); } else { - if (isNumeric) { - this.model.set("range", false) - } else { - this.model.unset("range") - } + this.model.set("max", 0); } + } - // If the operator changed for a numeric or date field, reset the value - // select. This way it can change from a range to a single value input if - // needed. - if (isNumeric) { - this.removeInput("value"); - this.addValueSelect(); + // Add a value if one is needed + if (needsValue) { + if (modelHasMin) { + this.model.set("values", [modelMin]); + } else if (modelHasMax) { + this.model.set("values", [modelMax]); + } else { + this.model.set("values", [0]); } - } catch (e) { - console.error("Failed to handle the operator selection in a Query Rule " + - "view, error message: " + e); } - }, - /** - * Get a list of {@link QueryRuleView#operatorOptions operatorOptions} that are - * allowed for this view's filter model - * - * @param {string[]} [fields] - Optional list of fields to use instead of the - * fields set on this view's Filter model - * - * @since 2.15.0 - */ - getOperatorOptions: function(fields){ - - try { - // Check which type of rule this is (boolean, numeric, text, date) - var type = this.model.get("nodeName"); - - // If this rule contains a special field, replace the real query field names - // with the special field names for the purpose of selecting operator options - var fields = fields || this.model.get("fields"); - var fields = _.clone(fields); - var fields = this.convertToSpecialFields(fields); - - // Get the list of options for a user to select from based on field name. - // All of the rule's fields must be contained within the operator option's - // list of allowed fields for it to be a match. - var options = _.filter(this.operatorOptions, function (option) { - if(option.fields){ - return _.every(fields, function(fieldName){ - return option.fields.includes(fieldName) - }) - } - }); + // Remove the minimum and max if they should not be included in the filter + if (modelHasMax && !operator.hasMax) { + this.model.set("max", this.model.defaults().max); + } + if (modelHasMin && !operator.hasMin) { + this.model.set("min", this.model.defaults().min); + } - // Get the list of options for a user to select from based on type, if there - // were none that matched based on field names - if(!options || !options.length){ - options = _.filter(this.operatorOptions, function (option) { - if(option.types){ - return option.types.includes(type) - } - }, this); + if (isRange) { + this.model.set("range", true); + } else { + if (isNumeric) { + this.model.set("range", false); + } else { + this.model.unset("range"); } - - return options - } catch (error) { - console.log("Error getting operator options in a Query Rule View, " + - "Error details: " + error); } - }, - - /** - * getSelectedOperator - Based on values set on the model, get the label to show - * in the "operator" filed of the Query Rule - * - * @return {string} The operator label - */ - getSelectedOperator: function () { - try { - - // This view - var view = this, - // The options that we will filter down - options = this.operatorOptions, - // The user-facing operator label that we will return - selectedOperator = ""; - - // --- Filter 1 - Filter options by type --- // - - // Reduce list of options to only those that apply to the current filter type - var type = view.model.get("nodeName"); - var options = this.getOperatorOptions(); - - // --- Filter 2 - filter by 'matchSubstring', 'exclude', 'min', 'max' --- // + // If the operator changed for a numeric or date field, reset the value + // select. This way it can change from a range to a single value input if + // needed. + if (isNumeric) { + this.removeInput("value"); + this.addValueSelect(); + } + } catch (e) { + console.error( + "Failed to handle the operator selection in a Query Rule " + + "view, error message: " + + e, + ); + } + }, + + /** + * Get a list of {@link QueryRuleView#operatorOptions operatorOptions} that are + * allowed for this view's filter model + * + * @param {string[]} [fields] - Optional list of fields to use instead of the + * fields set on this view's Filter model + * + * @since 2.15.0 + */ + getOperatorOptions: function (fields) { + try { + // Check which type of rule this is (boolean, numeric, text, date) + var type = this.model.get("nodeName"); + + // If this rule contains a special field, replace the real query field names + // with the special field names for the purpose of selecting operator options + var fields = fields || this.model.get("fields"); + var fields = _.clone(fields); + var fields = this.convertToSpecialFields(fields); + + // Get the list of options for a user to select from based on field name. + // All of the rule's fields must be contained within the operator option's + // list of allowed fields for it to be a match. + var options = _.filter(this.operatorOptions, function (option) { + if (option.fields) { + return _.every(fields, function (fieldName) { + return option.fields.includes(fieldName); + }); + } + }); - // Create the conditions based on the model - var conditions = _.pick( - this.model.attributes, - 'matchSubstring', 'exclude', 'min', 'max' + // Get the list of options for a user to select from based on type, if there + // were none that matched based on field names + if (!options || !options.length) { + options = _.filter( + this.operatorOptions, + function (option) { + if (option.types) { + return option.types.includes(type); + } + }, + this, ); + } - var isNumeric = ["dateFilter", "numericFilter"].includes(type); - - if (!conditions.min && conditions.min !== 0) { - if (isNumeric) { - conditions.hasMin = false - } - } else if (isNumeric) { - conditions.hasMin = true + return options; + } catch (error) { + console.log( + "Error getting operator options in a Query Rule View, " + + "Error details: " + + error, + ); + } + }, + + /** + * getSelectedOperator - Based on values set on the model, get the label to show + * in the "operator" filed of the Query Rule + * + * @return {string} The operator label + */ + getSelectedOperator: function () { + try { + // This view + var view = this, + // The options that we will filter down + options = this.operatorOptions, + // The user-facing operator label that we will return + selectedOperator = ""; + + // --- Filter 1 - Filter options by type --- // + + // Reduce list of options to only those that apply to the current filter type + var type = view.model.get("nodeName"); + var options = this.getOperatorOptions(); + + // --- Filter 2 - filter by 'matchSubstring', 'exclude', 'min', 'max' --- // + + // Create the conditions based on the model + var conditions = _.pick( + this.model.attributes, + "matchSubstring", + "exclude", + "min", + "max", + ); + + var isNumeric = ["dateFilter", "numericFilter"].includes(type); + + if (!conditions.min && conditions.min !== 0) { + if (isNumeric) { + conditions.hasMin = false; } - if (!conditions.max && conditions.max !== 0) { - if (isNumeric) { - conditions.hasMax = false - } - } else if (isNumeric) { - conditions.hasMax = true + } else if (isNumeric) { + conditions.hasMin = true; + } + if (!conditions.max && conditions.max !== 0) { + if (isNumeric) { + conditions.hasMax = false; } + } else if (isNumeric) { + conditions.hasMax = true; + } - delete conditions.min - delete conditions.max + delete conditions.min; + delete conditions.max; - var options = _.where(options, conditions); + var options = _.where(options, conditions); - // --- Filter 3 - filter based on the value, if there's > 1 option --- // + // --- Filter 3 - filter based on the value, if there's > 1 option --- // - if (options.length > 1) { - // Model values that determine the user-facing operator eg ["*"], [true], - // [false] - var specialValues = _.compact( - _.pluck(this.operatorOptions, "values") + if (options.length > 1) { + // Model values that determine the user-facing operator eg ["*"], [true], + // [false] + var specialValues = _.compact( + _.pluck(this.operatorOptions, "values"), ), - specialValues = specialValues.map(val => JSON.stringify(val)), - specialValues = _.uniq(specialValues); - - options = options.filter(function (option) { - var modelValsStringified = JSON.stringify(view.model.get("values")); - if (specialValues.includes(modelValsStringified)) { - if (JSON.stringify(option.values) === modelValsStringified) { - return true - } - } else { - if (!option.values) { - return true - } - } - }) - } - // --- Return value --- // + specialValues = specialValues.map((val) => JSON.stringify(val)), + specialValues = _.uniq(specialValues); - if (options.length === 1) { - selectedOperator = options[0].label - } - - return selectedOperator - } catch (e) { - console.error("Failed to select an operator in the Query Rule View, error" + - " message: " + e); + options = options.filter(function (option) { + var modelValsStringified = JSON.stringify( + view.model.get("values"), + ); + if (specialValues.includes(modelValsStringified)) { + if (JSON.stringify(option.values) === modelValsStringified) { + return true; + } + } else { + if (!option.values) { + return true; + } + } + }); } - }, + // --- Return value --- // - /** - * getCategory - Given an array of query fields, get the user-facing category that - * these fields belong to. If there are fields from multiple categories, then a - * default "Text" category is returned. - * - * @param {string[]} fields An array of query (Solr) fields - * @return {string} The label for the category that the given fields belong to - */ - getCategory: function (fields) { - - try { - var categories = [], - // When fields is empty or are different types - defaultCategory = "Text"; - - if (!fields || fields.length === 0 || fields[0] === "") { - return defaultCategory - } + if (options.length === 1) { + selectedOperator = options[0].label; + } - fields.forEach((field, i) => { - // Get the category of the field from the matching filter model in the Query - // Fields Collection - var fieldModel = MetacatUI.queryFields.findWhere({ name: field }); - categories.push(fieldModel.get("category")) - }); + return selectedOperator; + } catch (e) { + console.error( + "Failed to select an operator in the Query Rule View, error" + + " message: " + + e, + ); + } + }, + + /** + * getCategory - Given an array of query fields, get the user-facing category that + * these fields belong to. If there are fields from multiple categories, then a + * default "Text" category is returned. + * + * @param {string[]} fields An array of query (Solr) fields + * @return {string} The label for the category that the given fields belong to + */ + getCategory: function (fields) { + try { + var categories = [], + // When fields is empty or are different types + defaultCategory = "Text"; + + if (!fields || fields.length === 0 || fields[0] === "") { + return defaultCategory; + } - // Test of all the fields are of the same type - var allEqual = categories.every((val, i, arr) => val === arr[0]); + fields.forEach((field, i) => { + // Get the category of the field from the matching filter model in the Query + // Fields Collection + var fieldModel = MetacatUI.queryFields.findWhere({ name: field }); + categories.push(fieldModel.get("category")); + }); - if (allEqual) { - return categories[0] - } else { - return defaultCategory - } + // Test of all the fields are of the same type + var allEqual = categories.every((val, i, arr) => val === arr[0]); - } catch (e) { - console.log("Failed to detect the category for a group of filters in the" + - " Query Rule View, error message: " + e); + if (allEqual) { + return categories[0]; + } else { + return defaultCategory; } - - }, - - /** - * Create and insert an input field where the user can provide a search value - */ - addValueSelect: function () { - try { - - var view = this - fields = this.model.get("fields"), - filterType = MetacatUI.queryFields.getRequiredFilterType(fields), - category = this.getCategory(fields), - interfaces = this.valueSelectUImap, - label = ""; - - // To help guide users to create valid queries, the type of value field will - // vary based on the type of field (i.e. filter nodeName), and the operator - // selected. - - // Some user-facing operators (e.g. "is true") don't require a value to be set - var selectedOperator = _.findWhere( - this.operatorOptions, - { label: this.getSelectedOperator() } - ); - if (selectedOperator) { - if (selectedOperator.values && selectedOperator.values.length) { - return - } + } catch (e) { + console.log( + "Failed to detect the category for a group of filters in the" + + " Query Rule View, error message: " + + e, + ); + } + }, + + /** + * Create and insert an input field where the user can provide a search value + */ + addValueSelect: function () { + try { + var view = this; + (fields = this.model.get("fields")), + (filterType = MetacatUI.queryFields.getRequiredFilterType(fields)), + (category = this.getCategory(fields)), + (interfaces = this.valueSelectUImap), + (label = ""); + + // To help guide users to create valid queries, the type of value field will + // vary based on the type of field (i.e. filter nodeName), and the operator + // selected. + + // Some user-facing operators (e.g. "is true") don't require a value to be set + var selectedOperator = _.findWhere(this.operatorOptions, { + label: this.getSelectedOperator(), + }); + if (selectedOperator) { + if (selectedOperator.values && selectedOperator.values.length) { + return; } + } - // Find the appropriate UI to use the the value select field. Find the first - // match in the valueSelectUImap according to the filter type and the - // categories associated with the metadata field. - var interfaceProperties = _.find(interfaces, function (thisInterface) { + // Find the appropriate UI to use the the value select field. Find the first + // match in the valueSelectUImap according to the filter type and the + // categories associated with the metadata field. + var interfaceProperties = _.find( + interfaces, + function (thisInterface) { var typesMatch = true, categoriesMatch = true, namesMatch = true; - if (thisInterface.queryFields && thisInterface.queryFields.length) { + if ( + thisInterface.queryFields && + thisInterface.queryFields.length + ) { fields.forEach((field, i) => { if (thisInterface.queryFields.includes(field) === false) { namesMatch = false; } }); } - if (thisInterface.filterTypes && thisInterface.filterTypes.length) { - typesMatch = thisInterface.filterTypes.includes(filterType) + if ( + thisInterface.filterTypes && + thisInterface.filterTypes.length + ) { + typesMatch = thisInterface.filterTypes.includes(filterType); } if (thisInterface.categories && thisInterface.categories.length) { - categoriesMatch = thisInterface.categories.includes(category) - } - return typesMatch && categoriesMatch && namesMatch - }); - - this.valueSelect = interfaceProperties.uiFunction.call(this); - if (interfaceProperties.label && interfaceProperties.label.length) { - label = $( - "<p class='subtle searchable-select-label'>" + - interfaceProperties.label + "</p>" - ); - } - - // Append and render the chosen value selector - this.el.append(view.valueSelect.el); - this.valueSelect.$el.addClass(this.valuesClass); - view.valueSelect.render(); - if (label) { - view.valueSelect.$el.prepend(label) - } - - // Make sure the listeners set below are not set multiple times - this.stopListening(view.valueSelect, 'changeSelection inputFocus separatorChanged'); - - // Update model when the values change - note that the date & numeric filter - // views do not trigger a 'changeSelection' event, (because they are not based - // on a SearchSelect View) but update the models directly - this.listenTo( - view.valueSelect, - 'changeSelection', - this.handleValueChange - ); - - // Update the model when the operator changes - this.listenTo( - view.valueSelect, - 'separatorChanged', - function (newOperator) { - this.model.set("operator", newOperator) + categoriesMatch = thisInterface.categories.includes(category); } + return typesMatch && categoriesMatch && namesMatch; + }, + ); + + this.valueSelect = interfaceProperties.uiFunction.call(this); + if (interfaceProperties.label && interfaceProperties.label.length) { + label = $( + "<p class='subtle searchable-select-label'>" + + interfaceProperties.label + + "</p>", ); - - // Show a message that reminds the user that capitalization matters when they - // are typing a value for a field that is case-sensitive. - this.listenTo( - view.valueSelect, - 'inputFocus', - function(event){ - var fields = this.model.get("fields"); - var isCaseSensitive = _.some(fields, function(field){ - return MetacatUI.queryFields.findWhere({ - name: field, - caseSensitive: true - }); - }) - if(isCaseSensitive){ - var fieldsText = "The field" - if(fields.length > 1){ - fieldsText = "At least one of the fields" - } - var message = "<i class='icon-lightbulb icon-on-left'></i> <b>Hint:</b> " + - fieldsText + - " you selected is case-sensitive. Capitalization matters here." - view.valueSelect.showMessage(message, type = "info", removeOnChange = false) - } else { - view.valueSelect.removeMessages() - } - } - ) - - // Set the value to the value provided if there was one. Then validateValue() - } catch (e) { - console.error("Error adding a search value input in the Query Rule View," + - " error message:", e); } - }, - /** - * handleValueChange - Called when the select values for rule are changed. Updates - * the model. - * - * @param {string[]} newValues The new values that were selected - */ - handleValueChange: function (newValues) { - - try { - // TODO: validate values - - // Don't add empty values to the model - newValues = _.reject(newValues, function (val) { return val === "" }); - this.model.set("values", newValues); - } catch (e) { - console.error("Failed to handle a change in select values in the Query Ryle" + - " View, error message: " + e); + // Append and render the chosen value selector + this.el.append(view.valueSelect.el); + this.valueSelect.$el.addClass(this.valuesClass); + view.valueSelect.render(); + if (label) { + view.valueSelect.$el.prepend(label); } - }, - // /** - // * Ensure the value entered is valid, given the metadata field selected. - // * If it's not, show an error. If it is, remove the error if there was one. - // * - // * @return {type} description - // */ - // validateValue: function() {// TODO - // }, - - /** - * Remove one of the three input fields from the rule - * - * @param {string} inputType Which of the inputs to remove? "field", "operator", - * or "value" - */ - removeInput: function (inputType) { - try { - // TODO - what, if any, model updates should happen here? - switch (inputType) { - case "value": - if (this.valueSelect) { - this.stopListening( this.valueSelect, 'changeSelection inputFocus' ); - this.valueSelect.remove(); - this.valueSelect = null; - } - break; - case "operator": - if (this.operatorSelect) { - this.stopListening(this.operatorSelect, 'changeSelection'); - this.operatorSelect.remove(); - this.operatorSelect = null; - } - break; - case "field": - if (this.fieldSelect) { - this.stopListening(this.fieldSelect, 'changeSelection'); - this.fieldSelect.remove(); - this.fieldSelect = null; - } - break; - default: - console.error("Must specify either value, operator, or field in the" + - " removeInput function in the Query Rule View") + // Make sure the listeners set below are not set multiple times + this.stopListening( + view.valueSelect, + "changeSelection inputFocus separatorChanged", + ); + + // Update model when the values change - note that the date & numeric filter + // views do not trigger a 'changeSelection' event, (because they are not based + // on a SearchSelect View) but update the models directly + this.listenTo( + view.valueSelect, + "changeSelection", + this.handleValueChange, + ); + + // Update the model when the operator changes + this.listenTo( + view.valueSelect, + "separatorChanged", + function (newOperator) { + this.model.set("operator", newOperator); + }, + ); + + // Show a message that reminds the user that capitalization matters when they + // are typing a value for a field that is case-sensitive. + this.listenTo(view.valueSelect, "inputFocus", function (event) { + var fields = this.model.get("fields"); + var isCaseSensitive = _.some(fields, function (field) { + return MetacatUI.queryFields.findWhere({ + name: field, + caseSensitive: true, + }); + }); + if (isCaseSensitive) { + var fieldsText = "The field"; + if (fields.length > 1) { + fieldsText = "At least one of the fields"; + } + var message = + "<i class='icon-lightbulb icon-on-left'></i> <b>Hint:</b> " + + fieldsText + + " you selected is case-sensitive. Capitalization matters here."; + view.valueSelect.showMessage( + message, + (type = "info"), + (removeOnChange = false), + ); + } else { + view.valueSelect.removeMessages(); } - } catch (e) { - console.error("Error removing an input from the Query Rule View, error" + - " message:", e); + }); + + // Set the value to the value provided if there was one. Then validateValue() + } catch (e) { + console.error( + "Error adding a search value input in the Query Rule View," + + " error message:", + e, + ); + } + }, + + /** + * handleValueChange - Called when the select values for rule are changed. Updates + * the model. + * + * @param {string[]} newValues The new values that were selected + */ + handleValueChange: function (newValues) { + try { + // TODO: validate values + + // Don't add empty values to the model + newValues = _.reject(newValues, function (val) { + return val === ""; + }); + this.model.set("values", newValues); + } catch (e) { + console.error( + "Failed to handle a change in select values in the Query Ryle" + + " View, error message: " + + e, + ); + } + }, + + // /** + // * Ensure the value entered is valid, given the metadata field selected. + // * If it's not, show an error. If it is, remove the error if there was one. + // * + // * @return {type} description + // */ + // validateValue: function() {// TODO + // }, + + /** + * Remove one of the three input fields from the rule + * + * @param {string} inputType Which of the inputs to remove? "field", "operator", + * or "value" + */ + removeInput: function (inputType) { + try { + // TODO - what, if any, model updates should happen here? + switch (inputType) { + case "value": + if (this.valueSelect) { + this.stopListening( + this.valueSelect, + "changeSelection inputFocus", + ); + this.valueSelect.remove(); + this.valueSelect = null; + } + break; + case "operator": + if (this.operatorSelect) { + this.stopListening(this.operatorSelect, "changeSelection"); + this.operatorSelect.remove(); + this.operatorSelect = null; + } + break; + case "field": + if (this.fieldSelect) { + this.stopListening(this.fieldSelect, "changeSelection"); + this.fieldSelect.remove(); + this.fieldSelect = null; + } + break; + default: + console.error( + "Must specify either value, operator, or field in the" + + " removeInput function in the Query Rule View", + ); } - }, - - /** - * Indicate to the user that the rule will be removed when they hover over the - * remove button. - */ - previewRemove: function (e) { - try { - - var normalOpacity = 1.0, - previewOpacity = 0.2, - speed = 175; - - var removeEl = e.target; - var subElements = this.$el.children().not(removeEl); - - if(e.type === "mouseover"){ - subElements.fadeTo(speed, previewOpacity) - $(removeEl).fadeTo(speed, normalOpacity) - } - if(e.type === "mouseout"){ - subElements.fadeTo(speed, normalOpacity) - $(removeEl).fadeTo(speed, previewOpacity) - } - - } catch (error) { - console.log("Error showing a preview of the removal of a Query Rule View," + - " details: " + error); + } catch (e) { + console.error( + "Error removing an input from the Query Rule View, error" + + " message:", + e, + ); + } + }, + + /** + * Indicate to the user that the rule will be removed when they hover over the + * remove button. + */ + previewRemove: function (e) { + try { + var normalOpacity = 1.0, + previewOpacity = 0.2, + speed = 175; + + var removeEl = e.target; + var subElements = this.$el.children().not(removeEl); + + if (e.type === "mouseover") { + subElements.fadeTo(speed, previewOpacity); + $(removeEl).fadeTo(speed, normalOpacity); } - }, - - /** - * removeSelf - When the delete button is clicked, remove this entire View and - * associated model - */ - removeSelf: function () { - try { - $("body .popover").remove(); - $("body .tooltip").remove(); - if (this.model && this.model.collection) { - this.model.collection.remove(this.model); - } - this.remove(); - } catch (error) { - console.log("Error removing a Query Rule View, details: " + error); + if (e.type === "mouseout") { + subElements.fadeTo(speed, normalOpacity); + $(removeEl).fadeTo(speed, previewOpacity); } - }, - - }); - }); + } catch (error) { + console.log( + "Error showing a preview of the removal of a Query Rule View," + + " details: " + + error, + ); + } + }, + + /** + * removeSelf - When the delete button is clicked, remove this entire View and + * associated model + */ + removeSelf: function () { + try { + $("body .popover").remove(); + $("body .tooltip").remove(); + if (this.model && this.model.collection) { + this.model.collection.remove(this.model); + } + this.remove(); + } catch (error) { + console.log("Error removing a Query Rule View, details: " + error); + } + }, + }, + ); +});

diff --git a/docs/docs/src_js_views_searchSelect_AccountSelectView.js.html b/docs/docs/src_js_views_searchSelect_AccountSelectView.js.html index 3d8e89968..8d9e0ccf4 100644 --- a/docs/docs/src_js_views_searchSelect_AccountSelectView.js.html +++ b/docs/docs/src_js_views_searchSelect_AccountSelectView.js.html @@ -45,113 +45,110 @@

Source: src/js/views/searchSelect/AccountSelectView.js
define([
-    "jquery",
-    "underscore",
-    "backbone",
-    "views/searchSelect/SearchableSelectView",
-    "models/LookupModel"
-  ],
-  function($, _, Backbone,  SearchableSelect, LookupModel) {
-
-    /**
-     * @class AccountSelectView
-     * @classdesc A select interface that allows the user to search for and select one or
-     * more accountIDs
-     * @classcategory Views/SearchSelect
-     * @extends SearchableSelect
-     * @constructor
-     * @since 2.15.0
-     * @screenshot views/searchSelect/AccountSelectViewView.png
-     */
-    var AccountSelectView = SearchableSelect.extend(
-      /** @lends AccountSelectViewView.prototype */
-      {
-        /**
-         * The type of View this is
-         * @type {string}
-         * @since 2.15.0
-         */
-        type: "AccountSelect",
-
-        /**
-         * The HTML class names for this view element
-         * @type {string}
-         * @since 2.15.0
-         */
-        className: SearchableSelect.prototype.className + " account-select",
-
-        /**
-         * Text to show in the input field before any value has been entered
-         * @type {string}
-         * @default "Start typing a name"
-         * @since 2.15.0
-         */
-        placeholderText: "Start typing a name",
-
-        /**
-         * Label for the input element
-         * @type {string}
-         * @default "Search for a person or group"
-         * @since 2.15.0
-         */
-        inputLabel: "Search for a person or group",
-
-        /**
-         * Whether to allow users to select more than one value
-         * @type {boolean}
-         * @default true
-         * @since 2.15.0
-         */
-        allowMulti: true,
-
-        /**
-         * Setting to true gives users the ability to add their own options that
-         * are not listed in this.options. This can work with either single
-         * or multiple search select dropdowns
-         * @type {boolean}
-         * @default true
-         */
-        allowAdditions: true,
-
-        /**
-         * Can be set to an object to specify API settings for retrieving remote selection
-         * menu content from an API endpoint. Details of what can be set here are
-         * specified by the Semantic-UI / Fomantic-UI package. Set to false if not
-         * retrieving remote content.
-         * @type {Object|booealn}
-         * @since 2.15.0
-         * @see {@link https://fomantic-ui.com/modules/dropdown.html#remote-settings}
-         * @see {@link https://fomantic-ui.com/behaviors/api.html#/settings}
-         */
-        apiSettings: {
-
-          // Use the Accounts lookup to search for a person or group when the user types
-          // something into the input
-          responseAsync: function(settings, callback){
-
-            var view = $(this).data("view");
-
-            // The search term that the user has typed into the input
-            var searchTerm = settings.urlData.query
-
-            // Only use the account lookup service is the user has typed at least two
-            // characters. Otherwise, the callback function is never called.
-            if(searchTerm.length < 2){
-              callback({
-                success: false,
-              })
-              return
-            }
-
-            // For search terms at least 2 characters long, use the Lookup Model
-            MetacatUI.appLookupModel.getAccountsAutocomplete({ term: searchTerm }, function(results){
+  "jquery",
+  "underscore",
+  "backbone",
+  "views/searchSelect/SearchableSelectView",
+  "models/LookupModel",
+], function ($, _, Backbone, SearchableSelect, LookupModel) {
+  /**
+   * @class AccountSelectView
+   * @classdesc A select interface that allows the user to search for and select one or
+   * more accountIDs
+   * @classcategory Views/SearchSelect
+   * @extends SearchableSelect
+   * @constructor
+   * @since 2.15.0
+   * @screenshot views/searchSelect/AccountSelectViewView.png
+   */
+  var AccountSelectView = SearchableSelect.extend(
+    /** @lends AccountSelectViewView.prototype */
+    {
+      /**
+       * The type of View this is
+       * @type {string}
+       * @since 2.15.0
+       */
+      type: "AccountSelect",
+
+      /**
+       * The HTML class names for this view element
+       * @type {string}
+       * @since 2.15.0
+       */
+      className: SearchableSelect.prototype.className + " account-select",
+
+      /**
+       * Text to show in the input field before any value has been entered
+       * @type {string}
+       * @default "Start typing a name"
+       * @since 2.15.0
+       */
+      placeholderText: "Start typing a name",
+
+      /**
+       * Label for the input element
+       * @type {string}
+       * @default "Search for a person or group"
+       * @since 2.15.0
+       */
+      inputLabel: "Search for a person or group",
+
+      /**
+       * Whether to allow users to select more than one value
+       * @type {boolean}
+       * @default true
+       * @since 2.15.0
+       */
+      allowMulti: true,
+
+      /**
+       * Setting to true gives users the ability to add their own options that
+       * are not listed in this.options. This can work with either single
+       * or multiple search select dropdowns
+       * @type {boolean}
+       * @default true
+       */
+      allowAdditions: true,
+
+      /**
+       * Can be set to an object to specify API settings for retrieving remote selection
+       * menu content from an API endpoint. Details of what can be set here are
+       * specified by the Semantic-UI / Fomantic-UI package. Set to false if not
+       * retrieving remote content.
+       * @type {Object|booealn}
+       * @since 2.15.0
+       * @see {@link https://fomantic-ui.com/modules/dropdown.html#remote-settings}
+       * @see {@link https://fomantic-ui.com/behaviors/api.html#/settings}
+       */
+      apiSettings: {
+        // Use the Accounts lookup to search for a person or group when the user types
+        // something into the input
+        responseAsync: function (settings, callback) {
+          var view = $(this).data("view");
+
+          // The search term that the user has typed into the input
+          var searchTerm = settings.urlData.query;
+
+          // Only use the account lookup service is the user has typed at least two
+          // characters. Otherwise, the callback function is never called.
+          if (searchTerm.length < 2) {
+            callback({
+              success: false,
+            });
+            return;
+          }
 
+          // For search terms at least 2 characters long, use the Lookup Model
+          MetacatUI.appLookupModel.getAccountsAutocomplete(
+            { term: searchTerm },
+            function (results) {
               // If no match was found to the search term
-              if(results && results.length < 1){
+              if (results && results.length < 1) {
                 callback({
                   success: false,
-                })
-                return
+                });
+                return;
               }
 
               // If there were results, format for the semantic UI dropdown function
@@ -159,236 +156,232 @@ 

Source: src/js/views/searchSelect/AccountSelectView.js', + ); + result.label = result.label.replace(")", "</span>"); + } - SearchableSelect.prototype.initialize.call(view, options); + var icon = ""; - } catch (e) { - console.log("Failed to initialize an Account Select view, error message:", - e); + if (result.type === "person") { + icon = "user"; + } + if (result.type === "group") { + icon = "group"; } - }, - - /** - * Takes the results returned from from - * {@link LookupModel#getAccountsAutocomplete} and re-formats them for other - * functions. When the forTemplate argument is false, the results are formatted as - * a list mapping dropdown content specifically for the FomanticUI API. When - * forTemplate is true, then the results are formatted for use in the - * SearchableSelectView template: {@link SearchableSelectView#template}. - * - * @param {Object[]} results The response from - * {@link LookupModel#getAccountsAutocomplete} - * @param {boolean} forTemplate=false Whether or not to format the results for - * the {@link SearchableSelectView#template} - * @return {Object[]} The re-formatted results - * - * @see {@link https://fomantic-ui.com/modules/dropdown.html#remote-settings} - * @since 2.15.0 - */ - formatResults: function(results, forTemplate = false){ - - results = _.map(results, function(result){ - - if(forTemplate){ - // Get the ID which is saved in the parentheses - var regExp = /\(([^)]+)\)$/; - var matches = regExp.exec(result.label); - //matches[1] contains the value between the parentheses - result.description = "Account ID: " + matches[1]; - result.label = result.label.replace(regExp, "") - result.label = result.label.trim() + if (icon) { + if (forTemplate) { + result.icon = icon; } else { - result.label = result.label.replace("(", '<span class="description">') - result.label = result.label.replace(")", '</span>') - } - - var icon = "" - - if(result.type === "person"){ - icon = "user" - } - if(result.type === "group"){ - icon = "group" - } - - if(icon){ - if(forTemplate){ - result.icon = icon; - } else { - result.label = '<i class="icon icon-on-left icon-' + icon + '"></i>' + result.label - } - } - - if(!forTemplate) { - result = { - name: result.label, - value: result.value, - } + result.label = + '<i class="icon icon-on-left icon-' + + icon + + '"></i>' + + result.label; } - return result - - }) - - return results - }, - - /** - * Render the view - * - * @return {AccountSelectView} Returns the view - * @since 2.15.0 - */ - render: function(){ - - var view = this; - - // Use the account lookup service to match the pre-selected values to - // the account holder's name to use as a label. - - // If we haven't started looking up user/organization names yet... - if(typeof view.labelsToFetch === "undefined"){ - - // Keep a count of the number of accounts we need to lookup - view.labelsToFetch = this.selected ? this.selected.length : 0; - - if(view.labelsToFetch > 0){ - - view.options = []; - - view.selected.forEach(function(accountId){ - MetacatUI.appLookupModel.getAccountsAutocomplete({ term: accountId }, function(results){ + } + if (!forTemplate) { + result = { + name: result.label, + value: result.value, + }; + } + return result; + }); + + return results; + }, + + /** + * Render the view + * + * @return {AccountSelectView} Returns the view + * @since 2.15.0 + */ + render: function () { + var view = this; + + // Use the account lookup service to match the pre-selected values to + // the account holder's name to use as a label. + + // If we haven't started looking up user/organization names yet... + if (typeof view.labelsToFetch === "undefined") { + // Keep a count of the number of accounts we need to lookup + view.labelsToFetch = this.selected ? this.selected.length : 0; + + if (view.labelsToFetch > 0) { + view.options = []; + + view.selected.forEach(function (accountId) { + MetacatUI.appLookupModel.getAccountsAutocomplete( + { term: accountId }, + function (results) { // The value should match only one account (since the value is an // account ID). If we found the match, format it for the // SearchableSelectView, and the icon and tooltip will automatically be // added to the pre-selected labels. - if(results && results.length === 1){ + if (results && results.length === 1) { results = view.formatResults(results, true); - view.options.push(results[0]) + view.options.push(results[0]); } // Whether or not we found a match, count this lookup as complete - --view.labelsToFetch + --view.labelsToFetch; // Once we've looked up all of the accounts, call this render function // again - if(view.labelsToFetch === 0 ){ + if (view.labelsToFetch === 0) { view.render(); } + }, + ); + }); - }) - }) - - return - - } - + return; + } + } + + // Once we've fetched the labels for any pre-selected account IDs, + // render as usual + SearchableSelect.prototype.render.call(view); + }, + + /** + * addTooltip - Add a tooltip to a given element using the description in the + * options object that's set on the view. + * + * @param {HTMLElement} element The HTML element a tooltip should be added + * @param {string} position how to position the tooltip - top | bottom | left | + * right + * @return {jQuery} The element with a tooltip wrapped by jQuery + * @since 2.15.0 + */ + addTooltip: function (element, position = "bottom") { + try { + if (!element) { + return; } - // Once we've fetched the labels for any pre-selected account IDs, - // render as usual - SearchableSelect.prototype.render.call(view); - - }, - - /** - * addTooltip - Add a tooltip to a given element using the description in the - * options object that's set on the view. - * - * @param {HTMLElement} element The HTML element a tooltip should be added - * @param {string} position how to position the tooltip - top | bottom | left | - * right - * @return {jQuery} The element with a tooltip wrapped by jQuery - * @since 2.15.0 - */ - addTooltip: function(element, position = "bottom"){ - - try { - if(!element){ - return - } - - // The account ID is saved in a <span> element in the label with the - // description class when the label is added from the list of search results - var descEl = $(element).find(".description"); - var id = descEl.text(); - descEl.remove(); + // The account ID is saved in a <span> element in the label with the + // description class when the label is added from the list of search results + var descEl = $(element).find(".description"); + var id = descEl.text(); + descEl.remove(); - // Otherwise, the ID is always saved as a data attribute - if(!id){ - id = $(element).attr("data-value") - } + // Otherwise, the ID is always saved as a data attribute + if (!id) { + id = $(element).attr("data-value"); + } - // Show the account ID as a tooltip rather than in the label. Otherwise - // the input gets too crowded. - $(element).tooltip({ + // Show the account ID as a tooltip rather than in the label. Otherwise + // the input gets too crowded. + $(element) + .tooltip({ title: id ? "Account ID: " + id : "", placement: position, container: "body", delay: { show: 900, - hide: 50 - } + hide: 50, + }, }) - .on("show.bs.popover", - function(){ - var $el = $(this); - // Allow time for the popup to be added to the DOM - setTimeout(function () { - // Then add a special class to identify - // these popups if they need to be removed. - $el.data('tooltip').$tip.addClass("search-select-tooltip") - }, 10); + .on("show.bs.popover", function () { + var $el = $(this); + // Allow time for the popup to be added to the DOM + setTimeout(function () { + // Then add a special class to identify + // these popups if they need to be removed. + $el.data("tooltip").$tip.addClass("search-select-tooltip"); + }, 10); }); - return $(element) - } catch (e) { - console.log("Failed to add tooltips in a searchable select view, error message: " + e); - } - - }, - - // TODO: We may want to add a custom is valid option to warn the user when - // a value entered cannot be found in the accounts lookup service. - - // /** - // * isValidOption - Checks if a value is one of the values given in view.options - // * - // * @param {string} value The value to check - // * @return {boolean} returns true if the value is one of the values given in - // * view.options - // */ - // isValidOption: function(value){ - // }, - - - }); - - return AccountSelectView - }); + return $(element); + } catch (e) { + console.log( + "Failed to add tooltips in a searchable select view, error message: " + + e, + ); + } + }, + + // TODO: We may want to add a custom is valid option to warn the user when + // a value entered cannot be found in the accounts lookup service. + + // /** + // * isValidOption - Checks if a value is one of the values given in view.options + // * + // * @param {string} value The value to check + // * @return {boolean} returns true if the value is one of the values given in + // * view.options + // */ + // isValidOption: function(value){ + // }, + }, + ); + + return AccountSelectView; +});

diff --git a/docs/docs/src_js_views_searchSelect_AnnotationFilterView.js.html b/docs/docs/src_js_views_searchSelect_AnnotationFilterView.js.html index 718a132d7..bf9c0fd37 100644 --- a/docs/docs/src_js_views_searchSelect_AnnotationFilterView.js.html +++ b/docs/docs/src_js_views_searchSelect_AnnotationFilterView.js.html @@ -44,102 +44,99 @@

Source: src/js/views/searchSelect/AnnotationFilterView.js
-
define(
-  [
-    "jquery",
-    "underscore",
-    "backbone",
-    "bioportal",
-  ],
-  function(
-    $, _, Backbone, Bioportal,
-  ) {
-
-    /**
-     * @class AnnotationFilter
-     * @classdesc A view that renders an annotation filter interface, which uses
-     * the bioportal tree search to select ontology terms.
-     * @classcategory Views/SearchSelect
-     * @extends Backbone.View
-     * @constructor
-     * @since 2.14.0
-     * @screenshot views/searchSelect/AnnotationFilterView.png
-     */
-    return Backbone.View.extend(
-      /** @lends AnnotationFilterView.prototype */
-      {
-        /**
-         * The type of View this is
-         * @type {string}
-         */
-        type: "AnnotationFilter",
-
-        /**
-         * The HTML class names for this view element
-         * @type {string}
-         */
-        className: "filter annotation-filter",
-
-        /**
-         * The selector for the element that will show/hide the annotation
-         * popover interface when clicked. Searches within body.
-         * @type {string}
-         */
-        popoverTriggerSelector: "",
-
-        /**
-         * If set to true, instead of showing the annotation tree interface in
-         * a popover, show it in a multi-select input interface, which allows
-         * the user to select multiple annotations.
-         * @type {boolean}
-         */
-        multiselect: false,
-
-        /**
-         * If true, this filter will be added to the query but will
-         * act in the "background", like a default filter
-         * @type {boolean}
-         * @since 2.22.0
-         */
-        isInvisible: true,
-
-        /**
-         * If set to true, instead of showing the annotation tree interface in
-         * a popover, show it on the custom search filter interface, which allows
-         * the user to filter search based on the annotations.
-         * @type {boolean}
-         * @since 2.22.0
-         */
-        useSearchableSelect: false,
-
-        /**
-         * The acronym of the ontology or ontologies to render a tree from.
-         *
-         * Must be an ontology that's present on BioPortal.
-         *
-         * TODO: Test out comma-separated lists. How does that render?
-         * @type {string}
-         * @since 2.22.0
-         */
-        defaultOntology: "ECSO",
-
-        /**
-         * The URL that indicates the concept where the tree should start
-         * @type {string}
-         */
-        defaultStartingRoot: "http://ecoinformatics.org/oboe/oboe.1.2/oboe-core.owl#MeasurementType",
-
-        /**
-         * Creates a new AnnotationFilterView
-         * @param {Object} options - A literal object with options to pass to the view
-         */
-        initialize: function(options) {
-          try {
-
-            // Get all the options and apply them to this view
-            if (typeof options == "object") {
-              var optionKeys = Object.keys(options);
-              _.each(optionKeys, function(key, i) {
+            
define(["jquery", "underscore", "backbone", "bioportal"], function (
+  $,
+  _,
+  Backbone,
+  Bioportal,
+) {
+  /**
+   * @class AnnotationFilter
+   * @classdesc A view that renders an annotation filter interface, which uses
+   * the bioportal tree search to select ontology terms.
+   * @classcategory Views/SearchSelect
+   * @extends Backbone.View
+   * @constructor
+   * @since 2.14.0
+   * @screenshot views/searchSelect/AnnotationFilterView.png
+   */
+  return Backbone.View.extend(
+    /** @lends AnnotationFilterView.prototype */
+    {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "AnnotationFilter",
+
+      /**
+       * The HTML class names for this view element
+       * @type {string}
+       */
+      className: "filter annotation-filter",
+
+      /**
+       * The selector for the element that will show/hide the annotation
+       * popover interface when clicked. Searches within body.
+       * @type {string}
+       */
+      popoverTriggerSelector: "",
+
+      /**
+       * If set to true, instead of showing the annotation tree interface in
+       * a popover, show it in a multi-select input interface, which allows
+       * the user to select multiple annotations.
+       * @type {boolean}
+       */
+      multiselect: false,
+
+      /**
+       * If true, this filter will be added to the query but will
+       * act in the "background", like a default filter
+       * @type {boolean}
+       * @since 2.22.0
+       */
+      isInvisible: true,
+
+      /**
+       * If set to true, instead of showing the annotation tree interface in
+       * a popover, show it on the custom search filter interface, which allows
+       * the user to filter search based on the annotations.
+       * @type {boolean}
+       * @since 2.22.0
+       */
+      useSearchableSelect: false,
+
+      /**
+       * The acronym of the ontology or ontologies to render a tree from.
+       *
+       * Must be an ontology that's present on BioPortal.
+       *
+       * TODO: Test out comma-separated lists. How does that render?
+       * @type {string}
+       * @since 2.22.0
+       */
+      defaultOntology: "ECSO",
+
+      /**
+       * The URL that indicates the concept where the tree should start
+       * @type {string}
+       */
+      defaultStartingRoot:
+        "http://ecoinformatics.org/oboe/oboe.1.2/oboe-core.owl#MeasurementType",
+
+      /**
+       * Creates a new AnnotationFilterView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            var optionKeys = Object.keys(options);
+            _.each(
+              optionKeys,
+              function (key, i) {
                 // Only override non-null values so we can pass in nulls and
                 // still trigger default behavior
                 if (typeof options[key] === "undefined") {
@@ -147,280 +144,313 @@ 

Source: src/js/views/searchSelect/AnnotationFilterView.js } this[key] = options[key]; - }, this); - } - - // Mix in defaults if needed - if (!this.ontology) { - this.ontology = this.defaultOntology; - this.startingRoot = this.defaultStartingRoot; - } - - } catch (e) { - console.log("Failed to initialize an Annotation Filter View, error message:", e); + }, + this, + ); } - }, - - /** - * render - Render the view - * - * @return {AnnotationFilter} Returns the view - */ - render: function() { - try { - - if(!MetacatUI.appModel.get("bioportalAPIKey")){ - console.log("A bioportal key is required for the Annotation Filter View. Please set a key in the MetacatUI config. The view will not render."); - return - } - - var view = this; - if(view.multiselect || view.useSearchableSelect){ - view.createMultiselect() - } else { - view.setUpTree() - view.createPopoverHTML() - view.setListeners() - } - - return this - - } catch (e) { - console.log("Failed to render an Annotation Filter View, error message: " + e); + // Mix in defaults if needed + if (!this.ontology) { + this.ontology = this.defaultOntology; + this.startingRoot = this.defaultStartingRoot; } - }, - - /** - * setUpTree - Create the HTML for the annotation tree - */ - setUpTree: function() { - - try { - - var view = this; - - view.treeEl = $('<div id="bioportal-tree"></div>').NCBOTree({ - apikey: MetacatUI.appModel.get("bioportalAPIKey"), - ontology: view.ontology, - width: "400", - startingRoot: view.startingRoot - }); - - // Make an element that contains the tree and reset/jumpUp buttons - var buttonProps = "data-trigger='hover' data-placement='top' data-container='body' style='margin-right: 3px'" - view.treeContent = $("<div></div>"); - view.buttonContainer = $('<div class="ncbo-tree-buttons-container"></div>'); - view.jumpUpButton = $("<button class='icon icon-level-up tooltip-this btn' id='jumpUp' data-title='Go up to parent' " + buttonProps + " ></button>"); - view.resetButton = $("<button class='icon icon-undo tooltip-this btn' id='resetTree' data-title='Reset tree' " + buttonProps + " ></button>"); - $(view.buttonContainer).append(view.jumpUpButton); - $(view.buttonContainer).append(view.resetButton); - $(view.treeContent).append(view.buttonContainer); - $(view.treeContent).append(view.treeEl); - - } catch (e) { - console.log("Failed to set up an annotation tree, error message: " + e); + } catch (e) { + console.log( + "Failed to initialize an Annotation Filter View, error message:", + e, + ); + } + }, + + /** + * render - Render the view + * + * @return {AnnotationFilter} Returns the view + */ + render: function () { + try { + if (!MetacatUI.appModel.get("bioportalAPIKey")) { + console.log( + "A bioportal key is required for the Annotation Filter View. Please set a key in the MetacatUI config. The view will not render.", + ); + return; } - }, - - /** - * createMultiselect - Create a searchable multi-select interface - * that includes an annotation filter tree. - */ - createMultiselect: function(){ - - try { - var view = this; - - require(["views/searchSelect/SearchableSelectView"], function(SearchableSelect){ - - view.multiSelectView = new SearchableSelect({ - placeholderText: view.placeholderText ? view.placeholderText : "Search for or select a value", - icon: view.icon, - separatorText: view.separatorText, - inputLabel: view.inputLabel - }) - view.$el.append(view.multiSelectView.el); - view.multiSelectView.render(); - // If there are pre-selected values, get the user-facing labels - // and then update the multiselect - if(view.selected && view.selected.length){ - view.getClassLabels.call(view, view.updateMultiselect); - } else { - // Otherwise, update the multi-select right away with tree element - view.updateMultiselect.call(view) - } - - //Forward the separatorChanged event from the SearchableSelectView to this AnnotationFilterView - //(perhaps this view should have been a subclass?) - view.multiSelectView.on("separatorChanged", (separatorText) => { - view.trigger("separatorChanged", separatorText) - }) - }) - } catch (e) { - console.log("Failed to create the multi-select interface for an Annotation Filter View, error message: " + e); - } - }, - - /** - * updateMultiselect - Functions to run once a SearchableSelect view has - * been rendered and inserted into this view, and the labels for any - * pre-selected annotation values have been fetched. Updates the - * hidden menu of items and the selected items. - */ - updateMultiselect: function(){ - - try { - var view = this; - - // Check if this is the first time we are updating this multiselect. - // If it is, then don't trigger the event that updates the model, - // because nothing has changed. - if(view.updateMultiselectTimes === undefined){ - view.updateMultiselectTimes = 0 - } else { - view.updateMultiselectTimes++ - } + var view = this; - // Re-init the tree + if (view.multiselect || view.useSearchableSelect) { + view.createMultiselect(); + } else { view.setUpTree(); - - // Re-render the multiselect menu with the new options. These options - // will be hidden from view, but they must be present in the DOM for - // the multi-select interface to function correctly. - // Add an empty item to the list of selected values, so that - // the dropdown menu is always expandable. - if(view.options === undefined){ - view.options = [] - } - view.options.push({value:""}); - view.multiSelectView.options = view.options; - view.multiSelectView.updateMenu(); - // Make sure the new menu is attached before updating list of selected - // annotations - setTimeout(function () { - var silent = view.updateMultiselectTimes === 0; - var newValues = _.reject(view.selected, function(val){ return val === "" }); - view.multiSelectView.changeSelection(newValues, silent); - }, 25); - - // Add the annotation tree to the menu content - view.multiSelectView.$el.find(".menu").append(view.treeContent); - view.searchInput = view.multiSelectView.$selectUI.find("input"); - - // Simulate a search in the annotation tree when the user - // searches in the multiSelect interface - view.searchInput.on("keyup", function(e){ - var treeInput = view.treeContent.find("input.ncboAutocomplete"); - treeInput.val(e.target.value).keydown(); - }); - + view.createPopoverHTML(); view.setListeners(); - - } catch (e) { - console.log("Failed to update an annotation filter with selected values, error message: " + e); } - }, - - /** - * getClassLabels - Given an array of bioontology IDs set in - * view.selected, query the bioontology API to find the user-friendly - * labels (prefLabels) - * - * @param {function} callback A function to call once the labels have - * been found (or not). The function will be called with the formatted - * response: an array with an object for each ID with the properties - * value (the original ID) and label (the user-friendly label, or the - * value again if no label was found) - */ - getClassLabels: function(callback){ - - try { - var view = this; - - if(!view.selected || !view.selected.length){ - return + return this; + } catch (e) { + console.log( + "Failed to render an Annotation Filter View, error message: " + e, + ); + } + }, + + /** + * setUpTree - Create the HTML for the annotation tree + */ + setUpTree: function () { + try { + var view = this; + + view.treeEl = $('<div id="bioportal-tree"></div>').NCBOTree({ + apikey: MetacatUI.appModel.get("bioportalAPIKey"), + ontology: view.ontology, + width: "400", + startingRoot: view.startingRoot, + }); + + // Make an element that contains the tree and reset/jumpUp buttons + var buttonProps = + "data-trigger='hover' data-placement='top' data-container='body' style='margin-right: 3px'"; + view.treeContent = $("<div></div>"); + view.buttonContainer = $( + '<div class="ncbo-tree-buttons-container"></div>', + ); + view.jumpUpButton = $( + "<button class='icon icon-level-up tooltip-this btn' id='jumpUp' data-title='Go up to parent' " + + buttonProps + + " ></button>", + ); + view.resetButton = $( + "<button class='icon icon-undo tooltip-this btn' id='resetTree' data-title='Reset tree' " + + buttonProps + + " ></button>", + ); + $(view.buttonContainer).append(view.jumpUpButton); + $(view.buttonContainer).append(view.resetButton); + $(view.treeContent).append(view.buttonContainer); + $(view.treeContent).append(view.treeEl); + } catch (e) { + console.log( + "Failed to set up an annotation tree, error message: " + e, + ); + } + }, + + /** + * createMultiselect - Create a searchable multi-select interface + * that includes an annotation filter tree. + */ + createMultiselect: function () { + try { + var view = this; + + require(["views/searchSelect/SearchableSelectView"], function ( + SearchableSelect, + ) { + view.multiSelectView = new SearchableSelect({ + placeholderText: view.placeholderText + ? view.placeholderText + : "Search for or select a value", + icon: view.icon, + separatorText: view.separatorText, + inputLabel: view.inputLabel, + }); + view.$el.append(view.multiSelectView.el); + view.multiSelectView.render(); + // If there are pre-selected values, get the user-facing labels + // and then update the multiselect + if (view.selected && view.selected.length) { + view.getClassLabels.call(view, view.updateMultiselect); + } else { + // Otherwise, update the multi-select right away with tree element + view.updateMultiselect.call(view); } - const ontologyCollection = _.map(view.selected, function(id){ - return { - "class" : id, - "ontology": "http://data.bioontology.org/ontologies/" + view.ontology - } + //Forward the separatorChanged event from the SearchableSelectView to this AnnotationFilterView + //(perhaps this view should have been a subclass?) + view.multiSelectView.on("separatorChanged", (separatorText) => { + view.trigger("separatorChanged", separatorText); }); + }); + } catch (e) { + console.log( + "Failed to create the multi-select interface for an Annotation Filter View, error message: " + + e, + ); + } + }, + + /** + * updateMultiselect - Functions to run once a SearchableSelect view has + * been rendered and inserted into this view, and the labels for any + * pre-selected annotation values have been fetched. Updates the + * hidden menu of items and the selected items. + */ + updateMultiselect: function () { + try { + var view = this; + + // Check if this is the first time we are updating this multiselect. + // If it is, then don't trigger the event that updates the model, + // because nothing has changed. + if (view.updateMultiselectTimes === undefined) { + view.updateMultiselectTimes = 0; + } else { + view.updateMultiselectTimes++; + } - const bioData = JSON.stringify({ - "http://www.w3.org/2002/07/owl#Class": { - "collection": ontologyCollection, - "display": "prefLabel" - } + // Re-init the tree + view.setUpTree(); + + // Re-render the multiselect menu with the new options. These options + // will be hidden from view, but they must be present in the DOM for + // the multi-select interface to function correctly. + // Add an empty item to the list of selected values, so that + // the dropdown menu is always expandable. + if (view.options === undefined) { + view.options = []; + } + view.options.push({ value: "" }); + view.multiSelectView.options = view.options; + view.multiSelectView.updateMenu(); + // Make sure the new menu is attached before updating list of selected + // annotations + setTimeout(function () { + var silent = view.updateMultiselectTimes === 0; + var newValues = _.reject(view.selected, function (val) { + return val === ""; }); + view.multiSelectView.changeSelection(newValues, silent); + }, 25); + + // Add the annotation tree to the menu content + view.multiSelectView.$el.find(".menu").append(view.treeContent); + view.searchInput = view.multiSelectView.$selectUI.find("input"); + + // Simulate a search in the annotation tree when the user + // searches in the multiSelect interface + view.searchInput.on("keyup", function (e) { + var treeInput = view.treeContent.find("input.ncboAutocomplete"); + treeInput.val(e.target.value).keydown(); + }); + + view.setListeners(); + } catch (e) { + console.log( + "Failed to update an annotation filter with selected values, error message: " + + e, + ); + } + }, + + /** + * getClassLabels - Given an array of bioontology IDs set in + * view.selected, query the bioontology API to find the user-friendly + * labels (prefLabels) + * + * @param {function} callback A function to call once the labels have + * been found (or not). The function will be called with the formatted + * response: an array with an object for each ID with the properties + * value (the original ID) and label (the user-friendly label, or the + * value again if no label was found) + */ + getClassLabels: function (callback) { + try { + var view = this; + + if (!view.selected || !view.selected.length) { + return; + } - const formatResponse = function(response, success){ - if(view.options === undefined){ - view.options = [] - } - view.selected.forEach(function(item,index){ - if(success){ - var match = _.findWhere(response[Object.keys(response)[0]], { "@id": item }); - } else { - var match = null; - } - view.options[index] = { - value: item, - label: match ? match.prefLabel : item - } - }) + const ontologyCollection = _.map(view.selected, function (id) { + return { + class: id, + ontology: + "http://data.bioontology.org/ontologies/" + view.ontology, + }; + }); + + const bioData = JSON.stringify({ + "http://www.w3.org/2002/07/owl#Class": { + collection: ontologyCollection, + display: "prefLabel", + }, + }); + + const formatResponse = function (response, success) { + if (view.options === undefined) { + view.options = []; } - - // Get the pre-selected values - $.ajax({ - type: "POST", - url: "http://data.bioontology.org/batch?display_context=false", - headers: { - "Authorization" : "apikey token=" + MetacatUI.appModel.get("bioportalAPIKey"), - "Accept" : "application/json", - "Content-Type" : "application/json" - }, - processData: false, - data: bioData, - crossDomain: true, - timeout: 5000, - success: function(response) { - formatResponse(response, true) - callback.call(view) - }, - error: function(response) { - console.log("Error finding class labels for the Annotation Filter, error response:", response); - formatResponse(response, false) - callback.call(view) + view.selected.forEach(function (item, index) { + if (success) { + var match = _.findWhere(response[Object.keys(response)[0]], { + "@id": item, + }); + } else { + var match = null; } + view.options[index] = { + value: item, + label: match ? match.prefLabel : item, + }; }); - } catch (e) { - console.log("Failed to fetch labels for bioontology IDs, error message: " + e); - } - - }, - - /** - * createPopoverHTML - Create the HTML for annotation filters that are - * displayed as a popup (e.g. in the search catalog) - * - * @return {type} description - */ - createPopoverHTML: function(){ - try { - var view = this; - $("body").append($('<div id="bioportal-popover" data-category="annotation"></div>')); - $(view.popoverTriggerSelector).popover({ + }; + + // Get the pre-selected values + $.ajax({ + type: "POST", + url: "http://data.bioontology.org/batch?display_context=false", + headers: { + Authorization: + "apikey token=" + MetacatUI.appModel.get("bioportalAPIKey"), + Accept: "application/json", + "Content-Type": "application/json", + }, + processData: false, + data: bioData, + crossDomain: true, + timeout: 5000, + success: function (response) { + formatResponse(response, true); + callback.call(view); + }, + error: function (response) { + console.log( + "Error finding class labels for the Annotation Filter, error response:", + response, + ); + formatResponse(response, false); + callback.call(view); + }, + }); + } catch (e) { + console.log( + "Failed to fetch labels for bioontology IDs, error message: " + e, + ); + } + }, + + /** + * createPopoverHTML - Create the HTML for annotation filters that are + * displayed as a popup (e.g. in the search catalog) + * + * @return {type} description + */ + createPopoverHTML: function () { + try { + var view = this; + $("body").append( + $('<div id="bioportal-popover" data-category="annotation"></div>'), + ); + $(view.popoverTriggerSelector) + .popover({ html: true, placement: "bottom", trigger: "manual", content: view.treeContent, - container: "#bioportal-popover" - }).on("click", function() { + container: "#bioportal-popover", + }) + .on("click", function () { if ($($(this).data().popover.options.content).is(":visible")) { // Detach the tree from the popover so it doesn't get removed by Bootstrap $(this).data().popover.options.content.detach(); @@ -428,11 +458,12 @@

Source: src/js/views/searchSelect/AnnotationFilterView.js $(this).popover("hide"); } else { // Get the popover content - var content = $(this).data().popoverContent || + var content = + $(this).data().popoverContent || $(this).data().popover.options.content.detach(); // Cache it $(this).data({ - popoverContent: content + popoverContent: content, }); // Show the popover $(this).popover("show"); @@ -443,231 +474,252 @@

Source: src/js/views/searchSelect/AnnotationFilterView.js $(".tooltip-this").tooltip(); } }); - } catch (e) { - console.log("Failed to create popover HTML for an annotation filter, error message: " + e); - } - }, - - /** - * setListeners - Sets listeners on the tree elements. Must be run - * after the tree HTML is created. - */ - setListeners: function(){ - try { - var view = this; - view.treeEl.off(); - view.jumpUpButton.off(); - view.resetButton.off(); - view.treeEl.on("afterSelect", function(event, classId, prefLabel, selectedNode) { - view.selectConcept.call(view, event, classId, prefLabel, selectedNode) + } catch (e) { + console.log( + "Failed to create popover HTML for an annotation filter, error message: " + + e, + ); + } + }, + + /** + * setListeners - Sets listeners on the tree elements. Must be run + * after the tree HTML is created. + */ + setListeners: function () { + try { + var view = this; + view.treeEl.off(); + view.jumpUpButton.off(); + view.resetButton.off(); + view.treeEl.on( + "afterSelect", + function (event, classId, prefLabel, selectedNode) { + view.selectConcept.call( + view, + event, + classId, + prefLabel, + selectedNode, + ); + }, + ); + view.treeEl.on("afterJumpToClass", function (event, classId) { + view.afterJumpToClass.call(view, event, classId); + }); + view.treeEl.on("afterExpand", function () { + view.afterExpand.call(view); + }); + view.jumpUpButton.on("click", function () { + view.jumpUp.call(view); + }); + view.resetButton.on("click", function () { + view.resetTree.call(view); + }); + if (view.multiselect) { + view.treeEl.off("searchItemSelected"); + view.treeEl.on("searchItemSelected", function () { + view.searchInput.val(""); }); - view.treeEl.on("afterJumpToClass", function(event, classId) { - view.afterJumpToClass.call(view, event, classId); - }); - view.treeEl.on("afterExpand", function() { - view.afterExpand.call(view) - }); - view.jumpUpButton.on("click", function(){ - view.jumpUp.call(view); - }); - view.resetButton.on("click", function(){ - view.resetTree.call(view); - }); - if(view.multiselect){ - view.treeEl.off("searchItemSelected"); - view.treeEl.on("searchItemSelected", function(){ - view.searchInput.val("") - }); - view.stopListening(view.multiSelectView, "changeSelection"); - view.listenTo(view.multiSelectView, "changeSelection", function(newValues){ + view.stopListening(view.multiSelectView, "changeSelection"); + view.listenTo( + view.multiSelectView, + "changeSelection", + function (newValues) { // When values are removed, update the interface - if(newValues != view.selected){ + if (newValues != view.selected) { view.selected = newValues; // So that the function doesn't trigger an endless loop delete view.updateMultiselectTimes; - view.updateMultiselect() + view.updateMultiselect(); } view.trigger("changeSelection", newValues); - }) - } - } catch (e) { - console.log("Failed to set listeners in an Annotation Filter View, error message: " + e); + }, + ); } - }, - - /** - * selectConcept - Actions that are performed after the user selects - * a concept from the annotation tree interface. Triggers an event for - * any parent views, hides and resets the annotation popup. - * - * @param {object} event The "afterSelect" event - * @param {string} classId The ID for the selected concept (a URL) - * @param {string} prefLabel The label for the selected concept - * @param {jQuery} selectedNode The element that was clicked - */ - selectConcept: function(event, classId, prefLabel, selectedNode) { - - try { - - var view = this; - - // Get the concept info - var item = { - value: classId, - label: prefLabel, - filterLabel: prefLabel, - desc: "" - } - - // Trigger an event so that the parent view can update filters, etc. - view.trigger("annotationSelected", event, item); - - // Hide the popover - if(!view.multiselect){ - var annotationFilterEl = $(view.popoverTriggerSelector); - annotationFilterEl.trigger("click"); - $(selectedNode).trigger("mouseout"); - view.resetTree(); + } catch (e) { + console.log( + "Failed to set listeners in an Annotation Filter View, error message: " + + e, + ); + } + }, + + /** + * selectConcept - Actions that are performed after the user selects + * a concept from the annotation tree interface. Triggers an event for + * any parent views, hides and resets the annotation popup. + * + * @param {object} event The "afterSelect" event + * @param {string} classId The ID for the selected concept (a URL) + * @param {string} prefLabel The label for the selected concept + * @param {jQuery} selectedNode The element that was clicked + */ + selectConcept: function (event, classId, prefLabel, selectedNode) { + try { + var view = this; + + // Get the concept info + var item = { + value: classId, + label: prefLabel, + filterLabel: prefLabel, + desc: "", + }; + + // Trigger an event so that the parent view can update filters, etc. + view.trigger("annotationSelected", event, item); + + // Hide the popover + if (!view.multiselect) { + var annotationFilterEl = $(view.popoverTriggerSelector); + annotationFilterEl.trigger("click"); + $(selectedNode).trigger("mouseout"); + view.resetTree(); // Update the multi-select with the new options - } else { - view.options.push(item); - view.selected.push(item.value); - view.updateMultiselect(); - } - - // Ensure tooltips are removed - $("body > .tooltip").remove(); - - // Prevent default action - return false; - - } catch (e) { - console.log("Failed to select an annotation concept, error message: " + e); - } - - }, - - /** - * afterExpand - Actions to perform when the user expands a concept in - * the tree - */ - afterExpand: function() { - try { - // Ensure tooltips are activated - $(".tooltip-this").tooltip(); - } catch (e) { - console.log("Failed to initialize tooltips in the annotation filter, error message: " + e); - } - }, - - /** - * afterJumpToClass - Called when a user searches for and selects a - * concept from the search results - * - * @param {type} event The jump to class event - * @param {type} classId The ID for the selected concept (a URL) - */ - afterJumpToClass: function(event, classId) { - - try { - var view = this; - // Re-root the tree at this concept - var tree = view.treeEl.data("NCBOTree"); - var options = tree.options(); - $.extend(options, { - startingRoot: classId - }); - - // Force a re-render - tree.init(); - - // Ensure the tooltips are activated - $(".tooltip-this").tooltip(); - - } catch (e) { - console.log("Failed to re-render the annotation filter after jump to class, error message: " + e); - } - - }, - - /** - * jumpUp - Jumps up to the parent concept in the UI - * - * @return {boolean} Returns false - */ - jumpUp: function() { - - try { - // Re-root the tree at the parent concept of the root - var view = this, - tree = view.treeEl.data("NCBOTree"), - options = tree.options(), - startingRoot = options.startingRoot; - - if (startingRoot == view.startingRoot) { - return false; - } - - var parentId = $("a[data-id='" + encodeURIComponent(startingRoot) + "'").attr("data-subclassof"); - - // Re-root - $.extend(options, { - startingRoot: parentId - }); - - // Force a re-render - tree.init(); - - // Ensure the tooltips are activated - $(".tooltip-this").tooltip(); - - return false; - - } catch (e) { - console.log("Failed to jump to parent concept in the annotation filter, error message: " + e); + } else { + view.options.push(item); + view.selected.push(item.value); + view.updateMultiselect(); } - }, - - /** - * resetTree - Collapse all expanded concepts - * - * @return {boolean} Returns false - */ - resetTree: function() { - - try { - - var view = this; - - // Re-root the tree at the original concept - var tree = view.treeEl.data("NCBOTree"); - - var options = tree.options(); - - // Re-root - $.extend(options, { - startingRoot: view.startingRoot - }); - - tree.changeOntology(view.ontology); - - // Force a re-render - tree.init(); - - // Ensure the tooltips are activated - $(".tooltip-this").tooltip(); - + // Ensure tooltips are removed + $("body > .tooltip").remove(); + + // Prevent default action + return false; + } catch (e) { + console.log( + "Failed to select an annotation concept, error message: " + e, + ); + } + }, + + /** + * afterExpand - Actions to perform when the user expands a concept in + * the tree + */ + afterExpand: function () { + try { + // Ensure tooltips are activated + $(".tooltip-this").tooltip(); + } catch (e) { + console.log( + "Failed to initialize tooltips in the annotation filter, error message: " + + e, + ); + } + }, + + /** + * afterJumpToClass - Called when a user searches for and selects a + * concept from the search results + * + * @param {type} event The jump to class event + * @param {type} classId The ID for the selected concept (a URL) + */ + afterJumpToClass: function (event, classId) { + try { + var view = this; + // Re-root the tree at this concept + var tree = view.treeEl.data("NCBOTree"); + var options = tree.options(); + $.extend(options, { + startingRoot: classId, + }); + + // Force a re-render + tree.init(); + + // Ensure the tooltips are activated + $(".tooltip-this").tooltip(); + } catch (e) { + console.log( + "Failed to re-render the annotation filter after jump to class, error message: " + + e, + ); + } + }, + + /** + * jumpUp - Jumps up to the parent concept in the UI + * + * @return {boolean} Returns false + */ + jumpUp: function () { + try { + // Re-root the tree at the parent concept of the root + var view = this, + tree = view.treeEl.data("NCBOTree"), + options = tree.options(), + startingRoot = options.startingRoot; + + if (startingRoot == view.startingRoot) { return false; - } catch (e) { - console.log("Failed to reset the annotation filter tree, error message: " + e); } - }, - - }); - }); + var parentId = $( + "a[data-id='" + encodeURIComponent(startingRoot) + "'", + ).attr("data-subclassof"); + + // Re-root + $.extend(options, { + startingRoot: parentId, + }); + + // Force a re-render + tree.init(); + + // Ensure the tooltips are activated + $(".tooltip-this").tooltip(); + + return false; + } catch (e) { + console.log( + "Failed to jump to parent concept in the annotation filter, error message: " + + e, + ); + } + }, + + /** + * resetTree - Collapse all expanded concepts + * + * @return {boolean} Returns false + */ + resetTree: function () { + try { + var view = this; + + // Re-root the tree at the original concept + var tree = view.treeEl.data("NCBOTree"); + + var options = tree.options(); + + // Re-root + $.extend(options, { + startingRoot: view.startingRoot, + }); + + tree.changeOntology(view.ontology); + + // Force a re-render + tree.init(); + + // Ensure the tooltips are activated + $(".tooltip-this").tooltip(); + + return false; + } catch (e) { + console.log( + "Failed to reset the annotation filter tree, error message: " + e, + ); + } + }, + }, + ); +});

diff --git a/docs/docs/src_js_views_searchSelect_NodeSelectView.js.html b/docs/docs/src_js_views_searchSelect_NodeSelectView.js.html index 3eb3c52c2..ba45cb722 100644 --- a/docs/docs/src_js_views_searchSelect_NodeSelectView.js.html +++ b/docs/docs/src_js_views_searchSelect_NodeSelectView.js.html @@ -45,109 +45,108 @@

Source: src/js/views/searchSelect/NodeSelectView.js

define([
-    "jquery",
-    "underscore",
-    "backbone",
-    "views/searchSelect/SearchableSelectView",
-    "models/NodeModel"
-  ],
-  function($, _, Backbone, SearchableSelect, NodeModel) {
-
-    /**
-     * @class NodeSelect
-     * @classdesc A select interface that allows the user to search for and
-     * select a member node
-     * @classcategory Views/SearchSelect
-     * @extends SearchableSelect
-     * @constructor
-     * @since 2.14.0
-     * @screenshot views/searchSelect/NodeSelectView.png
-     */
-    var NodeSelect = SearchableSelect.extend(
-      /** @lends NodeSelectView.prototype */
-      {
-        /**
-         * The type of View this is
-         * @type {string}
-         */
-        type: "NodeSelect",
-
-        /**
-         * className - Returns the class names for this view element
-         *
-         * @return {string}  class names
-         */
-        className: SearchableSelect.prototype.className + " node-select",
-
-        /**
-         * Text to show in the input field before any value has been entered
-         * @type {string}
-         */
-        placeholderText: "Select a DataONE repository",
-
-        /**
-         * Label for the input element
-         * @type {string}
-         */
-        inputLabel: "Select a DataONE repository",
-
-        /**
-         * Whether to allow users to select more than one value
-         * @type {boolean}
-         */
-        allowMulti: true,
-
-        /**
-         * Setting to true gives users the ability to add their own options that
-         * are not listed in this.options. This can work with either single
-         * or multiple search select dropxdowns
-         * @type {boolean}
-         * @default true
-         */
-        allowAdditions: true,
-
-        /**
-         * Creates a new NodeSelectView
-         * @param {Object} options - A literal object with options to pass to the view
-         */
-        initialize: function(options){
-          try {
-            
-            // Ensure the query fields are cached
-            if ( typeof MetacatUI.nodeModel === "undefined" ) {
-              MetacatUI.nodeModel = new NodeModel();
+  "jquery",
+  "underscore",
+  "backbone",
+  "views/searchSelect/SearchableSelectView",
+  "models/NodeModel",
+], function ($, _, Backbone, SearchableSelect, NodeModel) {
+  /**
+   * @class NodeSelect
+   * @classdesc A select interface that allows the user to search for and
+   * select a member node
+   * @classcategory Views/SearchSelect
+   * @extends SearchableSelect
+   * @constructor
+   * @since 2.14.0
+   * @screenshot views/searchSelect/NodeSelectView.png
+   */
+  var NodeSelect = SearchableSelect.extend(
+    /** @lends NodeSelectView.prototype */
+    {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "NodeSelect",
+
+      /**
+       * className - Returns the class names for this view element
+       *
+       * @return {string}  class names
+       */
+      className: SearchableSelect.prototype.className + " node-select",
+
+      /**
+       * Text to show in the input field before any value has been entered
+       * @type {string}
+       */
+      placeholderText: "Select a DataONE repository",
+
+      /**
+       * Label for the input element
+       * @type {string}
+       */
+      inputLabel: "Select a DataONE repository",
+
+      /**
+       * Whether to allow users to select more than one value
+       * @type {boolean}
+       */
+      allowMulti: true,
+
+      /**
+       * Setting to true gives users the ability to add their own options that
+       * are not listed in this.options. This can work with either single
+       * or multiple search select dropxdowns
+       * @type {boolean}
+       * @default true
+       */
+      allowAdditions: true,
+
+      /**
+       * Creates a new NodeSelectView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Ensure the query fields are cached
+          if (typeof MetacatUI.nodeModel === "undefined") {
+            MetacatUI.nodeModel = new NodeModel();
+          }
+
+          var members = MetacatUI.nodeModel.get("members");
+
+          // Maps the nodeModel member attributes (keys) to the searchSelect
+          // dropdown options properties (values)
+          var map = Object.entries({
+            logo: "image",
+            name: "label",
+            description: "description",
+            identifier: "value",
+          });
+
+          this.options = [];
+
+          // Convert nodeModel members to options of searchSelect
+          members.forEach((member, i) => {
+            this.options[i] = {};
+            for (const [oldName, newName] of map) {
+              this.options[i][newName] = member[oldName];
             }
+          });
 
-            var members = MetacatUI.nodeModel.get("members");
-
-            // Maps the nodeModel member attributes (keys) to the searchSelect
-            // dropdown options properties (values)
-            var map = Object.entries({
-              logo: "image",
-              name: "label",
-              description: "description",
-              identifier: "value"
-            });
-
-            this.options = [];
-
-            // Convert nodeModel members to options of searchSelect
-            members.forEach((member, i) => {
-              this.options[i] = {};
-              for (const [oldName, newName] of map) {
-                this.options[i][newName] = member[oldName]
-              }
-            });
-
-            SearchableSelect.prototype.initialize.call(this, options);
-          } catch (e) {
-            console.log("Failed to initialize a Node Select View, error message: " + e);
-          }
+          SearchableSelect.prototype.initialize.call(this, options);
+        } catch (e) {
+          console.log(
+            "Failed to initialize a Node Select View, error message: " + e,
+          );
         }
-
-      });
-      return NodeSelect;
-  });
+      },
+    },
+  );
+  return NodeSelect;
+});
 
diff --git a/docs/docs/src_js_views_searchSelect_ObjectFormatSelectView.js.html b/docs/docs/src_js_views_searchSelect_ObjectFormatSelectView.js.html index c0f670873..78fcc0bf5 100644 --- a/docs/docs/src_js_views_searchSelect_ObjectFormatSelectView.js.html +++ b/docs/docs/src_js_views_searchSelect_ObjectFormatSelectView.js.html @@ -49,121 +49,123 @@

Source: src/js/views/searchSelect/ObjectFormatSelectView. "underscore", "backbone", "views/searchSelect/SearchableSelectView", - "collections/ObjectFormats" -], - function ($, _, Backbone, SearchableSelect, ObjectFormats) { - - /** - * @class ObjectFormatSelect - * @classdesc A select interface that allows the user to search for and - * select a DataONE object format - * @classcategory Views/SearchSelect - * @extends SearchableSelect - * @constructor - * @since 2.15.0 - * @screenshot views/searchSelect/ObjectFormatSelectView.png - */ - var ObjectFormatSelect = SearchableSelect.extend( - /** @lends ObjectFormatSelectView.prototype */ - { - /** - * The type of View this is - * @type {string} - */ - type: "ObjectFormatSelect", - - /** - * className - Returns the class names for this view element - * - * @return {string} class names - */ - className: SearchableSelect.prototype.className + " object-format-select", - - /** - * Label for the input element - * @type {string} - * @since 2.15.0 - */ - inputLabel: "Select one or more metadata types", - - /** - * Text to show in the input field before any value has been entered - * @type {string} - * @since 2.15.0 - */ - placeholderText: "Type in a metadata type", - - /** - * Whether to allow users to select more than one value - * @type {boolean} - * @since 2.15.0 - */ - allowMulti: true, - - /** - * Setting to true gives users the ability to add their own options that - * are not listed in this.options. This can work with either single - * or multiple search select dropdowns - * @type {boolean} - * @default true - * @since 2.15.0 - */ - allowAdditions: true, - - /** - * Render the view - * - * @return {ObjectFormatSelect} Returns the view - * @since 2.15.0 - */ - render: function () { - - try { - var view = this; - - // Ensure the object formats are cached - if (typeof MetacatUI.objectFormats === "undefined") { - MetacatUI.objectFormats = new ObjectFormats(); - } - - // If not already synced, then get the object formats - if ( - MetacatUI.objectFormats.length === 0 && - !(MetacatUI.objectFormats._events && MetacatUI.objectFormats._events.sync) - ) { - this.listenToOnce(MetacatUI.objectFormats, "sync error", view.render); - MetacatUI.objectFormats.fetch(); - return - } - - var formatIds = MetacatUI.objectFormats.toJSON(); - var options = _.chain(formatIds) - // Since the Query Rules automatically include a rule for formatType = - // "METADATA", only allow filtering datasets by specific metadata type. - .where({ formatType: "METADATA" }) - .map( - function (format) { - return { - label: format.formatName, - value: format.formatId, - description: format.formatId - } - } - ) - .value(); - - this.options = options; - - SearchableSelect.prototype.render.call(view); - } catch (error) { - console.log("Error rendering an Object Format Select View."); - console.log(error); + "collections/ObjectFormats", +], function ($, _, Backbone, SearchableSelect, ObjectFormats) { + /** + * @class ObjectFormatSelect + * @classdesc A select interface that allows the user to search for and + * select a DataONE object format + * @classcategory Views/SearchSelect + * @extends SearchableSelect + * @constructor + * @since 2.15.0 + * @screenshot views/searchSelect/ObjectFormatSelectView.png + */ + var ObjectFormatSelect = SearchableSelect.extend( + /** @lends ObjectFormatSelectView.prototype */ + { + /** + * The type of View this is + * @type {string} + */ + type: "ObjectFormatSelect", + + /** + * className - Returns the class names for this view element + * + * @return {string} class names + */ + className: SearchableSelect.prototype.className + " object-format-select", + + /** + * Label for the input element + * @type {string} + * @since 2.15.0 + */ + inputLabel: "Select one or more metadata types", + + /** + * Text to show in the input field before any value has been entered + * @type {string} + * @since 2.15.0 + */ + placeholderText: "Type in a metadata type", + + /** + * Whether to allow users to select more than one value + * @type {boolean} + * @since 2.15.0 + */ + allowMulti: true, + + /** + * Setting to true gives users the ability to add their own options that + * are not listed in this.options. This can work with either single + * or multiple search select dropdowns + * @type {boolean} + * @default true + * @since 2.15.0 + */ + allowAdditions: true, + + /** + * Render the view + * + * @return {ObjectFormatSelect} Returns the view + * @since 2.15.0 + */ + render: function () { + try { + var view = this; + + // Ensure the object formats are cached + if (typeof MetacatUI.objectFormats === "undefined") { + MetacatUI.objectFormats = new ObjectFormats(); + } + + // If not already synced, then get the object formats + if ( + MetacatUI.objectFormats.length === 0 && + !( + MetacatUI.objectFormats._events && + MetacatUI.objectFormats._events.sync + ) + ) { + this.listenToOnce( + MetacatUI.objectFormats, + "sync error", + view.render, + ); + MetacatUI.objectFormats.fetch(); + return; } - } - }); - return ObjectFormatSelect; - }); + var formatIds = MetacatUI.objectFormats.toJSON(); + var options = _.chain(formatIds) + // Since the Query Rules automatically include a rule for formatType = + // "METADATA", only allow filtering datasets by specific metadata type. + .where({ formatType: "METADATA" }) + .map(function (format) { + return { + label: format.formatName, + value: format.formatId, + description: format.formatId, + }; + }) + .value(); + + this.options = options; + + SearchableSelect.prototype.render.call(view); + } catch (error) { + console.log("Error rendering an Object Format Select View."); + console.log(error); + } + }, + }, + ); + return ObjectFormatSelect; +});

diff --git a/docs/docs/src_js_views_searchSelect_QueryFieldSelectView.js.html b/docs/docs/src_js_views_searchSelect_QueryFieldSelectView.js.html index 8f7739158..7bdad8f9b 100644 --- a/docs/docs/src_js_views_searchSelect_QueryFieldSelectView.js.html +++ b/docs/docs/src_js_views_searchSelect_QueryFieldSelectView.js.html @@ -45,371 +45,392 @@

Source: src/js/views/searchSelect/QueryFieldSelectView.js
define([
-    "jquery",
-    "underscore",
-    "backbone",
-    "views/searchSelect/SearchableSelectView",
-    "collections/queryFields/QueryFields"
-  ],
-  function($, _, Backbone, SearchableSelect, QueryFields) {
-
-    /**
-     * @class QueryFieldSelectView
-     * @classdesc A select interface that allows the user to search for and
-     * select metadata field(s).
-     * @classcategory Views/SearchSelect
-     * @extends SearchableSelect
-     * @constructor
-     * @since 2.14.0
-     * @screenshot views/searchSelect/QueryFieldSelectView.png
-     */
-    var QueryFieldSelectView = SearchableSelect.extend(
-      /** @lends QueryFieldSelectView.prototype */
-      {
-        /**
-         * The type of View this is
-         * @type {string}
-         */
-        type: "QueryFieldSelect",
-
-        /**
-         * className - the class names for this view element
-         *
-         * @type {string}
-         */
-        className: SearchableSelect.prototype.className + " query-field-select",
-
-        /**
-         * Text to show in the input field before any value has been entered
-         * @type {string}
-         */
-        placeholderText: "Search for or select a field",
-
-        /**
-         * Label for the input element
-         * @type {string}
-         */
-        inputLabel: "Select one or more metadata fields to query",
-
-        /**
-         * @see SearchableSelectView#submenuStyle
-         * @default "accordion"
-         */
-        submenuStyle: "accordion",
-
-        /**
-         * A list of query fields names to exclude from the list of options
-         * @type {string[]}
-         */
-        excludeFields: [],
-
-        /**
-         * An additional field object contains the properties an additional query field to
-         * add that are required to render it correctly. An additional query field is one
-         * that does not actually exist in the query service index.
-         *
-         * @typedef {Object} AdditionalField
-         *
-         * @property {string} name - A unique ID to represent this field. It must not
-         * match the name of any other query fields.
-         * @property {string[]} fields - The list of real query fields that this
-         * abstracted field will represent. It must exactly match the names of the query
-         * fields that actually exist.
-         * @property {string} label - A user-facing label to display.
-         * @property {string} description - A description for this field.
-         * @property {string} category - The name of the category under which to place
-         * this field. It must match one of the category names for an existing query
-         * field.
-         *
-         * @since 2.15.0
-         */
-
-        /**
-         * A list of additional fields which are not retrieved from the query service
-         * index, but which should be added to the list of options. This can be used to
-         * add abstracted fields which are a combination of multiple query fields, or to
-         * add a duplicate field that has a different label.
-         *
-         * @type {AdditionalField[]}
-         * @since 2.15.0
-         */
-        addFields: [],
-
-        /**
-         * A list of query fields names to display at the top of the menu, above
-         * all other category headers
-         * @type {string[]}
-         */
-        commonFields: ["text", "documents-special-field"],
-
-        /**
-         * The names of categories that should have items sorted alphabetically. Names
-         * must exactly match those in the
-         * {@link QueryField#categoriesMap Query Field model}
-         * @type {string[]}
-         * @since 2.15.0
-         */
-        categoriesToAlphabetize: ["General"],
-
-        /**
-         * Whether or not to exclude fields which are not searchable. Set to
-         * false to keep query fields that are not searchable in the returned list
-         * @type {boolean}
-         */
-        excludeNonSearchable: true,
-
-        /**
-         * Creates a new QueryFieldSelectView
-         * @param {Object} options - A literal object with options to pass to the view
-         */
-        initialize: function(options){
-          try {
-            // Ensure the query fields are cached
-            if ( typeof MetacatUI.queryFields === "undefined" ) {
+  "jquery",
+  "underscore",
+  "backbone",
+  "views/searchSelect/SearchableSelectView",
+  "collections/queryFields/QueryFields",
+], function ($, _, Backbone, SearchableSelect, QueryFields) {
+  /**
+   * @class QueryFieldSelectView
+   * @classdesc A select interface that allows the user to search for and
+   * select metadata field(s).
+   * @classcategory Views/SearchSelect
+   * @extends SearchableSelect
+   * @constructor
+   * @since 2.14.0
+   * @screenshot views/searchSelect/QueryFieldSelectView.png
+   */
+  var QueryFieldSelectView = SearchableSelect.extend(
+    /** @lends QueryFieldSelectView.prototype */
+    {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "QueryFieldSelect",
+
+      /**
+       * className - the class names for this view element
+       *
+       * @type {string}
+       */
+      className: SearchableSelect.prototype.className + " query-field-select",
+
+      /**
+       * Text to show in the input field before any value has been entered
+       * @type {string}
+       */
+      placeholderText: "Search for or select a field",
+
+      /**
+       * Label for the input element
+       * @type {string}
+       */
+      inputLabel: "Select one or more metadata fields to query",
+
+      /**
+       * @see SearchableSelectView#submenuStyle
+       * @default "accordion"
+       */
+      submenuStyle: "accordion",
+
+      /**
+       * A list of query fields names to exclude from the list of options
+       * @type {string[]}
+       */
+      excludeFields: [],
+
+      /**
+       * An additional field object contains the properties an additional query field to
+       * add that are required to render it correctly. An additional query field is one
+       * that does not actually exist in the query service index.
+       *
+       * @typedef {Object} AdditionalField
+       *
+       * @property {string} name - A unique ID to represent this field. It must not
+       * match the name of any other query fields.
+       * @property {string[]} fields - The list of real query fields that this
+       * abstracted field will represent. It must exactly match the names of the query
+       * fields that actually exist.
+       * @property {string} label - A user-facing label to display.
+       * @property {string} description - A description for this field.
+       * @property {string} category - The name of the category under which to place
+       * this field. It must match one of the category names for an existing query
+       * field.
+       *
+       * @since 2.15.0
+       */
+
+      /**
+       * A list of additional fields which are not retrieved from the query service
+       * index, but which should be added to the list of options. This can be used to
+       * add abstracted fields which are a combination of multiple query fields, or to
+       * add a duplicate field that has a different label.
+       *
+       * @type {AdditionalField[]}
+       * @since 2.15.0
+       */
+      addFields: [],
+
+      /**
+       * A list of query fields names to display at the top of the menu, above
+       * all other category headers
+       * @type {string[]}
+       */
+      commonFields: ["text", "documents-special-field"],
+
+      /**
+       * The names of categories that should have items sorted alphabetically. Names
+       * must exactly match those in the
+       * {@link QueryField#categoriesMap Query Field model}
+       * @type {string[]}
+       * @since 2.15.0
+       */
+      categoriesToAlphabetize: ["General"],
+
+      /**
+       * Whether or not to exclude fields which are not searchable. Set to
+       * false to keep query fields that are not searchable in the returned list
+       * @type {boolean}
+       */
+      excludeNonSearchable: true,
+
+      /**
+       * Creates a new QueryFieldSelectView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Ensure the query fields are cached
+          if (typeof MetacatUI.queryFields === "undefined") {
+            MetacatUI.queryFields = new QueryFields();
+            MetacatUI.queryFields.fetch();
+          }
+          SearchableSelect.prototype.initialize.call(this, options);
+        } catch (e) {
+          console.log(
+            "Failed to initialize a Query Field Select View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * postRender - Updates the view once the dropdown UI has loaded. Processes the
+       * QueryFields given the options passed to this view, then updates the menu and
+       * selection. Processing the fields takes some time, which is why we allow the
+       * view to render before starting that process. This prevents slowing down the
+       * rendering of parent views.
+       */
+      postRender: function () {
+        try {
+          var view = this;
+          _.defer(function () {
+            view.processFields();
+            view.updateMenu();
+            // With the new menu in place, show the pre-selected values. Silent is set
+            // to true so that it doesn't trigger an update of the model. Defer to make
+            // sure the menu elements are attached.
+            _.defer(function () {
+              view.changeSelection(view.selected, true);
+            });
+            SearchableSelect.prototype.postRender.call(view);
+          });
+        } catch (error) {
+          console.log(
+            "Post-render failed in a QueryFieldSelectView." +
+              " Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Retrieves the queryFields collection if not already fetched, then organizes the
+       * fields based on the options passed to this view.
+       * @since 2.17.0
+       */
+      processFields: function () {
+        try {
+          var view = this;
+
+          // Ensure the query fields are cached for the Query Field Select
+          // View and the Query Rule View
+          if (
+            typeof MetacatUI.queryFields === "undefined" ||
+            MetacatUI.queryFields.length === 0
+          ) {
+            if (typeof MetacatUI.queryFields === "undefined") {
               MetacatUI.queryFields = new QueryFields();
-              MetacatUI.queryFields.fetch();
             }
-            SearchableSelect.prototype.initialize.call(this, options);
-          } catch (e) {
-            console.log("Failed to initialize a Query Field Select View, error message: " + e);
-          }
-        },
-
-        /**
-         * postRender - Updates the view once the dropdown UI has loaded. Processes the
-         * QueryFields given the options passed to this view, then updates the menu and
-         * selection. Processing the fields takes some time, which is why we allow the
-         * view to render before starting that process. This prevents slowing down the
-         * rendering of parent views.
-         */
-        postRender : function(){
-
-          try {
-            var view = this;
-            _.defer(function(){
-              view.processFields();
-              view.updateMenu();
-              // With the new menu in place, show the pre-selected values. Silent is set
-              // to true so that it doesn't trigger an update of the model. Defer to make
-              // sure the menu elements are attached.
-              _.defer(function() {
-                view.changeSelection(view.selected, true);
-              });
-              SearchableSelect.prototype.postRender.call(view);
-            })
-          }
-          catch (error) {
-            console.log( 'Post-render failed in a QueryFieldSelectView.' +
-              ' Error details: ' + error );
+            this.listenToOnce(
+              MetacatUI.queryFields,
+              "sync",
+              this.processFields,
+            );
+            MetacatUI.queryFields.fetch();
+            return;
           }
-        },
-
-        /**
-         * Retrieves the queryFields collection if not already fetched, then organizes the
-         * fields based on the options passed to this view.
-         * @since 2.17.0
-         */
-        processFields : function(){
-          try {
-            var view = this;
-
-            // Ensure the query fields are cached for the Query Field Select
-            // View and the Query Rule View
-            if (
-              typeof MetacatUI.queryFields === "undefined" ||
-              MetacatUI.queryFields.length === 0
-            ) {
-              if (typeof MetacatUI.queryFields === "undefined") {
-                MetacatUI.queryFields = new QueryFields();
-              }
-              this.listenToOnce(MetacatUI.queryFields, "sync", this.processFields)
-              MetacatUI.queryFields.fetch();
-              return
-            }
-
-            // Convert the queryFields collection to an object formatted for the
-            // SearchableSelect view.
-            var fieldsJSON = MetacatUI.queryFields.toJSON();
 
-            // Process & add additional fields set on this view (these are fields not
-            // retrieved from the query service API)
-            if (this.addFields && Array.isArray(this.addFields)) {
-              // For each added field, find the icon and category order from the already
-              // existing fields with the same category.
-              this.addFields = _.map(this.addFields, function (field) {
+          // Convert the queryFields collection to an object formatted for the
+          // SearchableSelect view.
+          var fieldsJSON = MetacatUI.queryFields.toJSON();
+
+          // Process & add additional fields set on this view (these are fields not
+          // retrieved from the query service API)
+          if (this.addFields && Array.isArray(this.addFields)) {
+            // For each added field, find the icon and category order from the already
+            // existing fields with the same category.
+            this.addFields = _.map(
+              this.addFields,
+              function (field) {
                 if (field.category) {
-                  var categoryInfo = _.findWhere(fieldsJSON, { category: field.category });
+                  var categoryInfo = _.findWhere(fieldsJSON, {
+                    category: field.category,
+                  });
                   ["icon", "categoryOrder"].forEach(function (prop) {
                     if (!field[prop]) {
-                      field[prop] = categoryInfo[prop]
+                      field[prop] = categoryInfo[prop];
                     }
-                  })
-                }
-                return field
-              }, this);
-              // Add the additional fields to the array of fields fetched from the
-              // query service API
-              fieldsJSON = fieldsJSON.concat(this.addFields);
-            }
-
-            // Move common fields to the top of the menu, outside of any
-            // category headers, so that they are easy to find
-            if (this.commonFields && Array.isArray(this.commonFields)) {
-              this.commonFields.forEach(function (commonFieldName) {
-                var i = _.findIndex(fieldsJSON, { name: commonFieldName });
-                if (i > 0) {
-                  // If the category name is an empty string, no header will
-                  // be created in the menu
-                  fieldsJSON[i].category = ""
-                  // The min categoryOrder in the QueryFields collection is 1
-                  fieldsJSON[i].categoryOrder = 0
-                  fieldsJSON[i].icon = "star"
+                  });
                 }
-              });
-            }
-
-            // Filter out non-searchable fields (if option is true),
-            // and fields that should be excluded
-            var processedFields = _(fieldsJSON)
-              .chain()
-              .sortBy("categoryOrder")
-              .filter(
-                function (filter) {
-                  if (this.excludeNonSearchable) {
-                    if (["false", false].includes(filter.searchable)) {
-                      return false
-                    }
-                  }
-                  if (this.excludeFields && this.excludeFields.length) {
-                    if (this.excludeFields.includes(filter.name)) {
-                      return false
-                    }
-                  }
-                  return true
-                }, this
-              )
-              .map(view.fieldToOption)
-              .groupBy("categoryOrder")
-              .value();
-
-            // Rename the grouped categories
-            for (const [key, value] of Object.entries(processedFields)) {
-              processedFields[value[0].category] = value;
-              delete processedFields[key];
-            }
-
-            // Sort items alphabetically for the specified categories
-            if (this.categoriesToAlphabetize && this.categoriesToAlphabetize.length) {
-              this.categoriesToAlphabetize.forEach(function (categoryName) {
-                // Sort by category label
-                processedFields[categoryName].sort(function (a, b) {
-                  // Ignore upper and lowercase
-                  var nameA = a.label.toUpperCase();
-                  var nameB = b.label.toUpperCase();
-                  if (nameA < nameB)
-                    return -1;
-                  if (nameA > nameB)
-                    return 1;
-                  return 0;
-                });
-              })
-            }
+                return field;
+              },
+              this,
+            );
+            // Add the additional fields to the array of fields fetched from the
+            // query service API
+            fieldsJSON = fieldsJSON.concat(this.addFields);
+          }
 
-            // Set the formatted fields on the view
-            this.options = processedFields;
+          // Move common fields to the top of the menu, outside of any
+          // category headers, so that they are easy to find
+          if (this.commonFields && Array.isArray(this.commonFields)) {
+            this.commonFields.forEach(function (commonFieldName) {
+              var i = _.findIndex(fieldsJSON, { name: commonFieldName });
+              if (i > 0) {
+                // If the category name is an empty string, no header will
+                // be created in the menu
+                fieldsJSON[i].category = "";
+                // The min categoryOrder in the QueryFields collection is 1
+                fieldsJSON[i].categoryOrder = 0;
+                fieldsJSON[i].icon = "star";
+              }
+            });
           }
-          catch (error) {
-            console.log( 'There was an error organizing the Fields in a QueryFieldSelectView' +
-              ' Error details: ' + error );
+
+          // Filter out non-searchable fields (if option is true),
+          // and fields that should be excluded
+          var processedFields = _(fieldsJSON)
+            .chain()
+            .sortBy("categoryOrder")
+            .filter(function (filter) {
+              if (this.excludeNonSearchable) {
+                if (["false", false].includes(filter.searchable)) {
+                  return false;
+                }
+              }
+              if (this.excludeFields && this.excludeFields.length) {
+                if (this.excludeFields.includes(filter.name)) {
+                  return false;
+                }
+              }
+              return true;
+            }, this)
+            .map(view.fieldToOption)
+            .groupBy("categoryOrder")
+            .value();
+
+          // Rename the grouped categories
+          for (const [key, value] of Object.entries(processedFields)) {
+            processedFields[value[0].category] = value;
+            delete processedFields[key];
           }
-        },
-
-        /**
-         * fieldToOption - Converts an object that represents a QueryField model in the
-         * format specified by the SearchableSelectView.options
-         *
-         * @param  {object} field An object with properties corresponding to a QueryField
-         * model
-         * @return {object}       An object in the format specified by
-         * SearchableSelectView.options
-         */
-        fieldToOption: function(field) {
-          return {
-            label: field.label ? field.label : field.name,
-            value: field.name,
-            description: field.friendlyDescription ? field.friendlyDescription : field.description,
-            icon: field.icon,
-            category: field.category,
-            categoryOrder: field.categoryOrder,
-            type: field.type
-          };
-        },
-
-        /**
-         * addTooltip - Add a tooltip to a given element using the description in the
-         * options object that's set on the view. This overwrites the prototype addTooltip
-         * function so that we can use popovers with more details for query select fields.
-         *
-         * @param  {HTMLElement} element The HTML element a tooltip should be added
-         * @param  {string} position how to position the tooltip - top | bottom | left |
-         * right
-         * @return {jQuery} The element with a tooltip wrapped by jQuery
-         */
-        addTooltip: function(element, position = "bottom"){
-
-          if(!element){
-            return
+
+          // Sort items alphabetically for the specified categories
+          if (
+            this.categoriesToAlphabetize &&
+            this.categoriesToAlphabetize.length
+          ) {
+            this.categoriesToAlphabetize.forEach(function (categoryName) {
+              // Sort by category label
+              processedFields[categoryName].sort(function (a, b) {
+                // Ignore upper and lowercase
+                var nameA = a.label.toUpperCase();
+                var nameB = b.label.toUpperCase();
+                if (nameA < nameB) return -1;
+                if (nameA > nameB) return 1;
+                return 0;
+              });
+            });
           }
 
-          // Find the description in the options object, using the data-value
-          // attribute set in the template. The data-value attribute is either
-          // the label, or the value, depending on if a value is provided.
-          var valueOrLabel = $(element).data("value"),
-              opt = _.chain(this.options)
-                          .values()
-                          .flatten()
-                          .find(function(option){
-                            return option.label == valueOrLabel || option.value == valueOrLabel
-                          })
-                          .value();
-
-          var label = opt.label,
-              value = opt.value,
-              type = opt.type,
-              description = (opt.description ? opt.description : "");
-
-          // For added fields, the value set on the options.value element is just a
-          // unique identifier. The values that should be used to build a query are saved
-          // in the addFields array set on this view.
-          if(this.addFields && Array.isArray(this.addFields)){
-            var specialField = _.findWhere(this.addFields, { name: valueOrLabel });
-            if(specialField){
-              value = specialField.fields;
-              type = [];
-              specialField.fields.forEach(function(fieldName){
-                var realField = MetacatUI.queryFields.findWhere({
-                  name: fieldName
-                });
-                if(realField){
-                  type.push(realField.get("type"))
-                } else {
-                  type.push("special field")
-                }
-              }, this);
-              type = type.join(", ");
-            }
+          // Set the formatted fields on the view
+          this.options = processedFields;
+        } catch (error) {
+          console.log(
+            "There was an error organizing the Fields in a QueryFieldSelectView" +
+              " Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * fieldToOption - Converts an object that represents a QueryField model in the
+       * format specified by the SearchableSelectView.options
+       *
+       * @param  {object} field An object with properties corresponding to a QueryField
+       * model
+       * @return {object}       An object in the format specified by
+       * SearchableSelectView.options
+       */
+      fieldToOption: function (field) {
+        return {
+          label: field.label ? field.label : field.name,
+          value: field.name,
+          description: field.friendlyDescription
+            ? field.friendlyDescription
+            : field.description,
+          icon: field.icon,
+          category: field.category,
+          categoryOrder: field.categoryOrder,
+          type: field.type,
+        };
+      },
+
+      /**
+       * addTooltip - Add a tooltip to a given element using the description in the
+       * options object that's set on the view. This overwrites the prototype addTooltip
+       * function so that we can use popovers with more details for query select fields.
+       *
+       * @param  {HTMLElement} element The HTML element a tooltip should be added
+       * @param  {string} position how to position the tooltip - top | bottom | left |
+       * right
+       * @return {jQuery} The element with a tooltip wrapped by jQuery
+       */
+      addTooltip: function (element, position = "bottom") {
+        if (!element) {
+          return;
+        }
+
+        // Find the description in the options object, using the data-value
+        // attribute set in the template. The data-value attribute is either
+        // the label, or the value, depending on if a value is provided.
+        var valueOrLabel = $(element).data("value"),
+          opt = _.chain(this.options)
+            .values()
+            .flatten()
+            .find(function (option) {
+              return (
+                option.label == valueOrLabel || option.value == valueOrLabel
+              );
+            })
+            .value();
+
+        var label = opt.label,
+          value = opt.value,
+          type = opt.type,
+          description = opt.description ? opt.description : "";
+
+        // For added fields, the value set on the options.value element is just a
+        // unique identifier. The values that should be used to build a query are saved
+        // in the addFields array set on this view.
+        if (this.addFields && Array.isArray(this.addFields)) {
+          var specialField = _.findWhere(this.addFields, {
+            name: valueOrLabel,
+          });
+          if (specialField) {
+            value = specialField.fields;
+            type = [];
+            specialField.fields.forEach(function (fieldName) {
+              var realField = MetacatUI.queryFields.findWhere({
+                name: fieldName,
+              });
+              if (realField) {
+                type.push(realField.get("type"));
+              } else {
+                type.push("special field");
+              }
+            }, this);
+            type = type.join(", ");
           }
+        }
 
-          var contentEl = $(document.createElement("div")),
-              titleEl = $("<div>" + label + "</div>"),
-              valueEl = $("<code class='pull-right'>" + value + "</code>"),
-              typeEl = $("<span class='muted pull-right'><b>Type: " + type + "</b></span>"),
-              descriptionEl = $("<p>" + description + "</p>");
+        var contentEl = $(document.createElement("div")),
+          titleEl = $("<div>" + label + "</div>"),
+          valueEl = $("<code class='pull-right'>" + value + "</code>"),
+          typeEl = $(
+            "<span class='muted pull-right'><b>Type: " + type + "</b></span>",
+          ),
+          descriptionEl = $("<p>" + description + "</p>");
 
-            titleEl.append(valueEl);
-            contentEl.append(descriptionEl, typeEl)
+        titleEl.append(valueEl);
+        contentEl.append(descriptionEl, typeEl);
 
-          $(element).popover({
+        $(element)
+          .popover({
             title: titleEl,
             content: contentEl,
             html: true,
@@ -418,73 +439,76 @@ 

Source: src/js/views/searchSelect/QueryFieldSelectView.js container: "body", delay: { show: 1100, - hide: 50 - } + hide: 50, + }, }) - .on("show.bs.popover", - function(){ - var $el = $(this); - // Allow time for the popup to be added to the DOM - setTimeout(function () { - // Then add some css rules, and a special class to identify - // these popups if they need to be removed. - $el.data('popover').$tip - .css({ - "maxWidth": "400px", - "pointerEvents" : "none" - }) - .addClass("search-select-tooltip"); - }, 10); + .on("show.bs.popover", function () { + var $el = $(this); + // Allow time for the popup to be added to the DOM + setTimeout(function () { + // Then add some css rules, and a special class to identify + // these popups if they need to be removed. + $el + .data("popover") + .$tip.css({ + maxWidth: "400px", + pointerEvents: "none", + }) + .addClass("search-select-tooltip"); + }, 10); }); - return $(element) - - }, - - /** - * isValidOption - Checks if a value is one of the values given in view.options - * - * @param {string} value The value to check - * @return {boolean} returns true if the value is one of the values given in view.options - */ - isValidOption: function(value){ - - try { - var view = this; - - // First check if the value is one of the fields that's excluded. - if(view.excludeFields.includes(value)){ - // If it is, then add it to the list of options - var newField = MetacatUI.queryFields.findWhere({ - name: value - }); - if(newField){ - newField = view.fieldToOption(newField.toJSON()); - } - view.options[newField.category].push(newField); - view.updateMenu(); - // Make sure the new menu is attached before updating the selections - setTimeout(function () { - // If the selected value has been removed, re-add it. - if(!view.selected.includes(value)){ - view.selected.push(value) - } - view.changeSelection(view.selected, silent = true); - }, 25); - return true - } else { - var isValid = SearchableSelect.prototype.isValidOption.call(view, value); - return isValid + return $(element); + }, + + /** + * isValidOption - Checks if a value is one of the values given in view.options + * + * @param {string} value The value to check + * @return {boolean} returns true if the value is one of the values given in view.options + */ + isValidOption: function (value) { + try { + var view = this; + + // First check if the value is one of the fields that's excluded. + if (view.excludeFields.includes(value)) { + // If it is, then add it to the list of options + var newField = MetacatUI.queryFields.findWhere({ + name: value, + }); + if (newField) { + newField = view.fieldToOption(newField.toJSON()); } - } catch (e) { - console.log("Failed to check if option is valid in a Query Field Select View, error message: " + e); + view.options[newField.category].push(newField); + view.updateMenu(); + // Make sure the new menu is attached before updating the selections + setTimeout(function () { + // If the selected value has been removed, re-add it. + if (!view.selected.includes(value)) { + view.selected.push(value); + } + view.changeSelection(view.selected, (silent = true)); + }, 25); + return true; + } else { + var isValid = SearchableSelect.prototype.isValidOption.call( + view, + value, + ); + return isValid; } - - }, - - }); - return QueryFieldSelectView; - }); + } catch (e) { + console.log( + "Failed to check if option is valid in a Query Field Select View, error message: " + + e, + ); + } + }, + }, + ); + return QueryFieldSelectView; +});

diff --git a/docs/docs/src_js_views_searchSelect_SearchableSelectView.js.html b/docs/docs/src_js_views_searchSelect_SearchableSelectView.js.html index ebedc2082..c07492f47 100644 --- a/docs/docs/src_js_views_searchSelect_SearchableSelectView.js.html +++ b/docs/docs/src_js_views_searchSelect_SearchableSelectView.js.html @@ -45,1160 +45,1227 @@

Source: src/js/views/searchSelect/SearchableSelectView.js
define([
-    "jquery",
-    "underscore",
-    "backbone",
-    "semanticUItransition",
-    "text!" + MetacatUI.root + "/components/semanticUI/transition.min.css",
-    "semanticUIdropdown",
-    "text!" + MetacatUI.root + "/components/semanticUI/dropdown.min.css",
-    "text!templates/selectUI/searchableSelect.html",
-  ],
-  function($, _, Backbone, Transition, TransitionCSS, Dropdown, DropdownCSS, Template) {
-
-    /**
-     * @class SearchableSelectView
-     * @classdesc A select interface that allows the user to search from within
-     * the options, and optionally select multiple items. Also allows the items
-     * to be grouped, and to display an icon or image for each item.
-     * @classcategory Views/SearchSelect
-     * @extends Backbone.View
-     * @constructor
-     * @since 2.14.0
-     * @screenshot views/searchSelect/SearchableSelectView.png
-     */
-    return Backbone.View.extend(
-      /** @lends SearchableSelectView.prototype */
-      {
-        /**
-         * The type of View this is
-         * @type {string}
-         */
-        type: "SearchableSelect",
-
-        /**
-         * The HTML class names for this view element
-         * @type {string}
-         */
-        className: "searchable-select",
-
-        /**
-         * Text to show in the input field before any value has been entered
-         * @type {string}
-         */
-        placeholderText: "Search for or select a value",
-
-        /**
-         * Label for the input element
-         * @type {string}
-         */
-        inputLabel: "Select a value",
-
-        /**
-         * Whether to allow users to select more than one value
-         * @type {boolean}
-         */
-        allowMulti: true,
-
-        /**
-         * Setting to true gives users the ability to add their own options that
-         * are not listed in this.options. This can work with either single
-         * or multiple search select dropdowns
-         * @type {boolean}
-         */
-        allowAdditions: false,
-
-        /**
-         * Whether the dropdown value can be cleared by the user after being
-         * selected.
-         * @type {boolean}
-         */
-        clearable: true,
-
-        /**
-        * When items are grouped within categories, this attribute determines how to display the items
-        * within each category.
-        * @type {string}
-        * @example
-        * // display the items in a traditional, non-interactive list below category titles
-        * "list"
-        * @example
-        * // initially show only a list of category titles, and popout
-        * // a submenu on the left or right when the user hovers over
-        * // or touches a category (can lead to the sub-menu being hidden
-        * // on mobile devices if the element is wide)
-        * "popout"
-        * @example
-        * // initially show only a list of category titles, and expand
-        * // the list of items below each category when a user clicks
-        * // on the category title, much like an "accordion" element.
-        * "accordion"
-        * @default "list"
-         */
-        submenuStyle: "list",
-
-        /**
-         * Set to false to always display category headers in the dropdown,
-         * even if there are no results in that category when a user is searching.
-         * @type {boolean}
-         */
-        hideEmptyCategoriesOnSearch: true,
-
-        /**
-         * The maximum width of images used for each option, in pixels
-         * @type {number}
-         */
-        imageWidth: 30,
-
-        /**
-         * The maximum height of images used for each option, in pixels
-         * @type {number}
-         */
-        imageHeight: 30,
-
-        /**
-         * For select inputs where multiple values are allowed
-         * ({@link SearchableSelectView#allowMulti} is true), optional text to insert
-         * between labels. Separator text is useful for indicating operators in filter
-         * fields or values.
-         * @type {string}
-         * @since 2.15.0
-         */
-        separatorText: "",
-
-        /**
-        * For select inputs where multiple values are allowed
-        * ({@link SearchableSelectView#allowMulti} is true), a list of
-        * {@link SearchableSelectView#separatorText} options. If a list is provided here
-        * (AND a value is provided for the {@link SearchableSelectView#separatorText}
-        * option), then a user can click on the separator text between two values to
-        * change the text to the next string in this list. If separatorTextOptions is
-        * false (or if there is no separatorText value), then changing the separator text
-        * is not possible. This view will trigger a "separatorChanged" event when the
-        * separator is updated.
-        * @type {string[]}
-        * @since 2.17.0
-        */
-        separatorTextOptions: ["AND", "OR"],
-
-        /**
-         * The HTML class name to add to the separator elements that are created for this
-         * view.
-         * @type {string}
-         * @since 2.15.0
-         */
-        separatorClass: "separator",
-
-        /** 
-         * An additional HTML class to add to separator elements on hover when a user can
-         * click that element to switch the text.
-         * @type {string}
-         * @since 2.17.0
-         */
-        changeableSeparatorClass: "changeable-separator",
-
-        /**
-         * For separators that are changeable (see
-         * {@link SearchableSelectView#separatorTextOptions}), optional tooltip text to
-         * show when a user hovers over a separator element.
-         * @type {string}
-         * @since 2.17.0
-         */
-        changeableSeparatorTooltip: "Click to switch the operator",
-
-        /**
-         * The list of options that a user can select from in the dropdown menu. For
-         * un-categorized options, provide an array of objects, where each object is a
-         * single option. To create category headings, provide an object containing named
-         * objects, where the key for each object is the category title to display, and
-         * the value of each object comprises the option properties.
-         * @name SearchableSelectView#options
-         * @type {Object[]|Object}
-         * @property {string} icon - The name of a Font Awesome 3.2.1 icon to display to
-         * the left of the label (e.g. "lemon", "heart")
-         * @property {string} image - The complete path to an image to use instead of an
-         * icon. If both icon and image are provided, the icon will be used.
-         * @property {string} label - The label to show for the option
-         * @property {string} description - A description of the option, displayed as a
-         * tooltip when the user hovers over the label
-         * @property {string} value - If the value differs from the label, the value to
-         * return when this option is selected (otherwise label is returned)
-         * @example
-         * [
-         *   {
-         *     icon: "",
-         *     image: "https://www.dataone.org/uploads/member_node_logos/bcodmo_hu707c109c683d6da57b432522b4add783_33081_300x0_resize_box_2.png",
-         *     label: "BCO",
-         *     description: "The The Biological and Chemical Oceanography Data Management Office (BCO-DMO) serve data from research projects funded by the Biological and Chemical Oceanography Sections and the Division of Polar Programs Antarctic Organisms & Ecosystems Program at the U.S. National Science Foundation.",
-         *     value: "urn:node:BCODMO"
-         *   },
-         *   {
-         *     icon: "",
-         *     image: "https://www.dataone.org/uploads/member_node_logos/arctic.png",
-         *     label: "ADC",
-         *     description: "The US National Science Foundation Arctic Data Center operates as the primary repository supporting the NSF Arctic community for data preservation and access.",
-         *     value: "urn:node:ARCTIC"
-         *   },
-         * ]
-         * @example
-         * {
-         *   "category A": [
-         *     {
-         *       icon: "flag",
-         *       label: "Flag",
-         *       description: "This is a flag"
-         *     },
-         *     {
-         *       icon: "gift",
-         *       label: "Gift",
-         *       description: "This is a gift"
-         *     }
-         *   ],
-         *   "category B": [
-         *     {
-         *       icon: "pencil",
-         *       label: "Pencil",
-         *       description: "This is a pencil"
-         *     },
-         *     {
-         *       icon: "hospital",
-         *       label: "Hospital",
-         *       description: "This is a hospital"
-         *     }
-         *   ]
-         * }
-         */
-        options: [],
-
-        /**
-         * The values that a user has selected. If provided to the view upon
-         * initialization, the values will be pre-selected. Selected values must
-         * exist as a label in the options {@link SearchableSelect#options}
-         * @type {string[]}
-         */
-        selected: [],
-
-        /**
-         * Can be set to an object to specify API settings for retrieving remote selection
-         * menu content from an API endpoint. Details of what can be set here are
-         * specified by the Semantic-UI / Fomantic-UI package. Set to false if not
-         * retrieving remote content.
-         * @type {Object|booealn}
-         * @default false
-         * @since 2.15.0
-         * @see {@link https://fomantic-ui.com/modules/dropdown.html#remote-settings}
-         * @see {@link https://fomantic-ui.com/behaviors/api.html#/settings}
-         */
-        apiSettings: false,
-
-        /**
-         * The primary HTML template for this view. The template follows the
-         * structure specified for the semanticUI dropdown module, see:
-         * https://semantic-ui.com/modules/dropdown.html#/definition
-         * @type {Underscore.template}
-         */
-        template: _.template(Template),
-
-        /**
-         * Creates a new SearchableSelectView
-         * @param {Object} options - A literal object with options to pass to the view
-         */
-        initialize: function(options) {
-
-          try {
-
-            // Add CSS required for this view
-            MetacatUI.appModel.addCSS(TransitionCSS, "semanticUItransition");
-            MetacatUI.appModel.addCSS(DropdownCSS, "semanticUIdropdown");
-
-            // If pre-selected values that are passed to this view are also attached to a
-            // model (e.g. when they were passed to this view as {selected:
-            // parentView.model.get("values")}), then it's important that we use a clone
-            // instead. Otherwise this view may silently update the model, and important
-            // events may not be triggered.
-            if(options.selected){
-              options.selected = _.clone(options.selected);
-            }
+  "jquery",
+  "underscore",
+  "backbone",
+  "semanticUItransition",
+  "text!" + MetacatUI.root + "/components/semanticUI/transition.min.css",
+  "semanticUIdropdown",
+  "text!" + MetacatUI.root + "/components/semanticUI/dropdown.min.css",
+  "text!templates/selectUI/searchableSelect.html",
+], function (
+  $,
+  _,
+  Backbone,
+  Transition,
+  TransitionCSS,
+  Dropdown,
+  DropdownCSS,
+  Template,
+) {
+  /**
+   * @class SearchableSelectView
+   * @classdesc A select interface that allows the user to search from within
+   * the options, and optionally select multiple items. Also allows the items
+   * to be grouped, and to display an icon or image for each item.
+   * @classcategory Views/SearchSelect
+   * @extends Backbone.View
+   * @constructor
+   * @since 2.14.0
+   * @screenshot views/searchSelect/SearchableSelectView.png
+   */
+  return Backbone.View.extend(
+    /** @lends SearchableSelectView.prototype */
+    {
+      /**
+       * The type of View this is
+       * @type {string}
+       */
+      type: "SearchableSelect",
+
+      /**
+       * The HTML class names for this view element
+       * @type {string}
+       */
+      className: "searchable-select",
+
+      /**
+       * Text to show in the input field before any value has been entered
+       * @type {string}
+       */
+      placeholderText: "Search for or select a value",
+
+      /**
+       * Label for the input element
+       * @type {string}
+       */
+      inputLabel: "Select a value",
+
+      /**
+       * Whether to allow users to select more than one value
+       * @type {boolean}
+       */
+      allowMulti: true,
+
+      /**
+       * Setting to true gives users the ability to add their own options that
+       * are not listed in this.options. This can work with either single
+       * or multiple search select dropdowns
+       * @type {boolean}
+       */
+      allowAdditions: false,
+
+      /**
+       * Whether the dropdown value can be cleared by the user after being
+       * selected.
+       * @type {boolean}
+       */
+      clearable: true,
+
+      /**
+       * When items are grouped within categories, this attribute determines how to display the items
+       * within each category.
+       * @type {string}
+       * @example
+       * // display the items in a traditional, non-interactive list below category titles
+       * "list"
+       * @example
+       * // initially show only a list of category titles, and popout
+       * // a submenu on the left or right when the user hovers over
+       * // or touches a category (can lead to the sub-menu being hidden
+       * // on mobile devices if the element is wide)
+       * "popout"
+       * @example
+       * // initially show only a list of category titles, and expand
+       * // the list of items below each category when a user clicks
+       * // on the category title, much like an "accordion" element.
+       * "accordion"
+       * @default "list"
+       */
+      submenuStyle: "list",
+
+      /**
+       * Set to false to always display category headers in the dropdown,
+       * even if there are no results in that category when a user is searching.
+       * @type {boolean}
+       */
+      hideEmptyCategoriesOnSearch: true,
+
+      /**
+       * The maximum width of images used for each option, in pixels
+       * @type {number}
+       */
+      imageWidth: 30,
+
+      /**
+       * The maximum height of images used for each option, in pixels
+       * @type {number}
+       */
+      imageHeight: 30,
+
+      /**
+       * For select inputs where multiple values are allowed
+       * ({@link SearchableSelectView#allowMulti} is true), optional text to insert
+       * between labels. Separator text is useful for indicating operators in filter
+       * fields or values.
+       * @type {string}
+       * @since 2.15.0
+       */
+      separatorText: "",
+
+      /**
+       * For select inputs where multiple values are allowed
+       * ({@link SearchableSelectView#allowMulti} is true), a list of
+       * {@link SearchableSelectView#separatorText} options. If a list is provided here
+       * (AND a value is provided for the {@link SearchableSelectView#separatorText}
+       * option), then a user can click on the separator text between two values to
+       * change the text to the next string in this list. If separatorTextOptions is
+       * false (or if there is no separatorText value), then changing the separator text
+       * is not possible. This view will trigger a "separatorChanged" event when the
+       * separator is updated.
+       * @type {string[]}
+       * @since 2.17.0
+       */
+      separatorTextOptions: ["AND", "OR"],
+
+      /**
+       * The HTML class name to add to the separator elements that are created for this
+       * view.
+       * @type {string}
+       * @since 2.15.0
+       */
+      separatorClass: "separator",
+
+      /**
+       * An additional HTML class to add to separator elements on hover when a user can
+       * click that element to switch the text.
+       * @type {string}
+       * @since 2.17.0
+       */
+      changeableSeparatorClass: "changeable-separator",
+
+      /**
+       * For separators that are changeable (see
+       * {@link SearchableSelectView#separatorTextOptions}), optional tooltip text to
+       * show when a user hovers over a separator element.
+       * @type {string}
+       * @since 2.17.0
+       */
+      changeableSeparatorTooltip: "Click to switch the operator",
+
+      /**
+       * The list of options that a user can select from in the dropdown menu. For
+       * un-categorized options, provide an array of objects, where each object is a
+       * single option. To create category headings, provide an object containing named
+       * objects, where the key for each object is the category title to display, and
+       * the value of each object comprises the option properties.
+       * @name SearchableSelectView#options
+       * @type {Object[]|Object}
+       * @property {string} icon - The name of a Font Awesome 3.2.1 icon to display to
+       * the left of the label (e.g. "lemon", "heart")
+       * @property {string} image - The complete path to an image to use instead of an
+       * icon. If both icon and image are provided, the icon will be used.
+       * @property {string} label - The label to show for the option
+       * @property {string} description - A description of the option, displayed as a
+       * tooltip when the user hovers over the label
+       * @property {string} value - If the value differs from the label, the value to
+       * return when this option is selected (otherwise label is returned)
+       * @example
+       * [
+       *   {
+       *     icon: "",
+       *     image: "https://www.dataone.org/uploads/member_node_logos/bcodmo_hu707c109c683d6da57b432522b4add783_33081_300x0_resize_box_2.png",
+       *     label: "BCO",
+       *     description: "The The Biological and Chemical Oceanography Data Management Office (BCO-DMO) serve data from research projects funded by the Biological and Chemical Oceanography Sections and the Division of Polar Programs Antarctic Organisms & Ecosystems Program at the U.S. National Science Foundation.",
+       *     value: "urn:node:BCODMO"
+       *   },
+       *   {
+       *     icon: "",
+       *     image: "https://www.dataone.org/uploads/member_node_logos/arctic.png",
+       *     label: "ADC",
+       *     description: "The US National Science Foundation Arctic Data Center operates as the primary repository supporting the NSF Arctic community for data preservation and access.",
+       *     value: "urn:node:ARCTIC"
+       *   },
+       * ]
+       * @example
+       * {
+       *   "category A": [
+       *     {
+       *       icon: "flag",
+       *       label: "Flag",
+       *       description: "This is a flag"
+       *     },
+       *     {
+       *       icon: "gift",
+       *       label: "Gift",
+       *       description: "This is a gift"
+       *     }
+       *   ],
+       *   "category B": [
+       *     {
+       *       icon: "pencil",
+       *       label: "Pencil",
+       *       description: "This is a pencil"
+       *     },
+       *     {
+       *       icon: "hospital",
+       *       label: "Hospital",
+       *       description: "This is a hospital"
+       *     }
+       *   ]
+       * }
+       */
+      options: [],
+
+      /**
+       * The values that a user has selected. If provided to the view upon
+       * initialization, the values will be pre-selected. Selected values must
+       * exist as a label in the options {@link SearchableSelect#options}
+       * @type {string[]}
+       */
+      selected: [],
+
+      /**
+       * Can be set to an object to specify API settings for retrieving remote selection
+       * menu content from an API endpoint. Details of what can be set here are
+       * specified by the Semantic-UI / Fomantic-UI package. Set to false if not
+       * retrieving remote content.
+       * @type {Object|booealn}
+       * @default false
+       * @since 2.15.0
+       * @see {@link https://fomantic-ui.com/modules/dropdown.html#remote-settings}
+       * @see {@link https://fomantic-ui.com/behaviors/api.html#/settings}
+       */
+      apiSettings: false,
+
+      /**
+       * The primary HTML template for this view. The template follows the
+       * structure specified for the semanticUI dropdown module, see:
+       * https://semantic-ui.com/modules/dropdown.html#/definition
+       * @type {Underscore.template}
+       */
+      template: _.template(Template),
+
+      /**
+       * Creates a new SearchableSelectView
+       * @param {Object} options - A literal object with options to pass to the view
+       */
+      initialize: function (options) {
+        try {
+          // Add CSS required for this view
+          MetacatUI.appModel.addCSS(TransitionCSS, "semanticUItransition");
+          MetacatUI.appModel.addCSS(DropdownCSS, "semanticUIdropdown");
+
+          // If pre-selected values that are passed to this view are also attached to a
+          // model (e.g. when they were passed to this view as {selected:
+          // parentView.model.get("values")}), then it's important that we use a clone
+          // instead. Otherwise this view may silently update the model, and important
+          // events may not be triggered.
+          if (options.selected) {
+            options.selected = _.clone(options.selected);
+          }
 
-            // If pre-selected values that are passed to this view are also attached to a
-            // model (e.g. when they were passed to this view as {selected:
-            // parentView.model.get("values")}), then it's important that we use a clone
-            // instead. Otherwise this view may silently update the model, and important
-            // events may not be triggered.
-            if(options.selected){
-              options.selected = _.clone(options.selected);
-            }
+          // If pre-selected values that are passed to this view are also attached to a
+          // model (e.g. when they were passed to this view as {selected:
+          // parentView.model.get("values")}), then it's important that we use a clone
+          // instead. Otherwise this view may silently update the model, and important
+          // events may not be triggered.
+          if (options.selected) {
+            options.selected = _.clone(options.selected);
+          }
 
-            // Get all the options and apply them to this view
-            if (typeof options == "object") {
-              var optionKeys = Object.keys(options);
-              _.each(optionKeys, function(key, i) {
+          // Get all the options and apply them to this view
+          if (typeof options == "object") {
+            var optionKeys = Object.keys(options);
+            _.each(
+              optionKeys,
+              function (key, i) {
                 this[key] = options[key];
-              }, this);
-            }
-
-          } catch (e) {
-            console.log("Failed to initialize a Searchable Select view, error message:",
-            e);
+              },
+              this,
+            );
+          }
+        } catch (e) {
+          console.log(
+            "Failed to initialize a Searchable Select view, error message:",
+            e,
+          );
+        }
+      },
+
+      /**
+       * Render the view
+       *
+       * @return {SearchableSelect}  Returns the view
+       */
+      render: function () {
+        try {
+          var view = this;
+
+          if (view.apiSettings && !view.semanticAPILoaded) {
+            require([
+              MetacatUI.root + "/components/semanticUI/api.min.js",
+            ], function (SemanticAPI) {
+              view.semanticAPILoaded = true;
+              view.render();
+            });
+            return;
           }
-        },
-
-        /**
-         * Render the view
-         *
-         * @return {SearchableSelect}  Returns the view
-         */
-        render: function() {
-
-          try {
-
-            var view = this;
-
-            if(view.apiSettings && !view.semanticAPILoaded){
-              require([MetacatUI.root + "/components/semanticUI/api.min.js"], function(SemanticAPI){
-                view.semanticAPILoaded = true
-                view.render();
-              })
-              return;
-            }
-
-            // Render the template using the view attributes
-            this.$el.html(this.template(this));
-
-            // Start the dropdown in a disabled state.
-            // This allows us to pre-select values without triggering a change
-            // event.
-            this.disable();
-            this.showLoading();
-
-            // Initialize the dropdown interface
-            // For explanations of settings, see:
-            // https://semantic-ui.com/modules/dropdown.html#/settings
-            this.$selectUI = this.$el.find('.ui.dropdown')
-              .dropdown({
-                keys : {
-                  // So that a user may enter search text using a comma
-                  delimiter  : false
-                },
-                apiSettings: this.apiSettings,
-                fullTextSearch: true,
-                duration: 90,
-                forceSelection: false,
-                ignoreDiacritics: true,
-                clearable: view.clearable,
-                allowAdditions: view.allowAdditions,
-                hideAdditions: false,
-                allowReselection: true,
-                onRemove: function(removedValue){
-                  // Callback when a value is removed *for multi-select inputs only*
-                  // Remove the value from the selected array
-                  view.selected = view.selected.filter(function(value){
-                    return value !== removedValue
-                  })
-                },
-                onLabelCreate: function(value, text){
-                   // Callback when a label is created *for multi-select inputs only*
-
-                  // Add the value to the selected array (but don't add twice). Do this in
-                  // the onLabelCreate callback instead of in the onAdd callback because
-                  // we would like to update the selected array before we create the
-                  // separator element (below).
-                  if(!view.selected.includes(value)){
-                    view.selected.push(value)
-                  }
-                  // Add a separator between labels if required.
-                  var label = this;
-                  if(view.separatorRequired.call(view)){
-                    // Create the separator element.
-                    var separator = view.createSeparator.call(view);
-                    if(separator){
-                      // Attach the separator to the label so that we can easily remove it
-                      // when the label is removed.
-                      label.data("separator", separator);
-                      // Add it before the label element.
-                      label = separator.add(label);
-                    }
-                  }
-                  return label
-                },
-                onLabelRemove(value){
-                  // Call back when a user deletes a label *for multi-select inputs only*
-                  var label = this;
-                  // Remove the separator before this label if there is one.
-                  var sep = label.data("separator")
-                  if(sep){
-                    sep.remove()
-                  }
-                  // If this is the first label in an input of at least two, then delete
-                  // the separator directly *after* this label - The label that's second
-                  // will become first, and should not have an separator before it.
-                  var allLabels = view.$selectUI.find(".label");
-                  if(allLabels.index(label) === 0){
-                    var separatorAfter = label.next("." + view.separatorClass);
-                    if(separatorAfter){
-                      separatorAfter.remove();
-                    }
-                  }
-                },
-                onChange: function(values, text, $choice){
-
-                  // Callback when values change for any type of input.
-
-                  // NOTE: The "values" argument is a string that contains all the
-                  // selected values separated by commas. We updated the view.selected
-                  // array with the onLabelCreate and onRemove callbacks instead of using
-                  // the values argument passed to this function in order to allow commas
-                  // within individual values. For example, if the user selected the value
-                  // "x" and the value "y,z", the values string would be "x,y,z" and it
-                  // would be difficult to see that two values were selected instead of
-                  // three.
-
-                  // Update values for single-select inputs (multi-select are updated
-                  // using the onLabelCreate and onRemove callbacks)
-                  if(!view.allowMulti){
-                    view.selected = [values]
-                  }
-
-                  // Trigger an event if items are selected after the UI has been rendered
-                  // (It is set as disabled until fully rendered).
-                  if(!$(this).hasClass("disabled")){
-                    var newValues = _.clone(view.selected);
-                    view.trigger('changeSelection', newValues);
-                  }
 
-                  // Refresh the tooltips on the labels/text
-
-                  // Ensure tooltips for labels are removed
-                  $(".search-select-tooltip").remove();
-
-                  // Add a tooltip for single select elements (.text) or multi-select
-                  // elements (.label). Delay so that to give time for DOM elements to be
-                  // added or removed.
-                  setTimeout(function(params) {
-                    var textEl = view.$selectUI.find(".text:not(.default),.label");
-                    // Single select text element will not have the value attribute, add
-                    // it so that we can find the matching description for the tooltip
-                    if(!textEl.data("value") && !view.allowMulti){
-                      textEl.data("value", values)
-                    }
-                    if(textEl){
-                      textEl.each(function(i, el){
-                        view.addTooltip.call(view, el, "top");
-                      })
-                    }
-                  }, 50);
-                },
+          // Render the template using the view attributes
+          this.$el.html(this.template(this));
+
+          // Start the dropdown in a disabled state.
+          // This allows us to pre-select values without triggering a change
+          // event.
+          this.disable();
+          this.showLoading();
+
+          // Initialize the dropdown interface
+          // For explanations of settings, see:
+          // https://semantic-ui.com/modules/dropdown.html#/settings
+          this.$selectUI = this.$el.find(".ui.dropdown").dropdown({
+            keys: {
+              // So that a user may enter search text using a comma
+              delimiter: false,
+            },
+            apiSettings: this.apiSettings,
+            fullTextSearch: true,
+            duration: 90,
+            forceSelection: false,
+            ignoreDiacritics: true,
+            clearable: view.clearable,
+            allowAdditions: view.allowAdditions,
+            hideAdditions: false,
+            allowReselection: true,
+            onRemove: function (removedValue) {
+              // Callback when a value is removed *for multi-select inputs only*
+              // Remove the value from the selected array
+              view.selected = view.selected.filter(function (value) {
+                return value !== removedValue;
               });
+            },
+            onLabelCreate: function (value, text) {
+              // Callback when a label is created *for multi-select inputs only*
+
+              // Add the value to the selected array (but don't add twice). Do this in
+              // the onLabelCreate callback instead of in the onAdd callback because
+              // we would like to update the selected array before we create the
+              // separator element (below).
+              if (!view.selected.includes(value)) {
+                view.selected.push(value);
+              }
+              // Add a separator between labels if required.
+              var label = this;
+              if (view.separatorRequired.call(view)) {
+                // Create the separator element.
+                var separator = view.createSeparator.call(view);
+                if (separator) {
+                  // Attach the separator to the label so that we can easily remove it
+                  // when the label is removed.
+                  label.data("separator", separator);
+                  // Add it before the label element.
+                  label = separator.add(label);
+                }
+              }
+              return label;
+            },
+            onLabelRemove(value) {
+              // Call back when a user deletes a label *for multi-select inputs only*
+              var label = this;
+              // Remove the separator before this label if there is one.
+              var sep = label.data("separator");
+              if (sep) {
+                sep.remove();
+              }
+              // If this is the first label in an input of at least two, then delete
+              // the separator directly *after* this label - The label that's second
+              // will become first, and should not have an separator before it.
+              var allLabels = view.$selectUI.find(".label");
+              if (allLabels.index(label) === 0) {
+                var separatorAfter = label.next("." + view.separatorClass);
+                if (separatorAfter) {
+                  separatorAfter.remove();
+                }
+              }
+            },
+            onChange: function (values, text, $choice) {
+              // Callback when values change for any type of input.
+
+              // NOTE: The "values" argument is a string that contains all the
+              // selected values separated by commas. We updated the view.selected
+              // array with the onLabelCreate and onRemove callbacks instead of using
+              // the values argument passed to this function in order to allow commas
+              // within individual values. For example, if the user selected the value
+              // "x" and the value "y,z", the values string would be "x,y,z" and it
+              // would be difficult to see that two values were selected instead of
+              // three.
+
+              // Update values for single-select inputs (multi-select are updated
+              // using the onLabelCreate and onRemove callbacks)
+              if (!view.allowMulti) {
+                view.selected = [values];
+              }
 
-            view.$selectUI.data("view", view);
+              // Trigger an event if items are selected after the UI has been rendered
+              // (It is set as disabled until fully rendered).
+              if (!$(this).hasClass("disabled")) {
+                var newValues = _.clone(view.selected);
+                view.trigger("changeSelection", newValues);
+              }
 
-            view.postRender();
+              // Refresh the tooltips on the labels/text
 
-            return this;
+              // Ensure tooltips for labels are removed
+              $(".search-select-tooltip").remove();
 
-          } catch (e) {
-            console.log("Error rendering the search select, error message: ", e);
+              // Add a tooltip for single select elements (.text) or multi-select
+              // elements (.label). Delay so that to give time for DOM elements to be
+              // added or removed.
+              setTimeout(function (params) {
+                var textEl = view.$selectUI.find(".text:not(.default),.label");
+                // Single select text element will not have the value attribute, add
+                // it so that we can find the matching description for the tooltip
+                if (!textEl.data("value") && !view.allowMulti) {
+                  textEl.data("value", values);
+                }
+                if (textEl) {
+                  textEl.each(function (i, el) {
+                    view.addTooltip.call(view, el, "top");
+                  });
+                }
+              }, 50);
+            },
+          });
+
+          view.$selectUI.data("view", view);
+
+          view.postRender();
+
+          return this;
+        } catch (e) {
+          console.log("Error rendering the search select, error message: ", e);
+        }
+      },
+
+      /**
+       * Change the options available in the dropdown menu and re-render.
+       * @param {SearchableSelectView#options} options - The new options
+       * @since 2.24.0
+       */
+      updateOptions: function (options) {
+        this.options = options;
+        this.render();
+      },
+
+      /**
+       * Checks whether a separator should be created for the label that was just
+       * created, but not yet attached to the DOM
+       * @return {boolean} - Returns true if a separator should be created, false
+       * otherwise.
+       * @since 2.15.0
+       */
+      separatorRequired: function () {
+        try {
+          if (
+            // Separators not required if only one selection is allowed
+            !this.allowMulti ||
+            // Need separator text to create a separator element
+            !this.separatorText ||
+            // Need the list of selected values to determine the value's position
+            !this.selected ||
+            // Separator is only required between two or more values
+            this.selected.length <= 1 ||
+            // Separator is only required after the first element has been added
+            this.$selectUI.find(".label").length === 0
+          ) {
+            return false;
+          } else {
+            return true;
           }
-        },
-
-        /**
-         * Change the options available in the dropdown menu and re-render.
-         * @param {SearchableSelectView#options} options - The new options
-         * @since 2.24.0
-         */
-        updateOptions: function (options) {
-          this.options = options;
-          this.render();
-        },
-
-        /**
-         * Checks whether a separator should be created for the label that was just
-         * created, but not yet attached to the DOM
-         * @return {boolean} - Returns true if a separator should be created, false
-         * otherwise.
-         * @since 2.15.0
-         */
-        separatorRequired: function(){
-          try {
-            if(
-              // Separators not required if only one selection is allowed
-              !this.allowMulti ||
-              // Need separator text to create a separator element
-              !this.separatorText ||
-              // Need the list of selected values to determine the value's position
-              !this.selected ||
-              // Separator is only required between two or more values
-              this.selected.length <= 1 ||
-              // Separator is only required after the first element has been added
-              this.$selectUI.find(".label").length === 0
-            ){
-              return false
-            } else {
-              return true
-            }
-          } catch (error) {
-            console.log("Error checking if a label in a searchable select input " +
-            "requires a separator. Assuming that it does not need one. Error details: " +
-            error);
-            return false
+        } catch (error) {
+          console.log(
+            "Error checking if a label in a searchable select input " +
+              "requires a separator. Assuming that it does not need one. Error details: " +
+              error,
+          );
+          return false;
+        }
+      },
+
+      /**
+       * Create the HTML for a separator element to insert between two labels. The
+       * view.separatorClass is added to the separator element.
+       * @return {JQuery} Returns the separator as a jQuery element
+       * @since 2.15.0
+       */
+      createSeparator: function () {
+        try {
+          var view = this;
+          var separatorText = this.separatorText;
+          // Text is required to create a separator.
+          if (!separatorText) {
+            return null;
           }
-        },
-
-        /**
-         * Create the HTML for a separator element to insert between two labels. The
-         * view.separatorClass is added to the separator element.
-         * @return {JQuery} Returns the separator as a jQuery element
-         * @since 2.15.0
-         */
-        createSeparator: function(){
-          try {
-            var view = this;
-            var separatorText = this.separatorText;
-            // Text is required to create a separator.
-            if(!separatorText){
-              return null
+          var separator = $("<span>" + separatorText + "</span>");
+          separator.addClass(this.separatorClass);
+
+          // Set a listener to change the text to one of the separatorText
+          // options on click, and to highlight all the separators when one is hovered
+          var separatorElHovered = false;
+          if (view.separatorTextOptions && view.separatorTextOptions.length) {
+            // Indicate that the separator is clickable
+            separator.css("cursor", "pointer");
+            // Make sure the listeners set below are only set once
+            separator.off("click mouseenter mouseout");
+            // Change all the separator text when one is clicked
+            separator.on("click", function () {
+              view.changeSeparator();
+            });
+            // Create the tooltip
+            if (view.changeableSeparatorTooltip) {
+              $(separator).tooltip("destroy");
+              $(separator).tooltip({
+                title: view.changeableSeparatorTooltip,
+                trigger: "manual",
+              });
             }
-            var separator = $("<span>" + separatorText + "</span>");
-            separator.addClass(this.separatorClass);
-            
-            // Set a listener to change the text to one of the separatorText
-            // options on click, and to highlight all the separators when one is hovered
-            var separatorElHovered = false
-            if (view.separatorTextOptions && view.separatorTextOptions.length) {
-              // Indicate that the separator is clickable
-              separator.css('cursor', 'pointer');
-              // Make sure the listeners set below are only set once
-              separator.off("click mouseenter mouseout");
-              // Change all the separator text when one is clicked
-              separator.on("click", function () {
-                view.changeSeparator();
-              })
-              // Create the tooltip
-              if (view.changeableSeparatorTooltip) {
-                $(separator).tooltip('destroy');
-                $(separator).tooltip({
-                  title: view.changeableSeparatorTooltip,
-                  trigger: 'manual',
-                })
-              }
-              // Highlight all of the separator elements when one is hovered
-              separator.on("mouseenter", function () {
-                var separatorEls = view.$el.find("." + view.separatorClass)
-                separatorElHovered = true;
-                // Add a delay before the highlight class is added
-                setTimeout(function () {
-                  if (separatorElHovered){
-                    separatorEls.addClass(view.changeableSeparatorClass);
-                    if (view.changeableSeparatorTooltip){
-                      // Add an even longer delay before the tooltip is shown
-                      setTimeout(function () {
-                        if (separatorElHovered) {
-                          $(separator).tooltip('show')
-                        }
-                      }, 600);
-                    }
+            // Highlight all of the separator elements when one is hovered
+            separator.on("mouseenter", function () {
+              var separatorEls = view.$el.find("." + view.separatorClass);
+              separatorElHovered = true;
+              // Add a delay before the highlight class is added
+              setTimeout(function () {
+                if (separatorElHovered) {
+                  separatorEls.addClass(view.changeableSeparatorClass);
+                  if (view.changeableSeparatorTooltip) {
+                    // Add an even longer delay before the tooltip is shown
+                    setTimeout(function () {
+                      if (separatorElHovered) {
+                        $(separator).tooltip("show");
+                      }
+                    }, 600);
                   }
-                }, 285);
-              })
-              // Hide all the tooltips and remove the highlight class on mouse out
-              separator.on("mouseout", function () {
-                separatorElHovered = false;
-                var separatorEls = view.$el.find("." + view.separatorClass)
-                separatorEls.removeClass(view.changeableSeparatorClass)
-                separatorEls.tooltip('hide')
-              })
-            }
-            return separator
-          } catch (error) {
-            console.log("There was an error creating a separator element in a " +
-              "Searchable Select View. Error details: " + error);
-          }
-        },
-
-        /**
-         * Changes the separator text for all separator elements to the next value that's
-         * set in the {@link SearchableSelectView#separatorTextOptions}. Triggers a
-         * "separatorChanged" event that passes on the new separator value.
-         */
-        changeSeparator: function(){
-          try {
-            var view = this;
-            if (
-              !view.separatorTextOptions ||
-              !view.separatorTextOptions.length ||
-              !view.separatorText
-            ){
-              return
-            }
-            // Get the next separator text option
-            var currentIndex = view.separatorTextOptions.indexOf(view.separatorText),
-                nextIndex = currentIndex + 1;
-            if (currentIndex === -1 || !view.separatorTextOptions[nextIndex] ){
-              nextIndex = 0
-            }
-            // Update the current separator text on the view
-            view.separatorText = view.separatorTextOptions[nextIndex]
-            // Change the separator text for all of the separators in the view with an
-            // animation
-            var separatorEls = view.$el.find("." + view.separatorClass)
-            separatorEls.transition({
-              animation: 'pulse',
-              displayType: 'inline-block',
-              duration: '250ms',
-              onComplete: function(){
-                $(this).text(view.separatorText)
-              }
+                }
+              }, 285);
+            });
+            // Hide all the tooltips and remove the highlight class on mouse out
+            separator.on("mouseout", function () {
+              separatorElHovered = false;
+              var separatorEls = view.$el.find("." + view.separatorClass);
+              separatorEls.removeClass(view.changeableSeparatorClass);
+              separatorEls.tooltip("hide");
             });
-            // Trigger an event for parent views
-            view.trigger("separatorChanged", view.separatorText);
           }
-          catch (error) {
-            console.log(
-              'There was an error switching the separator text in a SearchableSelectView' +
-              '. Error details: ' + error
-            );
+          return separator;
+        } catch (error) {
+          console.log(
+            "There was an error creating a separator element in a " +
+              "Searchable Select View. Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * Changes the separator text for all separator elements to the next value that's
+       * set in the {@link SearchableSelectView#separatorTextOptions}. Triggers a
+       * "separatorChanged" event that passes on the new separator value.
+       */
+      changeSeparator: function () {
+        try {
+          var view = this;
+          if (
+            !view.separatorTextOptions ||
+            !view.separatorTextOptions.length ||
+            !view.separatorText
+          ) {
+            return;
           }
-        },
-
-        /**
-         * updateMenu - Re-render the menu of options. Useful after changing
-         * the options that are set on the view.
-         */
-        updateMenu: function(){
-          try {
-            var menu = $(this.template(this).trim()).find(".menu")[0].innerHTML;
-            this.$el.find(".menu").html(menu);
-          } catch (e) {
-            console.log("Failed to update a searchable select menu, error message: " + e);
+          // Get the next separator text option
+          var currentIndex = view.separatorTextOptions.indexOf(
+              view.separatorText,
+            ),
+            nextIndex = currentIndex + 1;
+          if (currentIndex === -1 || !view.separatorTextOptions[nextIndex]) {
+            nextIndex = 0;
           }
-        },
-
-        /**
-         * postRender - Updates to the view once the dropdown UI has loaded
-         */
-        postRender: function(){
-          try {
-
-            var view = this;
-            view.trigger("postRender");
-
-            // Add tool tips for the description
-            this.$el.find(".item").each(function(){
-              view.addTooltip(this)
-            });
-
-            // Show an error message if the pre-selected options are not in the
-            // list of available options (only if user additions are not allowed)
-            if(!view.allowAdditions){
-              if(view.selected && view.selected.length){
-                var invalidOptions = [];
-                view.selected.forEach(function(item){
-                  if(!view.isValidOption(item)){
-                    invalidOptions.push(item)
-                  }
-                });
-                if(invalidOptions.length){
-                  var optionsString = "\"" + invalidOptions.join(", ") + "\"";
-                  var phrase = (invalidOptions.length === 1) ? "is not a valid option" : "are not valid options";
-                  var ending = ". Please change selection."
-                  var message = optionsString + " " + phrase + ending;
-                  view.showMessage(message, "error", true);
+          // Update the current separator text on the view
+          view.separatorText = view.separatorTextOptions[nextIndex];
+          // Change the separator text for all of the separators in the view with an
+          // animation
+          var separatorEls = view.$el.find("." + view.separatorClass);
+          separatorEls.transition({
+            animation: "pulse",
+            displayType: "inline-block",
+            duration: "250ms",
+            onComplete: function () {
+              $(this).text(view.separatorText);
+            },
+          });
+          // Trigger an event for parent views
+          view.trigger("separatorChanged", view.separatorText);
+        } catch (error) {
+          console.log(
+            "There was an error switching the separator text in a SearchableSelectView" +
+              ". Error details: " +
+              error,
+          );
+        }
+      },
+
+      /**
+       * updateMenu - Re-render the menu of options. Useful after changing
+       * the options that are set on the view.
+       */
+      updateMenu: function () {
+        try {
+          var menu = $(this.template(this).trim()).find(".menu")[0].innerHTML;
+          this.$el.find(".menu").html(menu);
+        } catch (e) {
+          console.log(
+            "Failed to update a searchable select menu, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * postRender - Updates to the view once the dropdown UI has loaded
+       */
+      postRender: function () {
+        try {
+          var view = this;
+          view.trigger("postRender");
+
+          // Add tool tips for the description
+          this.$el.find(".item").each(function () {
+            view.addTooltip(this);
+          });
+
+          // Show an error message if the pre-selected options are not in the
+          // list of available options (only if user additions are not allowed)
+          if (!view.allowAdditions) {
+            if (view.selected && view.selected.length) {
+              var invalidOptions = [];
+              view.selected.forEach(function (item) {
+                if (!view.isValidOption(item)) {
+                  invalidOptions.push(item);
                 }
+              });
+              if (invalidOptions.length) {
+                var optionsString = '"' + invalidOptions.join(", ") + '"';
+                var phrase =
+                  invalidOptions.length === 1
+                    ? "is not a valid option"
+                    : "are not valid options";
+                var ending = ". Please change selection.";
+                var message = optionsString + " " + phrase + ending;
+                view.showMessage(message, "error", true);
               }
             }
+          }
 
-            // Set the selected values in the dropdown
-            this.$selectUI.dropdown('set exactly', view.selected);
-            this.$selectUI.dropdown('save defaults');
-            this.enable();
-            this.hideLoading();
-
-            // Make sub-menus if the option is configured in this view
-            if(this.submenuStyle === "popout"){
-              this.convertToPopout();
-            }
-            else if (this.submenuStyle === "accordion"){
-              this.convertToAccordion();
-            }
+          // Set the selected values in the dropdown
+          this.$selectUI.dropdown("set exactly", view.selected);
+          this.$selectUI.dropdown("save defaults");
+          this.enable();
+          this.hideLoading();
+
+          // Make sub-menus if the option is configured in this view
+          if (this.submenuStyle === "popout") {
+            this.convertToPopout();
+          } else if (this.submenuStyle === "accordion") {
+            this.convertToAccordion();
+          }
 
-            // Convert interactive submenus to lists and hide empty categories
-            // when the user is searching for a term
-            if(
-              ["popout", "accordion"].includes(view.submenuStyle) ||
-              view.hideEmptyCategoriesOnSearch
-            ){
-              this.$selectUI.find("input").on("keyup blur", function(e){
-
-                inputVal = e.target.value;
-
-                // When the input is NOT empty
-                if(inputVal !== ""){
-                  // For interactive type submenus where items are sometimes
-                  // hidden, show all the matching items when a user is searching
-                  if(["popout", "accordion"].includes(view.submenuStyle)){
-                    view.convertToList();
-                  }
-                  if(view.hideEmptyCategoriesOnSearch){
-                    view.hideEmptyCategories();
-                  }
+          // Convert interactive submenus to lists and hide empty categories
+          // when the user is searching for a term
+          if (
+            ["popout", "accordion"].includes(view.submenuStyle) ||
+            view.hideEmptyCategoriesOnSearch
+          ) {
+            this.$selectUI.find("input").on("keyup blur", function (e) {
+              inputVal = e.target.value;
+
+              // When the input is NOT empty
+              if (inputVal !== "") {
+                // For interactive type submenus where items are sometimes
+                // hidden, show all the matching items when a user is searching
+                if (["popout", "accordion"].includes(view.submenuStyle)) {
+                  view.convertToList();
+                }
+                if (view.hideEmptyCategoriesOnSearch) {
+                  view.hideEmptyCategories();
+                }
 
                 // When the input is EMPTY
-                } else {
-                  // Convert back to sub-menus if the option is configured in this view
-                  if(view.submenuStyle === "popout"){
-                    view.convertToPopout();
-                  }
-                  else if (view.submenuStyle === "accordion"){
-                    view.convertToAccordion();
-                  }
-                  // Show all the category titles again, in cases some where hidden
-                  if(view.hideEmptyCategoriesOnSearch){
-                    view.showAllCategories();
-                  }
+              } else {
+                // Convert back to sub-menus if the option is configured in this view
+                if (view.submenuStyle === "popout") {
+                  view.convertToPopout();
+                } else if (view.submenuStyle === "accordion") {
+                  view.convertToAccordion();
                 }
-              });
-            }
+                // Show all the category titles again, in cases some where hidden
+                if (view.hideEmptyCategoriesOnSearch) {
+                  view.showAllCategories();
+                }
+              }
+            });
+          }
 
-            // Trigger an event when the user focuses in searchable inputs
-            var inputEl = this.$el.find("input.search")
-            if(inputEl){
-              inputEl.off("focus");
-              inputEl.on("focus", function(event){
-                view.trigger("inputFocus", event)
-              })
-            }
+          // Trigger an event when the user focuses in searchable inputs
+          var inputEl = this.$el.find("input.search");
+          if (inputEl) {
+            inputEl.off("focus");
+            inputEl.on("focus", function (event) {
+              view.trigger("inputFocus", event);
+            });
+          }
+        } catch (e) {
+          console.log(
+            "The searchable select post-render function failed, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * isValidOption - Checks if a value is one of the values given in view.options
+       *
+       * @param  {string} value The value to check
+       * @return {boolean}      returns true if the value is one of the values given in
+       * view.options
+       */
+      isValidOption: function (value) {
+        try {
+          var view = this;
+          var options = view.options;
+
+          // If there are no options set on the view, assume the value is invalid
+          if (!options || options.length === 0) {
+            return false;
+          }
 
-          } catch (e) {
-            console.log("The searchable select post-render function failed, error message: " + e);
+          // If the list of options doesn't have category headings, put it in the
+          // same format as options that do have headings.
+          if (Array.isArray(options)) {
+            options = { "": options };
           }
-        },
-
-        /**
-         * isValidOption - Checks if a value is one of the values given in view.options
-         *
-         * @param  {string} value The value to check
-         * @return {boolean}      returns true if the value is one of the values given in
-         * view.options
-         */
-        isValidOption: function(value){
-
-          try {
-            var view = this;
-            var options = view.options;
-
-            // If there are no options set on the view, assume the value is invalid
-            if(!options || options.length === 0){
-              return false
-            }
 
-            // If the list of options doesn't have category headings, put it in the
-            // same format as options that do have headings.
-            if (Array.isArray(options)) { options = { "" : options } };
-
-            // Reduce the options object to just an Array of value and label strings
-            var validValues = _(options)
-              .chain()
-              .values()
-              .flatten()
-              .map(function(item){
-                var items = [];
-                if(item.value !== undefined ){ items.push(item.value) }
-                if(item.label !== undefined ){ items.push(item.label) }
-                return items
-              })
-              .flatten()
-              .value();
-
-            return validValues.includes(value);
-          } catch (e) {
-            console.log("Failed to check if an option is valid in a Searchable Select View, error message: " + e);
+          // Reduce the options object to just an Array of value and label strings
+          var validValues = _(options)
+            .chain()
+            .values()
+            .flatten()
+            .map(function (item) {
+              var items = [];
+              if (item.value !== undefined) {
+                items.push(item.value);
+              }
+              if (item.label !== undefined) {
+                items.push(item.label);
+              }
+              return items;
+            })
+            .flatten()
+            .value();
+
+          return validValues.includes(value);
+        } catch (e) {
+          console.log(
+            "Failed to check if an option is valid in a Searchable Select View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * addTooltip - Add a tooltip to a given element using the description in the
+       * options object that's set on the view.
+       *
+       * @param  {HTMLElement} element The HTML element a tooltip should be added
+       * @param  {string} position how to position the tooltip - top | bottom | left |
+       * right
+       * @return {jQuery} The element with a tooltip wrapped by jQuery
+       */
+      addTooltip: function (element, position = "bottom") {
+        try {
+          if (!element) {
+            return;
           }
 
-        },
-
-        /**
-         * addTooltip - Add a tooltip to a given element using the description in the
-         * options object that's set on the view.
-         *
-         * @param  {HTMLElement} element The HTML element a tooltip should be added
-         * @param  {string} position how to position the tooltip - top | bottom | left |
-         * right
-         * @return {jQuery} The element with a tooltip wrapped by jQuery
-         */
-        addTooltip: function(element, position = "bottom"){
-
-          try {
-            if(!element){
-              return
-            }
+          // Find the description in the options object, using the data-value
+          // attribute set in the template. The data-value attribute is either
+          // the label, or the value, depending on if a value is provided.
+          var valueOrLabel = $(element).data("value");
+          if (typeof valueOrLabel === "undefined") {
+            return;
+          }
+          if (typeof valueOrLabel === "boolean") {
+            valueOrLabel = valueOrLabel.toString();
+          }
+          var opt = _.chain(this.options)
+            .values()
+            .flatten()
+            .find(function (option) {
+              return (
+                option.label == valueOrLabel || option.value == valueOrLabel
+              );
+            })
+            .value();
 
-            // Find the description in the options object, using the data-value
-            // attribute set in the template. The data-value attribute is either
-            // the label, or the value, depending on if a value is provided.
-            var valueOrLabel = $(element).data("value");
-            if(typeof valueOrLabel === "undefined"){
-              return
-            }
-            if(typeof valueOrLabel === "boolean"){
-              valueOrLabel = valueOrLabel.toString()
-            }
-            var opt = _.chain(this.options)
-                            .values()
-                            .flatten()
-                            .find(function(option){
-                              return option.label == valueOrLabel || option.value == valueOrLabel
-                            })
-                            .value()
-
-            if(!opt){
-              return
-            }
-            if(!opt.description){
-              return
-            }
+          if (!opt) {
+            return;
+          }
+          if (!opt.description) {
+            return;
+          }
 
-            $(element).tooltip({
+          $(element)
+            .tooltip({
               title: opt.description,
               placement: position,
               container: "body",
               delay: {
                 show: 900,
-                hide: 50
-              }
+                hide: 50,
+              },
             })
-            .on("show.bs.popover",
-              function(){
-                var $el = $(this);
-                // Allow time for the popup to be added to the DOM
-                setTimeout(function () {
-                  // Then add a special class to identify
-                  // these popups if they need to be removed.
-                  $el.data('tooltip').$tip.addClass("search-select-tooltip")
-                }, 10);
+            .on("show.bs.popover", function () {
+              var $el = $(this);
+              // Allow time for the popup to be added to the DOM
+              setTimeout(function () {
+                // Then add a special class to identify
+                // these popups if they need to be removed.
+                $el.data("tooltip").$tip.addClass("search-select-tooltip");
+              }, 10);
             });
 
-            return $(element)
-          } catch (e) {
-            console.log("Failed to add tooltip in a searchable select view, error message: " + e);
+          return $(element);
+        } catch (e) {
+          console.log(
+            "Failed to add tooltip in a searchable select view, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * convertToPopout - Re-arrange the HTML to display category contents
+       * as sub-menus that popout to the left or right of category titles
+       */
+      convertToPopout: function () {
+        try {
+          if (!this.$selectUI) {
+            return;
           }
-
-        },
-
-        /**
-         * convertToPopout - Re-arrange the HTML to display category contents
-         * as sub-menus that popout to the left or right of category titles
-         */
-        convertToPopout: function(){
-          try {
-            if(!this.$selectUI){
-              return
-            }
-            if(this.currentSubmenuMode === "popout"){
-              return
-            }
-            this.currentSubmenuMode = "popout";
-            this.$selectUI.addClass("popout-mode");
-            var $headers = this.$selectUI.find(".header");
-            if(!$headers || $headers.length === 0){
-              return
-            }
-            $headers.each(function(i){
-              var $itemGroup = $().add($(this).nextUntil(".header"));
-              var $itemAndHeaderGroup = $(this).add($(this).nextUntil(".header"));
-              var $icon = $(this).next().find(".icon");
-              if($icon && $icon.length > 0){
-                var $headerIcon = $icon
-                  .clone()
-                  .addClass("popout-mode-icon")
-                  .css({
-                    "opacity": "0.9",
-                    "margin-right" : "1rem"
-                  });
-                $(this).prepend($headerIcon[0])
-              }
-              $itemAndHeaderGroup.wrapAll("<div class='item popout-mode'/>");
-              $itemGroup.wrapAll("<div class='menu popout-mode'/>");
-              $(this).append("<i class='popout-mode-icon dropdown icon icon-on-right icon-chevron-right'></i>")
-            });
-          } catch (e) {
-            console.log("Failed to convert a Searchable Select interface to sub-menu mode, error message: " + e);
+          if (this.currentSubmenuMode === "popout") {
+            return;
           }
-        },
-
-        /**
-         * convertToList - Re-arrange HTML to display the full list of options
-         * in one static menu
-         */
-        convertToList: function(){
-          try {
-            if(!this.$selectUI){
-              return
-            }
-            if(this.currentSubmenuMode === "list"){
-              return
-            }
-            this.currentSubmenuMode = "list";
-            this.$selectUI.find(".popout-mode > *").unwrap();
-            this.$selectUI.find(".accordion-mode > *").unwrap();
-            this.$selectUI.find(".popout-mode-icon").remove();
-            this.$selectUI.find(".accordion-mode-icon").remove();
-            this.$selectUI.removeClass("popout-mode accordion-mode");
-          } catch (e) {
-            console.log("Failed to convert a Searchable Select interface to list mode, error message: " + e);
+          this.currentSubmenuMode = "popout";
+          this.$selectUI.addClass("popout-mode");
+          var $headers = this.$selectUI.find(".header");
+          if (!$headers || $headers.length === 0) {
+            return;
           }
-        },
-
-
-        /**
-         * convertToAccordion - Re-arrange the HTML to display category items
-         * with expandable sections, similar to an accordion element.
-         */
-        convertToAccordion: function(){
-
-          try {
-
-            if(!this.$selectUI){
-              return
-            }
-            if(this.currentSubmenuMode === "accordion"){
-              return
-            }
-            this.currentSubmenuMode = "accordion";
-            this.$selectUI.addClass("accordion-mode");
-            var $headers = this.$selectUI.find(".header");
-            if(!$headers || $headers.length === 0){
-              return
+          $headers.each(function (i) {
+            var $itemGroup = $().add($(this).nextUntil(".header"));
+            var $itemAndHeaderGroup = $(this).add($(this).nextUntil(".header"));
+            var $icon = $(this).next().find(".icon");
+            if ($icon && $icon.length > 0) {
+              var $headerIcon = $icon.clone().addClass("popout-mode-icon").css({
+                opacity: "0.9",
+                "margin-right": "1rem",
+              });
+              $(this).prepend($headerIcon[0]);
             }
-
-            // Id to match the header to the
-            $headers.each(function(i){
-
-              // Create an ID
-              var randomNum = Math.floor((Math.random() * 100000) + 1),
-                  headerText = $(this).text().replace(/\W/g, ''),
-                  id = headerText + randomNum;
-
-              var $itemGroup = $().add($(this).nextUntil(".header"));
-              var $icon = $(this).next().find(".icon");
-              if($icon && $icon.length > 0){
-                var $headerIcon = $icon
-                  .clone()
-                  .addClass("accordion-mode-icon")
-                  .css({
-                    "opacity": "0.9",
-                    "margin-right" : "1rem"
-                  });
-                $(this).prepend($headerIcon[0])
-                $(this).wrap("<a data-toggle='collapse' data-target='#" +
-                                id +
-                                "' class='accordion-mode collapsed'/>" )
-              }
-              $itemGroup.wrapAll("<div id='" + id + "' class='accordion-mode collapse'/>");
-              $(this).append("<i class='accordion-mode-icon dropdown icon icon-on-right icon-chevron-down'></i>");
-
-            });
-          } catch (e) {
-            console.log("Failed to convert a Searchable Select interface to accordion mode, error message: " + e);
+            $itemAndHeaderGroup.wrapAll("<div class='item popout-mode'/>");
+            $itemGroup.wrapAll("<div class='menu popout-mode'/>");
+            $(this).append(
+              "<i class='popout-mode-icon dropdown icon icon-on-right icon-chevron-right'></i>",
+            );
+          });
+        } catch (e) {
+          console.log(
+            "Failed to convert a Searchable Select interface to sub-menu mode, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * convertToList - Re-arrange HTML to display the full list of options
+       * in one static menu
+       */
+      convertToList: function () {
+        try {
+          if (!this.$selectUI) {
+            return;
           }
-        },
-
-        /**
-         * hideEmptyCategories - In the searchable select interface, hide
-         * category headers that are empty, if any
-         */
-        hideEmptyCategories: function(){
-          try {
-            var $headers = this.$selectUI.find(".header")
-            if(!$headers || $headers.length === 0){
-              return
-            }
-            $headers.each(function(i){
-              // this is the header
-              var $itemGroup = $().add($(this).nextUntil(".header"));
-              var $itemGroupFiltered = $().add($(this).nextUntil(".header", ".filtered"));
-              // If all items are filtered
-              if($itemGroup.length === $itemGroupFiltered.length){
-                // Then also hide the header
-                $(this).hide()
-              } else {
-                $(this).show()
-              }
-            });
-          } catch (e) {
-            console.log("Failed to hide empty categories in a dropdown, error message: " + e);
+          if (this.currentSubmenuMode === "list") {
+            return;
           }
-        },
-
-        /**
-         * showAllCategories - In the searchable select interface, show all
-         * category headers that were previously empty
-         */
-        showAllCategories: function(){
-          try {
-            this.$selectUI.find(".header:hidden").show();
-          } catch (e) {
-            console.log("Failed to show all categories in a dropdown, error message: " + e);
+          this.currentSubmenuMode = "list";
+          this.$selectUI.find(".popout-mode > *").unwrap();
+          this.$selectUI.find(".accordion-mode > *").unwrap();
+          this.$selectUI.find(".popout-mode-icon").remove();
+          this.$selectUI.find(".accordion-mode-icon").remove();
+          this.$selectUI.removeClass("popout-mode accordion-mode");
+        } catch (e) {
+          console.log(
+            "Failed to convert a Searchable Select interface to list mode, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * convertToAccordion - Re-arrange the HTML to display category items
+       * with expandable sections, similar to an accordion element.
+       */
+      convertToAccordion: function () {
+        try {
+          if (!this.$selectUI) {
+            return;
           }
-        },
-
-        /**
-         * changeSelection - Set selected values in the interface
-         *
-         * @param  {string[]} newValues - An array of strings to select
-         */
-        changeSelection: function(newValues, silent = false) {
-          try {
-            if(
-              !this.$selectUI ||
-              typeof newValues === "undefined" ||
-              !Array.isArray(newValues)
-            ){
-              return
-            }
-            var view = this;
-            this.selected = newValues;
-            if(silent === true){
-              view.disable();
+          if (this.currentSubmenuMode === "accordion") {
+            return;
+          }
+          this.currentSubmenuMode = "accordion";
+          this.$selectUI.addClass("accordion-mode");
+          var $headers = this.$selectUI.find(".header");
+          if (!$headers || $headers.length === 0) {
+            return;
+          }
+
+          // Id to match the header to the
+          $headers.each(function (i) {
+            // Create an ID
+            var randomNum = Math.floor(Math.random() * 100000 + 1),
+              headerText = $(this).text().replace(/\W/g, ""),
+              id = headerText + randomNum;
+
+            var $itemGroup = $().add($(this).nextUntil(".header"));
+            var $icon = $(this).next().find(".icon");
+            if ($icon && $icon.length > 0) {
+              var $headerIcon = $icon
+                .clone()
+                .addClass("accordion-mode-icon")
+                .css({
+                  opacity: "0.9",
+                  "margin-right": "1rem",
+                });
+              $(this).prepend($headerIcon[0]);
+              $(this).wrap(
+                "<a data-toggle='collapse' data-target='#" +
+                  id +
+                  "' class='accordion-mode collapsed'/>",
+              );
             }
-            this.$selectUI.dropdown('set exactly', newValues);
-            if(silent === true){
-              view.enable();
+            $itemGroup.wrapAll(
+              "<div id='" + id + "' class='accordion-mode collapse'/>",
+            );
+            $(this).append(
+              "<i class='accordion-mode-icon dropdown icon icon-on-right icon-chevron-down'></i>",
+            );
+          });
+        } catch (e) {
+          console.log(
+            "Failed to convert a Searchable Select interface to accordion mode, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * hideEmptyCategories - In the searchable select interface, hide
+       * category headers that are empty, if any
+       */
+      hideEmptyCategories: function () {
+        try {
+          var $headers = this.$selectUI.find(".header");
+          if (!$headers || $headers.length === 0) {
+            return;
+          }
+          $headers.each(function (i) {
+            // this is the header
+            var $itemGroup = $().add($(this).nextUntil(".header"));
+            var $itemGroupFiltered = $().add(
+              $(this).nextUntil(".header", ".filtered"),
+            );
+            // If all items are filtered
+            if ($itemGroup.length === $itemGroupFiltered.length) {
+              // Then also hide the header
+              $(this).hide();
+            } else {
+              $(this).show();
             }
-          } catch (e) {
-            console.log("Failed to change the selected values in a searchable select field, error message: " + e);
+          });
+        } catch (e) {
+          console.log(
+            "Failed to hide empty categories in a dropdown, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * showAllCategories - In the searchable select interface, show all
+       * category headers that were previously empty
+       */
+      showAllCategories: function () {
+        try {
+          this.$selectUI.find(".header:hidden").show();
+        } catch (e) {
+          console.log(
+            "Failed to show all categories in a dropdown, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * changeSelection - Set selected values in the interface
+       *
+       * @param  {string[]} newValues - An array of strings to select
+       */
+      changeSelection: function (newValues, silent = false) {
+        try {
+          if (
+            !this.$selectUI ||
+            typeof newValues === "undefined" ||
+            !Array.isArray(newValues)
+          ) {
+            return;
           }
-        },
-
-        /**
-         * enable - Remove the class the makes the select UI appear disabled
-         */
-        enable: function(){
-          try {
-            this.$el.find('.ui.dropdown').removeClass("disabled");
-          } catch (e) {
-            console.log("Failed to enable the searchable select field, error message: " + e);
+          var view = this;
+          this.selected = newValues;
+          if (silent === true) {
+            view.disable();
           }
-        },
-
-        /**
-         * disable - Add the class the makes the select UI appear disabled
-         */
-        disable: function(){
-          try {
-            this.$el.find('.ui.dropdown').addClass("disabled");
-          } catch (e) {
-            console.log("Failed to enable the searchable select field, error message: " + e);
+          this.$selectUI.dropdown("set exactly", newValues);
+          if (silent === true) {
+            view.enable();
+          }
+        } catch (e) {
+          console.log(
+            "Failed to change the selected values in a searchable select field, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * enable - Remove the class the makes the select UI appear disabled
+       */
+      enable: function () {
+        try {
+          this.$el.find(".ui.dropdown").removeClass("disabled");
+        } catch (e) {
+          console.log(
+            "Failed to enable the searchable select field, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * disable - Add the class the makes the select UI appear disabled
+       */
+      disable: function () {
+        try {
+          this.$el.find(".ui.dropdown").addClass("disabled");
+        } catch (e) {
+          console.log(
+            "Failed to enable the searchable select field, error message: " + e,
+          );
+        }
+      },
+
+      /**
+       * showMessage - Show an error, warning, or informational message, and highlight
+       * the select interface in an appropriate colour.
+       *
+       * @param  {string} message The message to display. Use an empty string to only
+       * highlight the select interface without showing any message text.
+       * @param  {string} type one of "error", "warning", or "info"
+       * @param  {boolean} removeOnChange set to true to remove the message as soon as
+       * the user changes the selection
+       *
+       */
+      showMessage: function (message, type = "info", removeOnChange = true) {
+        try {
+          if (!this.$selectUI) {
+            console.warn(
+              "A select UI element wasn't found, can't display error.",
+            );
+            return;
           }
-        },
-
-        /**
-         * showMessage - Show an error, warning, or informational message, and highlight
-         * the select interface in an appropriate colour.
-         *
-         * @param  {string} message The message to display. Use an empty string to only
-         * highlight the select interface without showing any message text.
-         * @param  {string} type one of "error", "warning", or "info"
-         * @param  {boolean} removeOnChange set to true to remove the message as soon as
-         * the user changes the selection
-         *
-         */
-        showMessage: function(message, type = "info", removeOnChange = true){
-          try {
-
-            if(!this.$selectUI){
-              console.warn("A select UI element wasn't found, can't display error.");
-              return
-            }
-
-            var messageTypes = {
-              error: {
-                messageClass: "text-error",
-                selectUIClass: "error"
-              },
-              warning: {
-                messageClass: "text-warning",
-                selectUIClass: "warning"
-              },
-              info: {
-                messageClass: "text-info",
-                selectUIClass: ""
-              }
-            };
 
-            if(!messageTypes.hasOwnProperty(type)){
-              console.log(type + "is not a message type for Select UI interfaces. Showing message as info type");
-              type = "info"
-            }
+          var messageTypes = {
+            error: {
+              messageClass: "text-error",
+              selectUIClass: "error",
+            },
+            warning: {
+              messageClass: "text-warning",
+              selectUIClass: "warning",
+            },
+            info: {
+              messageClass: "text-info",
+              selectUIClass: "",
+            },
+          };
+
+          if (!messageTypes.hasOwnProperty(type)) {
+            console.log(
+              type +
+                "is not a message type for Select UI interfaces. Showing message as info type",
+            );
+            type = "info";
+          }
 
-            this.removeMessages();
-            this.$selectUI.addClass(messageTypes[type].selectUIClass);
+          this.removeMessages();
+          this.$selectUI.addClass(messageTypes[type].selectUIClass);
 
-            if(message && message.length && typeof message === "string"){
-              this.message = $(
-                "<p style='margin:0.2rem' class='" +
+          if (message && message.length && typeof message === "string") {
+            this.message = $(
+              "<p style='margin:0.2rem' class='" +
                 messageTypes[type].messageClass +
-                "'><small>" + message +
-                "</small></p>"
-              );
-            }
-
-            this.$el.append(this.message);
-
-            if(removeOnChange){
-              this.listenToOnce(this, "changeSelection", this.removeMessages);
-            }
-
-          } catch (e) {
-            console.log("Failed to show an error state in a Searchable Select View, error message: " + e);
+                "'><small>" +
+                message +
+                "</small></p>",
+            );
           }
-        },
-
-
-        /**
-         * removeMessages - Remove all messages and classes set by the
-         * showMessage function.
-         */
-        removeMessages: function(){
-          try {
-            if(!this.$selectUI){
-              console.warn("A select UI element wasn't found, can't remove error.");
-              return
-            }
 
-            this.$selectUI.removeClass("error warning");
-            if(this.message){
-              this.message.remove();
-            }
-          } catch (e) {
-            console.log("Failed to hide an error state in a Searchable Select View, error message: " + e);
-          }
-        },
-
-        /**
-         * showLoading - Indicate that dropdown options are loading by showing
-         * a spinner in the select interface
-         */
-        showLoading: function(){
-          try {
-            this.$el.find('.ui.dropdown').addClass("loading");
-          } catch (e) {
-            console.log("Failed to show a loading state in a Searchable Select View, error message: " + e);
+          this.$el.append(this.message);
+
+          if (removeOnChange) {
+            this.listenToOnce(this, "changeSelection", this.removeMessages);
           }
-        },
-
-        /**
-         * hideLoading - Remove the loading spinner set by the showLoading
-         */
-        hideLoading: function(){
-          try {
-            this.$el.find('.ui.dropdown').removeClass("loading");
-          } catch (e) {
-            console.log("Failed to remove a loading state in a Searchable Select View, error message: " + e);
+        } catch (e) {
+          console.log(
+            "Failed to show an error state in a Searchable Select View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * removeMessages - Remove all messages and classes set by the
+       * showMessage function.
+       */
+      removeMessages: function () {
+        try {
+          if (!this.$selectUI) {
+            console.warn(
+              "A select UI element wasn't found, can't remove error.",
+            );
+            return;
           }
-        },
 
-      });
-  });
+          this.$selectUI.removeClass("error warning");
+          if (this.message) {
+            this.message.remove();
+          }
+        } catch (e) {
+          console.log(
+            "Failed to hide an error state in a Searchable Select View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * showLoading - Indicate that dropdown options are loading by showing
+       * a spinner in the select interface
+       */
+      showLoading: function () {
+        try {
+          this.$el.find(".ui.dropdown").addClass("loading");
+        } catch (e) {
+          console.log(
+            "Failed to show a loading state in a Searchable Select View, error message: " +
+              e,
+          );
+        }
+      },
+
+      /**
+       * hideLoading - Remove the loading spinner set by the showLoading
+       */
+      hideLoading: function () {
+        try {
+          this.$el.find(".ui.dropdown").removeClass("loading");
+        } catch (e) {
+          console.log(
+            "Failed to remove a loading state in a Searchable Select View, error message: " +
+              e,
+          );
+        }
+      },
+    },
+  );
+});
 
diff --git a/docs/docs/src_js_views_search_CatalogSearchView.js.html b/docs/docs/src_js_views_search_CatalogSearchView.js.html index 7e4fc129d..a07513fef 100644 --- a/docs/docs/src_js_views_search_CatalogSearchView.js.html +++ b/docs/docs/src_js_views_search_CatalogSearchView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/search/CatalogSearchView.js

-
/*global define */
-define([
+            
define([
   "jquery",
   "backbone",
   "views/search/SearchResultsView",
@@ -66,7 +65,7 @@ 

Source: src/js/views/search/CatalogSearchView.js

SorterView, Template, MapSearchFiltersConnector, - CatalogSearchViewCSS + CatalogSearchViewCSS, ) { "use strict"; @@ -409,7 +408,7 @@

Source: src/js/views/search/CatalogSearchView.js

} } catch (e) { console.error( - "Error setting the search mode, defaulting to list:" + e + "Error setting the search mode, defaulting to list:" + e, ); this.mapVisible = false; } @@ -432,7 +431,7 @@

Source: src/js/views/search/CatalogSearchView.js

"change:firstInteraction", function () { this.toggleMapFilter(true); - } + }, ); } }, @@ -459,11 +458,11 @@

Source: src/js/views/search/CatalogSearchView.js

this.$el.html( this.template({ mapFilterOn: this.limitSearchToMapArea === true, - }) + }), ); } catch (e) { console.log( - "There was an error setting up the CatalogSearchView:" + e + "There was an error setting up the CatalogSearchView:" + e, ); this.renderError(); } @@ -490,7 +489,7 @@

Source: src/js/views/search/CatalogSearchView.js

this.listenTo( this.model.get("searchResults"), "reset", - this.renderTitle + this.renderTitle, ); // Render Pager @@ -503,7 +502,7 @@

Source: src/js/views/search/CatalogSearchView.js

this.renderMap(); } catch (e) { console.log( - "There was an error rendering the CatalogSearchView:" + e + "There was an error rendering the CatalogSearchView:" + e, ); this.renderError(); } @@ -565,7 +564,7 @@

Source: src/js/views/search/CatalogSearchView.js

this.searchResultsView.render(); } catch (e) { console.log( - "There was an error rendering the SearchResultsView:" + e + "There was an error rendering the SearchResultsView:" + e, ); } }, @@ -669,7 +668,7 @@

Source: src/js/views/search/CatalogSearchView.js

let title = this.titleTemplate( searchResults.getStart() + 1, searchResults.getEnd() + 1, - searchResults.getNumFound() + searchResults.getNumFound(), ); titleEl.insertAdjacentHTML("beforeend", title); @@ -860,10 +859,10 @@

Source: src/js/views/search/CatalogSearchView.js

updateToggleFiltersLabel: function () { try { const toggleFiltersLabel = this.el.querySelector( - this.toggleFiltersLabel + this.toggleFiltersLabel, ); const toggleFiltersButton = this.el.querySelector( - this.toggleFiltersButton + this.toggleFiltersButton, ); if (this.filtersVisible) { if (toggleFiltersLabel) { @@ -944,7 +943,7 @@

Source: src/js/views/search/CatalogSearchView.js

console.error("Couldn't close search view. ", e); } }, - } + }, ); });
diff --git a/docs/docs/src_js_views_search_SearchResultView.js.html b/docs/docs/src_js_views_search_SearchResultView.js.html index bd53d00bb..2b1371e8a 100644 --- a/docs/docs/src_js_views_search_SearchResultView.js.html +++ b/docs/docs/src_js_views_search_SearchResultView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/search/SearchResultView.js

-
/*global define */
-define([
+            
define([
   "jquery",
   "underscore",
   "backbone",
@@ -93,7 +92,7 @@ 

Source: src/js/views/search/SearchResultView.js

metricStatTemplate: _.template( `<span class='catalog badge'> <i class='catalog-metric-icon <%=metricIcon%>'></i> <%=metricValue%> - </span>` + </span>`, ), /** @@ -179,7 +178,7 @@

Source: src/js/views/search/SearchResultView.js

json.sem_annotation.length > 0; json.showAnnotationIndicator = MetacatUI.appModel.get( - "showAnnotationIndicator" + "showAnnotationIndicator", ); // Find the member node object @@ -198,7 +197,7 @@

Source: src/js/views/search/SearchResultView.js

json.documents, this.model.get("id"), this.model.get("seriesId"), - this.model.get("resourceMap") + this.model.get("resourceMap"), ); json.numDataFiles = dataFileIDs.length; json.dataFilesMessage = @@ -279,7 +278,7 @@

Source: src/js/views/search/SearchResultView.js

.substring(0, this.model.get("abstract").indexOf(" ", 250)) + "..."; var content = $(document.createElement("div")).append( - $(document.createElement("p")).text(abridgedAbstract) + $(document.createElement("p")).text(abridgedAbstract), ); this.$(".popover-this.abstract").popover({ @@ -356,10 +355,10 @@

Source: src/js/views/search/SearchResultView.js

this.metricStatTemplate({ metricValue: MetacatUI.appView.numberAbbreviator( citationCount, - 1 + 1, ), metricIcon: "icon-quote-right", - }) + }), ) .tooltip({ placement: "top", @@ -374,10 +373,10 @@

Source: src/js/views/search/SearchResultView.js

this.metricStatTemplate({ metricValue: MetacatUI.appView.numberAbbreviator( downloadCount, - 1 + 1, ), metricIcon: "icon-cloud-download", - }) + }), ) .tooltip({ placement: "top", @@ -392,7 +391,7 @@

Source: src/js/views/search/SearchResultView.js

this.metricStatTemplate({ metricValue: MetacatUI.appView.numberAbbreviator(viewCount, 1), metricIcon: "icon-eye-open", - }) + }), ) .tooltip({ placement: "top", @@ -434,7 +433,7 @@

Source: src/js/views/search/SearchResultView.js

* @param {Event} e - The mouseover or mouseout event */ toggleShowOnMap: function (e) { - this.model.set("showOnMap", e.type === "mouseover") + this.model.set("showOnMap", e.type === "mouseover"); }, /** @@ -487,7 +486,7 @@

Source: src/js/views/search/SearchResultView.js

if (pkgFileName.lastIndexOf(".") > 0) pkgFileName = pkgFileName.substring( 0, - pkgFileName.lastIndexOf(".") + pkgFileName.lastIndexOf("."), ); var packageModel = new Package({ @@ -601,7 +600,7 @@

Source: src/js/views/search/SearchResultView.js

onClose: function () { this.clear(); }, - } + }, ); });
diff --git a/docs/docs/src_js_views_search_SearchResultsPagerView.js.html b/docs/docs/src_js_views_search_SearchResultsPagerView.js.html index 44199a578..1b33c2e73 100644 --- a/docs/docs/src_js_views_search_SearchResultsPagerView.js.html +++ b/docs/docs/src_js_views_search_SearchResultsPagerView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/search/SearchResultsPagerView.js

-
/*global define */
-define(["backbone"], function (Backbone) {
+            
define(["backbone"], function (Backbone) {
   "use strict";
 
   // Classes are from Bootstrap
@@ -101,9 +100,8 @@ 

Source: src/js/views/search/SearchResultsPagerView.js

Source: src/js/views/search/SearchResultsPagerView.js

Source: src/js/views/search/SearchResultsPagerView.js

Source: src/js/views/search/SearchResultsPagerView.js

0) { container.insertAdjacentHTML( "afterbegin", - this.linkTemplate({ page: currentPage - 1, pageDisplay: "<" }) + this.linkTemplate({ page: currentPage - 1, pageDisplay: "<" }), ); container.insertAdjacentHTML( "beforeend", - this.linkTemplate({ page: 0, pageDisplay: 1 }) + this.linkTemplate({ page: 0, pageDisplay: 1 }), ); //If there are pages between the first page and the current-2, then @@ -234,7 +231,7 @@

Source: src/js/views/search/SearchResultsPagerView.js

Source: src/js/views/search/SearchResultsPagerView.js

Source: src/js/views/search/SearchResultsPagerView.js

" }) + this.linkTemplate({ page: currentPage + 1, pageDisplay: ">" }), ); } } catch (e) { @@ -312,7 +309,6 @@

Source: src/js/views/search/SearchResultsPagerView.js

= 0) { this.goToPage(page); } - }, /** @@ -358,7 +354,7 @@

Source: src/js/views/search/SearchResultsPagerView.js

diff --git a/docs/docs/src_js_views_search_SearchResultsView.js.html b/docs/docs/src_js_views_search_SearchResultsView.js.html index cdffc73c7..b9ea4fa47 100644 --- a/docs/docs/src_js_views_search_SearchResultsView.js.html +++ b/docs/docs/src_js_views_search_SearchResultsView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/search/SearchResultsView.js

-
/*global define */
-define([
+            
define([
   "backbone",
   "collections/SolrResults",
   "views/search/SearchResultView",
@@ -314,7 +313,7 @@ 

Source: src/js/views/search/SearchResultsView.js

i++; } }, - } + }, ); });
diff --git a/docs/docs/src_js_views_search_SorterView.js.html b/docs/docs/src_js_views_search_SorterView.js.html index d8d4cd6f8..013d2d037 100644 --- a/docs/docs/src_js_views_search_SorterView.js.html +++ b/docs/docs/src_js_views_search_SorterView.js.html @@ -44,8 +44,7 @@

Source: src/js/views/search/SorterView.js

-
/*global define */
-define(["backbone"], function (Backbone) {
+            
define(["backbone"], function (Backbone) {
   "use strict";
   /**
    * @class SorterView
@@ -119,7 +118,7 @@ 

Source: src/js/views/search/SorterView.js

for (let opt of this.sortOptions) { select.insertAdjacentHTML( "beforeend", - `<option value="${opt.value}">${opt.label}</option>` + `<option value="${opt.value}">${opt.label}</option>`, ); } @@ -167,7 +166,7 @@

Source: src/js/views/search/SorterView.js

show: function () { this.el.style.visibility = "visible"; }, - } + }, ); });
diff --git a/docs/docs/src_loader.js.html b/docs/docs/src_loader.js.html index 41d7cf412..f6479980d 100644 --- a/docs/docs/src_loader.js.html +++ b/docs/docs/src_loader.js.html @@ -51,18 +51,18 @@

Source: src/loader.js

**/ /** -* @namespace MetacatUI -* @description The global object that contains all of the MetacatUI top-level classes, variables, and functions. -* @type {object} -*/ + * @namespace MetacatUI + * @description The global object that contains all of the MetacatUI top-level classes, variables, and functions. + * @type {object} + */ var MetacatUI = MetacatUI || {}; /** -* This function gets configuration settings from the {@see AppConfig}, such as `root`, -* `theme`, etc. and loads the theme configuration file. When the theme configuration file is -* loaded, the rest of the app is initialized, in {@see MetacatUI.initApp} -*/ -MetacatUI.loadTheme = function() { + * This function gets configuration settings from the {@see AppConfig}, such as `root`, + * `theme`, etc. and loads the theme configuration file. When the theme configuration file is + * loaded, the rest of the app is initialized, in {@see MetacatUI.initApp} + */ +MetacatUI.loadTheme = function () { //---- Get the MetacatUI root ----/ // Find out of MetacatUI is deployed in a sub-directory off the top level of // the domain. This value is used throughout the app to determin the location @@ -70,14 +70,17 @@

Source: src/loader.js

// should also set a FallbackResource directive accordingly in order to support // users entering MetacatUI from URLs other than the root /** - * The root path of this MetacatUI deployment. This should point to the `src` directory - * that was deployed, which contains the `index.html` file for MetacatUI. This root path - * is used throughout the app to construct URLs to pages, images, etc. - * @type {string} - * @default "/metacatui" - * @readonly - */ - MetacatUI.root = (typeof MetacatUI.AppConfig.root == "string")? MetacatUI.AppConfig.root : "/metacatui"; + * The root path of this MetacatUI deployment. This should point to the `src` directory + * that was deployed, which contains the `index.html` file for MetacatUI. This root path + * is used throughout the app to construct URLs to pages, images, etc. + * @type {string} + * @default "/metacatui" + * @readonly + */ + MetacatUI.root = + typeof MetacatUI.AppConfig.root == "string" + ? MetacatUI.AppConfig.root + : "/metacatui"; // Remove trailing slash if one is present MetacatUI.root = MetacatUI.root.replace(/\/$/, ""); @@ -86,53 +89,68 @@

Source: src/loader.js

var loaderEl = document.getElementById("loader"); /** - * @name MetacatUI.theme - * @type {string} - * @default "default" - * @readonly - * @description The theme name for this MetacatUI deployment. This is defined in the {@link AppConfig#theme}. - * If no theme is defined, the default theme is used. - */ + * @name MetacatUI.theme + * @type {string} + * @default "default" + * @readonly + * @description The theme name for this MetacatUI deployment. This is defined in the {@link AppConfig#theme}. + * If no theme is defined, the default theme is used. + */ //---- Get the theme name ----/ //Get the the name from the AppConfig file (the recommended way as of MetacatUI 2.12.0) - if(typeof MetacatUI.AppConfig.theme == "string" && MetacatUI.AppConfig.theme.length > 0){ + if ( + typeof MetacatUI.AppConfig.theme == "string" && + MetacatUI.AppConfig.theme.length > 0 + ) { MetacatUI.theme = MetacatUI.AppConfig.theme; } //Get the the name from the index.html file (old way - will be deprecated in the future!) - else if( loaderEl && typeof loaderEl.getAttribute("data-theme") == "string" ){ + else if (loaderEl && typeof loaderEl.getAttribute("data-theme") == "string") { MetacatUI.theme = loaderEl.getAttribute("data-theme"); } //Default to the "default" theme if one isn't specified - else{ + else { MetacatUI.theme = "default"; } //---Get the metacat context --- // Use the metacat context from the index.html file if it is NOT in the AppConfig. (old way - will be deprecated in the future!) // As of MetacatUI 2.12.0, it is recommended to put the metacatContext in the AppConfig file. - if( loaderEl && typeof loaderEl.getAttribute("data-metacat-context") == "string" && - typeof MetacatUI.AppConfig.metacatContext == "undefined" ){ - MetacatUI.AppConfig.metacatContext = loaderEl.getAttribute("data-metacat-context"); + if ( + loaderEl && + typeof loaderEl.getAttribute("data-metacat-context") == "string" && + typeof MetacatUI.AppConfig.metacatContext == "undefined" + ) { + MetacatUI.AppConfig.metacatContext = loaderEl.getAttribute( + "data-metacat-context", + ); } //Add a leading forward slash to the context - if( MetacatUI.AppConfig.metacatContext && MetacatUI.AppConfig.metacatContext.charAt(0) !== "/" ){ - MetacatUI.AppConfig.metacatContext = "/" + MetacatUI.AppConfig.metacatContext; + if ( + MetacatUI.AppConfig.metacatContext && + MetacatUI.AppConfig.metacatContext.charAt(0) !== "/" + ) { + MetacatUI.AppConfig.metacatContext = + "/" + MetacatUI.AppConfig.metacatContext; } /** - * @name MetacatUI.mapKey - * @type {string} - * @readonly - * @see {AppConfig#mapKey} - * @description The Google Maps API key for this MetacatUI deployment. This should be set in the - * {@see AppConfig} object. - */ + * @name MetacatUI.mapKey + * @type {string} + * @readonly + * @see {AppConfig#mapKey} + * @description The Google Maps API key for this MetacatUI deployment. This should be set in the + * {@see AppConfig} object. + */ //---Get the Google Maps API Key--- //The recommended way to set the Google Maps API Key is in the AppConfig file, as of MetacatUI 2.12.0 - MetacatUI.mapKey = loaderEl? loaderEl.getAttribute("data-map-key") : null; - if( typeof MetacatUI.mapKey !== "string" || typeof MetacatUI.AppConfig.mapKey == "string" ){ + MetacatUI.mapKey = loaderEl ? loaderEl.getAttribute("data-map-key") : null; + if ( + typeof MetacatUI.mapKey !== "string" || + typeof MetacatUI.AppConfig.mapKey == "string" + ) { MetacatUI.mapKey = MetacatUI.AppConfig.mapKey; - if( (MetacatUI.mapKey == "YOUR-GOOGLE-MAPS-API-KEY") || (!MetacatUI.mapKey) ){ + if (MetacatUI.mapKey == "YOUR-GOOGLE-MAPS-API-KEY" || !MetacatUI.mapKey) { MetacatUI.mapKey = null; } } @@ -140,32 +158,42 @@

Source: src/loader.js

//---- Load the theme config file ---- var script = document.createElement("script"); script.setAttribute("type", "text/javascript"); - script.setAttribute("src", MetacatUI.root + "/js/themes/" + MetacatUI.theme + "/config.js?v=" + MetacatUI.metacatUIVersion); + script.setAttribute( + "src", + MetacatUI.root + + "/js/themes/" + + MetacatUI.theme + + "/config.js?v=" + + MetacatUI.metacatUIVersion, + ); document.getElementsByTagName("body")[0].appendChild(script); //When the theme config file is loaded, intialize the application - script.onload = function(){ + script.onload = function () { //If this theme has a custom function to start the app, then use it - if(typeof MetacatUI.customInitApp == "function") { - MetacatUI.customInitApp(); - } + if (typeof MetacatUI.customInitApp == "function") { + MetacatUI.customInitApp(); + } //Start the app else MetacatUI.initApp(); - } -} + }; +}; /** -* Loads the RequireJS library and the `app.js` file, which contains all of the RequireJS -* configurations. The appjs is where the bulk of the application initialization happens -* (for example, creating top-level models and views, initializing the application router, -* and rendering the top-level {@see AppView}). -*/ + * Loads the RequireJS library and the `app.js` file, which contains all of the RequireJS + * configurations. The appjs is where the bulk of the application initialization happens + * (for example, creating top-level models and views, initializing the application router, + * and rendering the top-level {@see AppView}). + */ MetacatUI.initApp = function () { - var script = document.createElement("script"); - script.setAttribute("data-main", MetacatUI.root + "/js/app.js?v=" + MetacatUI.metacatUIVersion); - script.src = MetacatUI.root + "/components/require.js"; - document.getElementsByTagName("body")[0].appendChild(script); -} + var script = document.createElement("script"); + script.setAttribute( + "data-main", + MetacatUI.root + "/js/app.js?v=" + MetacatUI.metacatUIVersion, + ); + script.src = MetacatUI.root + "/components/require.js"; + document.getElementsByTagName("body")[0].appendChild(script); +}; /** * @namespace AppConfig diff --git a/docs/docs/styles/jsdoc-default.css b/docs/docs/styles/jsdoc-default.css index 940877795..65e0906c2 100644 --- a/docs/docs/styles/jsdoc-default.css +++ b/docs/docs/styles/jsdoc-default.css @@ -1,281 +1,302 @@ -html -{ - overflow: auto; - background-color: #fff; - font-size: 14px; +html { + overflow: auto; + background-color: #fff; + font-size: 14px; } -body -{ - font-family: 'Open Sans', sans-serif; - line-height: 1.5; - color: #4d4e53; - background-color: white; +body { + font-family: "Open Sans", sans-serif; + line-height: 1.5; + color: #4d4e53; + background-color: white; } -a, a:visited, a:active { - color: #0095dd; - text-decoration: none; +a, +a:visited, +a:active { + color: #0095dd; + text-decoration: none; } a:hover { - text-decoration: underline; + text-decoration: underline; } -header -{ - display: block; - padding: 0px 4px; +header { + display: block; + padding: 0px 4px; } -tt, code, kbd, samp { - font-family: Consolas, Monaco, 'Andale Mono', monospace; +tt, +code, +kbd, +samp { + font-family: Consolas, Monaco, "Andale Mono", monospace; } .class-description { - font-size: 130%; - line-height: 140%; - margin-bottom: 1em; - margin-top: 1em; + font-size: 130%; + line-height: 140%; + margin-bottom: 1em; + margin-top: 1em; } .class-description:empty { - margin: 0; + margin: 0; } #main { - float: left; - width: 70%; + float: left; + width: 70%; } article dl { - margin-bottom: 40px; + margin-bottom: 40px; } article img { max-width: 100%; } -section -{ - display: block; - background-color: #fff; - padding: 12px 24px; - border-bottom: 1px solid #ccc; - margin-right: 30px; +section { + display: block; + background-color: #fff; + padding: 12px 24px; + border-bottom: 1px solid #ccc; + margin-right: 30px; } .variation { - display: none; + display: none; } .signature-attributes { - font-size: 60%; - color: #aaa; - font-style: italic; - font-weight: lighter; + font-size: 60%; + color: #aaa; + font-style: italic; + font-weight: lighter; } -nav -{ - display: block; - float: right; - margin-top: 28px; - width: 30%; - box-sizing: border-box; - border-left: 1px solid #ccc; - padding-left: 16px; +nav { + display: block; + float: right; + margin-top: 28px; + width: 30%; + box-sizing: border-box; + border-left: 1px solid #ccc; + padding-left: 16px; } nav ul { - font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; - font-size: 100%; - line-height: 17px; - padding: 0; - margin: 0; - list-style-type: none; + font-family: "Lucida Grande", "Lucida Sans Unicode", arial, sans-serif; + font-size: 100%; + line-height: 17px; + padding: 0; + margin: 0; + list-style-type: none; } -nav ul a, nav ul a:visited, nav ul a:active { - font-family: Consolas, Monaco, 'Andale Mono', monospace; - line-height: 18px; - color: #4D4E53; +nav ul a, +nav ul a:visited, +nav ul a:active { + font-family: Consolas, Monaco, "Andale Mono", monospace; + line-height: 18px; + color: #4d4e53; } nav h3 { - margin-top: 12px; + margin-top: 12px; } nav li { - margin-top: 6px; + margin-top: 6px; } footer { - display: block; - padding: 6px; - margin-top: 12px; - font-style: italic; - font-size: 90%; + display: block; + padding: 6px; + margin-top: 12px; + font-style: italic; + font-size: 90%; } -h1, h2, h3, h4 { - font-weight: 200; - margin: 0; +h1, +h2, +h3, +h4 { + font-weight: 200; + margin: 0; } -h1 -{ - font-family: 'Open Sans Light', sans-serif; - font-size: 48px; - letter-spacing: -2px; - margin: 12px 24px 20px; +h1 { + font-family: "Open Sans Light", sans-serif; + font-size: 48px; + letter-spacing: -2px; + margin: 12px 24px 20px; } -h2, h3.subsection-title -{ - font-size: 30px; - font-weight: 700; - letter-spacing: -1px; - margin-bottom: 12px; +h2, +h3.subsection-title { + font-size: 30px; + font-weight: 700; + letter-spacing: -1px; + margin-bottom: 12px; } -h3 -{ - font-size: 24px; - letter-spacing: -0.5px; - margin-bottom: 12px; +h3 { + font-size: 24px; + letter-spacing: -0.5px; + margin-bottom: 12px; } -h4 -{ - font-size: 18px; - letter-spacing: -0.33px; - margin-bottom: 12px; - color: #4d4e53; +h4 { + font-size: 18px; + letter-spacing: -0.33px; + margin-bottom: 12px; + color: #4d4e53; } -h5, .container-overview .subsection-title -{ - font-size: 120%; - font-weight: bold; - letter-spacing: -0.01em; - margin: 8px 0 3px 0; +h5, +.container-overview .subsection-title { + font-size: 120%; + font-weight: bold; + letter-spacing: -0.01em; + margin: 8px 0 3px 0; } -h6 -{ - font-size: 100%; - letter-spacing: -0.01em; - margin: 6px 0 3px 0; - font-style: italic; +h6 { + font-size: 100%; + letter-spacing: -0.01em; + margin: 6px 0 3px 0; + font-style: italic; } -table -{ - border-spacing: 0; - border: 0; - border-collapse: collapse; +table { + border-spacing: 0; + border: 0; + border-collapse: collapse; } -td, th -{ - border: 1px solid #ddd; - margin: 0px; - text-align: left; - vertical-align: top; - padding: 4px 6px; - display: table-cell; +td, +th { + border: 1px solid #ddd; + margin: 0px; + text-align: left; + vertical-align: top; + padding: 4px 6px; + display: table-cell; } -thead tr -{ - background-color: #ddd; - font-weight: bold; +thead tr { + background-color: #ddd; + font-weight: bold; } -th { border-right: 1px solid #aaa; } -tr > th:last-child { border-right: 1px solid #ddd; } +th { + border-right: 1px solid #aaa; +} +tr > th:last-child { + border-right: 1px solid #ddd; +} -.ancestors, .attribs { color: #999; } -.ancestors a, .attribs a -{ - color: #999 !important; - text-decoration: none; +.ancestors, +.attribs { + color: #999; +} +.ancestors a, +.attribs a { + color: #999 !important; + text-decoration: none; } -.clear -{ - clear: both; +.clear { + clear: both; } -.important -{ - font-weight: bold; - color: #950B02; +.important { + font-weight: bold; + color: #950b02; } .yes-def { - text-indent: -1000px; + text-indent: -1000px; } .type-signature { - color: #aaa; + color: #aaa; } -.name, .signature { - font-family: Consolas, Monaco, 'Andale Mono', monospace; +.name, +.signature { + font-family: Consolas, Monaco, "Andale Mono", monospace; } -.details { margin-top: 14px; border-left: 2px solid #DDD; } -.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } -.details dd { margin-left: 70px; } -.details ul { margin: 0; } -.details ul { list-style-type: none; } -.details li { margin-left: 30px; padding-top: 6px; } -.details pre.prettyprint { margin: 0 } -.details .object-value { padding-top: 0; } +.details { + margin-top: 14px; + border-left: 2px solid #ddd; +} +.details dt { + width: 120px; + float: left; + padding-left: 10px; + padding-top: 6px; +} +.details dd { + margin-left: 70px; +} +.details ul { + margin: 0; +} +.details ul { + list-style-type: none; +} +.details li { + margin-left: 30px; + padding-top: 6px; +} +.details pre.prettyprint { + margin: 0; +} +.details .object-value { + padding-top: 0; +} .description { - margin-bottom: 1em; - margin-top: 1em; + margin-bottom: 1em; + margin-top: 1em; } -.code-caption -{ - font-style: italic; - font-size: 107%; - margin: 0; +.code-caption { + font-style: italic; + font-size: 107%; + margin: 0; } -.source -{ - border: 1px solid #ddd; - width: 80%; - overflow: auto; +.source { + border: 1px solid #ddd; + width: 80%; + overflow: auto; } .prettyprint.source { - width: inherit; + width: inherit; } -.source code -{ - font-size: 100%; - line-height: 18px; - display: block; - padding: 4px 12px; - margin: 0; - background-color: #fff; - color: #4D4E53; +.source code { + font-size: 100%; + line-height: 18px; + display: block; + padding: 4px 12px; + margin: 0; + background-color: #fff; + color: #4d4e53; } -.prettyprint code span.line -{ +.prettyprint code span.line { display: inline-block; } -.prettyprint.linenums -{ +.prettyprint.linenums { padding-left: 70px; -webkit-user-select: none; -moz-user-select: none; @@ -283,50 +304,46 @@ tr > th:last-child { border-right: 1px solid #ddd; } user-select: none; } -.prettyprint.linenums ol -{ +.prettyprint.linenums ol { padding-left: 0; } -.prettyprint.linenums li -{ +.prettyprint.linenums li { border-left: 3px #ddd solid; } .prettyprint.linenums li.selected, -.prettyprint.linenums li.selected * -{ +.prettyprint.linenums li.selected * { background-color: lightyellow; } -.prettyprint.linenums li * -{ +.prettyprint.linenums li * { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; } -.params .name, .props .name, .name code { - color: #4D4E53; - font-family: Consolas, Monaco, 'Andale Mono', monospace; - font-size: 100%; +.params .name, +.props .name, +.name code { + color: #4d4e53; + font-family: Consolas, Monaco, "Andale Mono", monospace; + font-size: 100%; } .params td.description > p:first-child, -.props td.description > p:first-child -{ - margin-top: 0; - padding-top: 0; +.props td.description > p:first-child { + margin-top: 0; + padding-top: 0; } .params td.description > p:last-child, -.props td.description > p:last-child -{ - margin-bottom: 0; - padding-bottom: 0; +.props td.description > p:last-child { + margin-bottom: 0; + padding-bottom: 0; } .disabled { - color: #454545; + color: #454545; } diff --git a/docs/docs/styles/style.css b/docs/docs/styles/style.css index 2a11a9cd8..377691735 100644 --- a/docs/docs/styles/style.css +++ b/docs/docs/styles/style.css @@ -1,11 +1,11 @@ -:root{ - --deprecated: #D62828; - --neutral: #EEEAD2; +:root { + --deprecated: #d62828; + --neutral: #eeead2; --neutral-dark: #003049; --info-light: #e4edd9; --info-dark: #684500; - --primary-brand: #FCBF49; - --primary-brand-on-dark: #FCBF49; + --primary-brand: #fcbf49; + --primary-brand-on-dark: #fcbf49; --primary-brand-dark: #684500; --secondary-brand: #2a9989; } @@ -14,21 +14,21 @@ Color pallete: https://coolors.co/003049-2a9989-fcbf49-eae2b7-d62828 */ -body{ +body { margin: 0px; font-family: "Open Sans", Helvetica, Arial, sans-serif; display: grid; grid-template-columns: 20% 80%; - background-color: #FFF; + background-color: #fff; color: var(--neutral-dark); } -#main{ +#main { float: none; width: auto; height: 100vh; overflow-y: auto; } -#nav{ +#nav { float: none; width: auto; height: 100vh; @@ -38,55 +38,63 @@ body{ padding-top: 20px; border: 0px; } -nav h2{ +nav h2 { font-size: 1em; } nav h3 { - color: var(--neutral); - font-size: 1.5em; - margin-top: 20px; - margin-bottom: 0px; - font-weight: bold; - letter-spacing: 0; -} -nav ul a, nav ul a:visited, nav ul a:active, nav h3, nav a{ + color: var(--neutral); + font-size: 1.5em; + margin-top: 20px; + margin-bottom: 0px; + font-weight: bold; + letter-spacing: 0; +} +nav ul a, +nav ul a:visited, +nav ul a:active, +nav h3, +nav a { font-family: inherit; color: var(--neutral); - font-size: .95em; + font-size: 0.95em; } .category-heading ~ li:not(.category-heading) { - margin-left: 10px; + margin-left: 10px; } .category-heading { color: var(--neutral); margin-top: 10px; font-weight: bold; - font-size: .95em; + font-size: 0.95em; } -.category-heading[data-category='Deprecated']{ +.category-heading[data-category="Deprecated"] { color: var(--deprecated); font-style: italic; } -.important{ +.important { color: var(--deprecated); } -h1, h2, h3, h4{ +h1, +h2, +h3, +h4 { color: var(--neutral-dark); } -h1{ +h1 { font-family: inherit; font-size: 2em; letter-spacing: normal; - border-bottom: 1px solid #DDD; + border-bottom: 1px solid #ddd; padding-bottom: 20px; } -h2, h3.subsection-title{ +h2, +h3.subsection-title { font-weight: normal; } -header{ +header { padding: 0px; } -.class-description{ +.class-description { font-size: 1em; line-height: inherit; padding: 20px; @@ -95,71 +103,71 @@ header{ color: var(--primary-brand-dark); } .description + h5 { - font-weight: bold; - font-size: 1em; - display: inline; - margin-right: 20px; + font-weight: bold; + font-size: 1em; + display: inline; + margin-right: 20px; } -.description + h5 + ul{ +.description + h5 + ul { list-style: none; - display: inline; - padding: 0px; + display: inline; + padding: 0px; } -.description + h5 + ul li{ +.description + h5 + ul li { list-style: none; display: inline; } -.name{ +.name { padding: 10px 20px; -background-color: var(--neutral-dark); -border-bottom: 1px solid var(--neutral); + background-color: var(--neutral-dark); + border-bottom: 1px solid var(--neutral); } h4.name { - color: var(--neutral); + color: var(--neutral); } -.name .type-signature a{ +.name .type-signature a { color: var(--primary-brand-on-dark); } -pre{ +pre { background-color: #eee; padding: 20px; border-radius: 4px; border: 1px solid #ddd; } -table{ +table { width: 100%; } -thead th{ +thead th { color: #fff; background-color: #272727; border-color: #000000; vertical-align: bottom; border-bottom: 2px solid #dee2e6; - padding: .75rem; + padding: 0.75rem; } .props thead th, .params thead th { - background-color: var(--neutral); - color: var(--neutral-dark); - border: 0px; - border-image-width: 0px; - border-bottom: 1px solid var(--neutral); -} -table td{ - padding: .75rem; + background-color: var(--neutral); + color: var(--neutral-dark); + border: 0px; + border-image-width: 0px; + border-bottom: 1px solid var(--neutral); +} +table td { + padding: 0.75rem; vertical-align: top; } -td.name{ +td.name { background-color: var(--neutral); } -.class-screenshot{ +.class-screenshot { margin-top: 20px; margin-bottom: 20px; } -.class-screenshot h3{ +.class-screenshot h3 { text-align: center; } -.class-screenshot img{ +.class-screenshot img { margin: auto; display: flex; max-width: 600px; @@ -167,10 +175,10 @@ td.name{ border-radius: 4px; padding: 10px; } -section{ +section { background-color: transparent; } -.link-icon svg{ +.link-icon svg { height: 1em; vertical-align: middle; } diff --git a/docs/index.md b/docs/index.md index 8c6b9f618..271db3108 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ MetacatUI is an open source, community project. We [welcome contributions](https Cite this software as: -> Matthew B. Jones, Chris Jones, Lauren Walker, Robyn Thiessen-Bock, Ben Leinfelder, Peter Slaughter, Bryce Mecum, Rushiraj Nenuji, Hesham Elbashandy, Val Hendrix, Ian Nesbitt, Yvonne Shi, Ian Guerin, Doug Hungarter. 2024. MetacatUI: A client-side web interface for DataONE data repositories (version 2.29.0). Arctic Data Center. [doi:10.18739/A2KD1QN1N](https://doi.org/10.18739/A2KD1QN1N) +> Matthew B. Jones, Chris Jones, Lauren Walker, Robyn Thiessen-Bock, Ben Leinfelder, Peter Slaughter, Bryce Mecum, Rushiraj Nenuji, Hesham Elbashandy, Val Hendrix, Ian Nesbitt, Yvonne Shi, Ian Guerin, Doug Hungarter. 2024. MetacatUI: A client-side web interface for DataONE data repositories (version 2.29.1). Arctic Data Center. [doi:10.18739/A2KD1QN1N](https://doi.org/10.18739/A2KD1QN1N) ## Related Projects diff --git a/package-lock.json b/package-lock.json index 53b2e6c25..a17d609ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "metacatui", - "version": "2.29.0", + "version": "2.29.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "metacatui", - "version": "2.29.0", + "version": "2.29.1", "license": "Apache-2.0", "dependencies": { "@actions/core": "^1.9.1", diff --git a/package.json b/package.json index bca9dbd0f..e8d74d187 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metacatui", - "version": "2.29.0", + "version": "2.29.1", "description": "MetacatUI: A client-side web interface for DataONE data repositories", "main": "server.js", "dependencies": { diff --git a/src/index.html b/src/index.html index de00a2f74..d3170845e 100644 --- a/src/index.html +++ b/src/index.html @@ -44,7 +44,7 @@ //Create the MetacatUI object var MetacatUI = {}; - MetacatUI.metacatUIVersion = "2.29.0"; + MetacatUI.metacatUIVersion = "2.29.1"; //Catch errors when the config or loader file fails to load. // These are mainly helpful for developers/operators installing MetacatUI