{"id":154,"date":"2025-04-11T16:58:44","date_gmt":"2025-04-11T14:58:44","guid":{"rendered":"https:\/\/highered-change.org\/?page_id=154"},"modified":"2026-06-19T03:48:17","modified_gmt":"2026-06-19T01:48:17","slug":"inventory-he-centers","status":"publish","type":"page","link":"https:\/\/highered-change.org\/index.php\/inventory-he-centers\/","title":{"rendered":"Inventory HE research centers"},"content":{"rendered":"\n<!--\n  ============================================================================\n  GOHEC \u2014 Inventory of HE Research Centers (single embed block, NO iframes)\n  ============================================================================\n\n  WHAT THIS IS\n  This is the HTML\/CSS\/JS that powers the \"Inventory HE research centers\" page:\n  https:\/\/highered-change.org\/index.php\/inventory-he-centers\/\n\n  It renders, on the SAME page (no iframes):\n    1. An interactive Leaflet world map with one marker per research center.\n    2. A searchable\/sortable DataTables table of the same centers.\n\n  DATA SOURCES (produced by the \"HE Maps data sync\" WordPress plugin):\n    - JSON for the map  : \/wp-content\/uploads\/json_and_csv\/he_research_centers.json\n    - CSV  for the table: \/wp-content\/uploads\/json_and_csv\/he_research_centers.csv\n  Those files are refreshed from Google Sheets via Tools \u2192 HE maps data in wp-admin.\n\n  HOW TO USE\n  Paste this whole block into a WordPress \"Custom HTML\" block on the page.\n  External libraries (jQuery, Leaflet, DataTables, PapaParse) are loaded from CDNs\n  and should appear only ONCE per page.\n\n  NOTE ON IDS\n  The table\/map element ids use the \"he-centers-*\" prefix on BOTH the research\n  centers page and the academic programs page so the shared CSS\/JS stays identical.\n  Each page still loads its own JSON\/CSV (see JSON_URL \/ CSV_URL below).\n-->\n\n<style>\n  \/* Base reset: shared font + remove default margins\/padding. *\/\n  * {\n    margin: 0;\n    padding: 0;\n    box-sizing: border-box;\n    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n  }\n\n  \/* Wrapper for all body text\/content of the embed. *\/\n  .itBody {\n    color: #001e2c;\n    line-height: 1.6;\n    box-sizing: border-box;\n    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n  }\n\n  .container {\n    max-width: 1200px;\n    margin: 0 auto;\n    padding: 0 20px;\n  }\n\n  \/* Optional dark page header (kept for reuse; not always rendered). *\/\n  .site-header {\n    background-color: #001e2c;\n    color: white;\n    padding: 30px 0;\n    border-bottom: 4px solid #ec7c34;\n  }\n\n  .site-title {\n    font-size: 2.5rem;\n    text-align: center;\n    margin-bottom: 10px;\n  }\n\n  .site-description {\n    text-align: center;\n    opacity: 0.9;\n    max-width: 800px;\n    margin: 0 auto;\n  }\n\n  \/* Optional tab bar (only used when the tabbed layout is present). *\/\n  .tabs-container {\n    display: flex;\n    justify-content: center;\n    padding-bottom: 20px;\n  }\n\n  .tab-button {\n    padding: 12px 24px;\n    background-color: #4e7bc7;\n    color: white;\n    border: none;\n    margin: 0 10px;\n    cursor: pointer;\n    font-weight: bold;\n    border-radius: 5px;\n    transition: all 0.3s ease;\n  }\n\n  .tab-button.active {\n    background-color: #ec7c34;\n    transform: translateY(-5px);\n  }\n\n  .tab-button:hover {\n    opacity: 0.9;\n  }\n\n  .content-area {\n    padding: 40px 0;\n  }\n\n  \/* Tab panels: hidden unless marked active. *\/\n  .panel-content {\n    display: none;\n    margin: 0px 14px;\n  }\n\n  .panel-content.active {\n    display: block;\n  }\n\n  .section-heading {\n    color: #4e7bc7;\n    margin-bottom: 20px;\n    border-bottom: 2px solid #ec7c34;\n    padding-bottom: 10px;\n    font-size: 2rem;\n  }\n\n  .content-paragraph {\n    margin-bottom: 20px;\n  }\n\n  .content-link {\n    color: #4e7bc7;\n    text-decoration: none;\n  }\n\n  .content-link:hover {\n    text-decoration: underline;\n    color: #ec7c34;\n  }\n\n  \/* White \"cards\" that hold the map and the table. *\/\n  .map-container,\n  .table-container {\n    background-color: white;\n    padding: 20px;\n    border-radius: 8px;\n    box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n    margin-bottom: 30px;\n  }\n\n  \/* The Leaflet map mount point (named *-iframe for legacy reasons; it is a div). *\/\n  .map-iframe {\n    width: 100%;\n    height: 600px;\n    border: 2px solid white;\n    border-radius: 5px;\n  }\n\n  .table-iframe {\n    width: 100%;\n    border: none;\n  }\n\n  \/* \"Add \/ edit a center\" call-to-action box. *\/\n  .cta-section {\n    background-color: #4e7bc7;\n    padding: 30px;\n    border-radius: 8px;\n    margin-top: 40px;\n    color: white;\n    text-align: center;\n  }\n\n  .cta-heading {\n    margin-bottom: 15px;\n    color: white;\n  }\n\n  .cta-button {\n    display: inline-block;\n    background-color: #ec7c34;\n    text-decoration: none;\n    color: white;\n    padding: 12px 24px;\n    border-radius: 5px;\n    font-weight: bold;\n    margin-top: 15px;\n    transition: all 0.3s ease;\n  }\n\n  .cta-button:hover {\n    transform: translateY(-3px);\n    box-shadow: 0 4px 12px rgba(0,0,0,0.2);\n    text-decoration: none;\n    color: white;\n  }\n\n  .text-highlight {\n    color: #ec7c34;\n    font-weight: bold;\n  }\n\n  .itBody p {\n    margin: 10px 0 10px 0;\n  }\n\n  \/* Inline links inside paragraphs (WordPress theme override). *\/\n  p a:where(:not(.wp-element-button)) {\n    color: blue;\n    font-weight: 500;\n  }\n\n  \/* ---- DataTables look & feel ---- *\/\n  table.dataTable thead {\n    background-color: #4193cf;\n    color: white;\n  }\n\n  table.dataTable tbody tr:nth-child(even) {\n    background-color: #f2f2f2;\n  }\n\n  table.dataTable tbody tr:hover {\n    background-color: #ddd;\n  }\n\n  table.dataTable {\n    border-collapse: collapse;\n    width: 100%;\n  }\n\n  table.dataTable th,\n  table.dataTable td {\n    padding: 8px 12px;\n    text-align: left;\n    border: 1px solid #ddd;\n  }\n\n  .dataTables_length,\n  .dataTables_filter {\n    margin-bottom: 15px;\n  }\n\n  \/* Per-column filter inputs live in the SECOND header row. *\/\n  #he-centers-table thead tr:nth-child(2) th input {\n    width: 100%;\n    box-sizing: border-box;\n    padding: 4px;\n    border: 1px solid #ccc;\n    min-width: 140px;\n  }\n\n  \/* Row above the table holding the heading + \"Clear filters\" button. *\/\n  .table-header-row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 12px;\n    margin-bottom: 12px;\n    flex-wrap: wrap;\n  }\n\n  \/* \"Clear filters\" button: hidden until at least one filter is active. *\/\n  .clear-filters-btn {\n    display: none;\n    background-color: #ec7c34;\n    color: white;\n    border: none;\n    border-radius: 5px;\n    padding: 10px 14px;\n    font-weight: 600;\n    cursor: pointer;\n    transition: opacity 0.2s ease, transform 0.2s ease;\n  }\n\n  .clear-filters-btn.is-visible {\n    display: inline-block;\n  }\n\n  .clear-filters-btn:hover {\n    opacity: 0.92;\n    transform: translateY(-1px);\n  }\n\n  \/* ===== Mobile-friendly horizontal scroll wrapper for the table ===== *\/\n  .table-scroll {\n    width: 100%;\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n  }\n\n  \/* Keep the table wide enough to be readable; it scrolls on small screens. *\/\n  #he-centers-table {\n    width: 100% !important;\n    min-width: 900px;\n  }\n\n  \/* Force all table links to render blue regardless of theme styles. *\/\n  #he-centers-table a,\n  #he-centers-table a:visited {\n    color: #1a73e8 !important;\n    font-weight: 500;\n    text-decoration: none;\n  }\n\n  #he-centers-table a:hover {\n    text-decoration: underline;\n  }\n\n  \/* The Website column shows a \"Website\" link but stores the raw URL in a hidden\n     span so DataTables search still matches against the full URL text. *\/\n  .table-search-hidden {\n    display: none;\n  }\n\n  \/* WordPress theme spacing tweaks (site-specific). *\/\n  .has-text-align-center.wp-block-post-title.has-x-large-font-size {\n    margin-top: 30px !important;\n  }\n\n  .wp-block-group.has-global-padding.is-layout-constrained.wp-container-core-group-is-layout-5da90744.wp-block-group-is-layout-constrained {\n    margin-top: 30px !important;\n  }\n\n  \/* ---- Responsive adjustments for phones\/tablets ---- *\/\n  @media (max-width: 768px) {\n    .site-title {\n      font-size: 2rem;\n    }\n\n    .tabs-container {\n      flex-direction: column;\n      align-items: center;\n    }\n\n    .tab-button {\n      margin: 5px 0;\n      width: 80%;\n    }\n\n    .map-iframe {\n      height: 400px;\n    }\n\n    .table-container {\n      padding: 12px;\n    }\n\n    table.dataTable th,\n    table.dataTable td {\n      padding: 6px 10px;\n      font-size: 14px;\n      white-space: nowrap;\n    }\n  }\n<\/style>\n\n<!-- Third-party libraries (load once per page): Leaflet (map), DataTables (table),\n     jQuery (DataTables dependency), PapaParse (CSV parser). -->\n<link rel=\"stylesheet\" href=\"https:\/\/unpkg.com\/leaflet\/dist\/leaflet.css\" \/>\n<link rel=\"stylesheet\" href=\"https:\/\/cdn.datatables.net\/1.11.5\/css\/jquery.dataTables.min.css\" \/>\n<script src=\"https:\/\/code.jquery.com\/jquery-3.6.0.min.js\"><\/script>\n<script src=\"https:\/\/unpkg.com\/leaflet\/dist\/leaflet.js\"><\/script>\n<script src=\"https:\/\/cdn.datatables.net\/1.11.5\/js\/jquery.dataTables.min.js\"><\/script>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/PapaParse\/5.3.2\/papaparse.min.js\"><\/script>\n\n<main class=\"itBody\">\n  <!-- Static intro copy. Safe to edit text; mirrors the live page content. -->\n  <p>In 2000, the <a href=\"http:\/\/www.bc.edu\/cihe\" target=\"_blank\" rel=\"noopener\">Center for International Higher Education<\/a> at Boston College, under the leadership of Philip G. Altbach, completed the first global inventory of higher education centers and programs around the world (Altbach &amp; Engberg, 2000). The CIHE inventory was updated in 2006 (Altbach et. al. 2006) and again in 2013 (Rumbley et al, 2014), the latter also including a partial inventory of journals publishing on issues related to higher education around the world. In 2024 and 2025, GOHEC worked with CIHE to revitalize the inventory project. The resulting Beta version, available here, is therefore the most current comprehensive global \u201cinventory\u201d of higher education research centers and degree-granting academic programs focused on higher education.<\/p>\n\n  <p>The <i>Inventory of Research Centers<\/i> provides valuable insights into the global landscape of higher education research and training, highlighting key trends and issues that shape the scholarly field of higher education worldwide. The accompanying map not only captures a \u201csnapshot\u201d of higher education research activities but also serves as an exceptional resource to enhance communication and collaboration within the higher education research community.<\/p>\n\n  <p>The version of the inventory of 2025 was developed by a research team that included: <i>Prof.A.Pausits, Prof. I.Frumin, Dr. A.Vorochkov, O.Mostafa.<\/i><\/p>\n\n  <p>Please cite as: Pausits, A., Frumin, I., Vorochkov, A., &#038; Mostafa, O. (2025). <i>Inventory of Higher Education Research Centers <\/i> [Data set]. DOOR, Universit\u00e4t f\u00fcr Weiterbildung Krems. <a href=\"https:\/\/doi.org\/10.48341\/6c60-ga34\" target=\"_blank\" rel=\"noopener\">https:\/\/doi.org\/10.48341\/6c60-ga34<\/a><\/p>\n\n  <p>License: <a href=\"https:\/\/creativecommons.org\/licenses\/by-nc\/4.0\/\" target=\"_blank\" rel=\"noopener\">CC BY-NC 4.0 International <\/a><\/p>\n\n   <p>If you would like more information about our data collection methodology, please <a href=\"https:\/\/docs.google.com\/document\/d\/1YeyFysUAngbCfphN2vnFEP-2oWXF3mhS_CfioAYIq28\/edit?usp=sharing\" target=\"_blank\" rel=\"noopener\">click here<\/a>.<\/p>\n\n  <p>If you have additional information, corrections, or know of any research centers not yet listed, please kindly fill out the form linked below. You can contact us via email: <a href=\"mailto:inventory.gohec@donau-uni.ac.at\">inventory.gohec@donau-uni.ac.at<\/a><\/p>\n\n  <p>\n    <a href=\"https:\/\/docs.google.com\/spreadsheets\/d\/1r3J_1mE9BF8x8mizE0Aafv3FDP7VqQ98\/edit?usp=sharing&#038;ouid=105446149564820474080&#038;rtpof=true&#038;sd=true\" target=\"_blank\" rel=\"noopener\">Download Excel data<\/a>\n  <\/p>\n  <br>\n\n  <!-- Map card: JS mounts the Leaflet map into #he-centers-map. -->\n  <div class=\"map-container\">\n    <h3>Interactive Map of Research Centers<\/h3>\n    <div id=\"he-centers-map\" class=\"map-iframe\"><\/div>\n  <\/div>\n\n  <!-- Table card: heading + clear-filters button, then the DataTables table. -->\n  <div class=\"table-container\">\n    <div class=\"table-header-row\">\n      <h3>Table of Research Centers<\/h3>\n      <button type=\"button\" id=\"clear-he-filters\" class=\"clear-filters-btn\">Clear filters<\/button>\n    <\/div>\n\n    <div class=\"table-scroll\">\n      <!-- Two header rows: row 1 = column titles, row 2 = per-column filter inputs.\n           Row order must match the CSV columns handled in initCentersTable(). -->\n      <table id=\"he-centers-table\" class=\"display\" style=\"width:100%\">\n        <thead>\n          <tr>\n            <th>Research center<\/th>\n            <th>Host organization<\/th>\n            <th>Country<\/th>\n            <th>Website<\/th>\n            <th>Focus Areas<\/th>\n          <\/tr>\n          <tr>\n            <th><input type=\"text\" placeholder=\"Filter Research center\" \/><\/th>\n            <th><input type=\"text\" placeholder=\"Filter Host organization\" \/><\/th>\n            <th><input type=\"text\" placeholder=\"Filter Country\" \/><\/th>\n            <th><input type=\"text\" placeholder=\"Filter Website\" \/><\/th>\n            <th><input type=\"text\" placeholder=\"Filter Focus areas\" \/><\/th>\n          <\/tr>\n        <\/thead>\n        <tbody><\/tbody>\n      <\/table>\n    <\/div>\n  <\/div>\n\n  <!-- Call to action: submit a missing\/updated center via the external form. -->\n  <div class=\"cta-section\">\n    <h3 class=\"cta-heading\">Missing or Editing a Research Center?<\/h3>\n    <p>Help us keep the map complete. If your research center isn&#8217;t listed or you&#8217;d like to update its information, please fill out the form below.<\/p>\n    <a href=\"https:\/\/forms.office.com\/Pages\/ResponsePage.aspx?id=v3HXst56qUWhtClAjz3C5j66EepJPWVHnbXY_51bLcxUNFJSS04wWVNBQ01YT1lBRklVTkM4RTdDQS4u\" class=\"cta-button\" target=\"_blank\" rel=\"noopener\">Add Research Center<\/a>\n  <\/div>\n\n  <!-- Optional panels used only if the tabbed layout (centers\/programs) is enabled. -->\n  <div id=\"centers-panel\" class=\"panel-content active\"><\/div>\n  <div id=\"programs-panel\" class=\"panel-content\"><\/div>\n<\/main>\n\n<script>\n\/*\n  All logic runs inside an IIFE to avoid polluting the global scope.\n  High-level flow:\n    boot() \u2192 build map + load JSON markers, build table from CSV, wire up filters.\n  Deep-linking: ?center=\u2026&country=\u2026 in the URL pre-applies table filters and the\n  same filters can be triggered by clicking marker popup links.\n*\/\n(function () {\n  \/\/ Data files written by the WordPress plugin (cache-busted with ?v=timestamp).\n  const JSON_URL = '\/wp-content\/uploads\/json_and_csv\/he_research_centers.json';\n  const CSV_URL  = '\/wp-content\/uploads\/json_and_csv\/he_research_centers.csv';\n\n  \/\/ Escape user\/sheet text before inserting it into HTML (prevents XSS \/ broken markup).\n  function escapeHtml(value) {\n    return String(value ?? '')\n      .replace(\/&\/g, '&amp;')\n      .replace(\/<\/g, '&lt;')\n      .replace(\/>\/g, '&gt;')\n      .replace(\/\"\/g, '&quot;')\n      .replace(\/'\/g, '&#039;');\n  }\n\n  \/\/ Return a URL only if it is a valid http(s) link; otherwise return '' (drop it).\n  function safeUrl(url) {\n    const value = String(url ?? '').trim();\n    if (!value) return '';\n\n    try {\n      const parsed = new URL(value, window.location.origin);\n      if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {\n        return parsed.href;\n      }\n    } catch (e) {}\n\n    return '';\n  }\n\n  \/\/ Allow only a small whitelist of tags inside map popups; strip everything else.\n  \/\/ This lets the sheet author use <b>, <br>, <a> etc. in names\/descriptions safely.\n  function sanitizePopupHtml(html) {\n    const template = document.createElement('template');\n    template.innerHTML = String(html ?? '');\n\n    const allowedTags = new Set(['BR', 'B', 'STRONG', 'I', 'EM', 'U', 'A', 'P', 'UL', 'OL', 'LI']);\n    const elements = Array.from(template.content.querySelectorAll('*'));\n\n    elements.forEach(function (el) {\n      \/\/ Replace any non-whitelisted element with its plain text content.\n      if (!allowedTags.has(el.tagName)) {\n        el.replaceWith(document.createTextNode(el.textContent || ''));\n        return;\n      }\n\n      \/\/ Strip all attributes except safe ones on <a>.\n      const attrs = Array.from(el.attributes);\n      attrs.forEach(function (attr) {\n        const attrName = attr.name.toLowerCase();\n\n        if (el.tagName === 'A' && (attrName === 'href' || attrName === 'target' || attrName === 'rel')) {\n          return;\n        }\n\n        el.removeAttribute(attr.name);\n      });\n\n      \/\/ Force links to be safe external links only.\n      if (el.tagName === 'A') {\n        const href = el.getAttribute('href') || '';\n        if (!\/^https?:\\\/\\\/\/i.test(href) && !\/^mailto:\/i.test(href)) {\n          el.removeAttribute('href');\n        }\n        el.setAttribute('target', '_blank');\n        el.setAttribute('rel', 'noopener');\n      }\n    });\n\n    return template.innerHTML;\n  }\n\n  \/\/ The per-column filter <input> elements (second <thead> row).\n  function getFilterInputs($table) {\n    return $table.find('thead tr:nth-child(2) th input');\n  }\n\n  \/\/ Normalize a pathname for comparison (strip trailing slashes).\n  function normalizePath(path) {\n    return String(path || '').replace(\/\\\/+$\/, '') || '\/';\n  }\n\n  \/\/ Read deep-link filter values from a URL: ?center=\u2026 (or ?research_center=\u2026) and ?country=\u2026\n  function getUrlFilterValues(urlLike) {\n    const url = urlLike instanceof URL ? urlLike : new URL(urlLike, window.location.href);\n    return {\n      center: (url.searchParams.get('center') || url.searchParams.get('research_center') || '').trim(),\n      country: (url.searchParams.get('country') || '').trim()\n    };\n  }\n\n  \/\/ True if any column filter currently has text.\n  function hasAnyActiveFilters($table) {\n    let active = false;\n    getFilterInputs($table).each(function () {\n      if (String(this.value || '').trim() !== '') {\n        active = true;\n        return false; \/\/ break out of jQuery .each\n      }\n    });\n    return active;\n  }\n\n  \/\/ Show\/hide the \"Clear filters\" button based on current filter state.\n  function updateClearFiltersButton($table) {\n    const btn = document.getElementById('clear-he-filters');\n    if (!btn) return;\n\n    if (hasAnyActiveFilters($table)) {\n      btn.classList.add('is-visible');\n    } else {\n      btn.classList.remove('is-visible');\n    }\n  }\n\n  \/\/ Smoothly scroll the table into view (used after applying a deep-link filter).\n  function scrollToTable() {\n    const target = document.getElementById('he-centers-table');\n    if (!target) return;\n\n    target.scrollIntoView({\n      behavior: 'smooth',\n      block: 'start'\n    });\n  }\n\n  \/\/ Keep the URL in sync with the \"center\" (col 0) and \"country\" (col 2) filters,\n  \/\/ so the current view can be bookmarked \/ shared \/ navigated back to.\n  function syncUrlWithCenterCountry($table, historyMode) {\n    const mode = historyMode || 'replace';\n    const $inputs = getFilterInputs($table);\n\n    const center = ($inputs.eq(0).val() || '').trim();\n    const country = ($inputs.eq(2).val() || '').trim();\n\n    const url = new URL(window.location.href);\n\n    if (center) {\n      url.searchParams.set('center', center);\n    } else {\n      url.searchParams.delete('center');\n      url.searchParams.delete('research_center');\n    }\n\n    if (country) {\n      url.searchParams.set('country', country);\n    } else {\n      url.searchParams.delete('country');\n    }\n\n    \/\/ Anchor to the table so a shared link jumps straight to it.\n    if (center || country) {\n      url.hash = 'he-centers-table';\n    } else {\n      url.hash = '';\n    }\n\n    if (mode === 'push') {\n      window.history.pushState({}, '', url.toString());\n    } else {\n      window.history.replaceState({}, '', url.toString());\n    }\n  }\n\n  \/\/ Reset every column filter and the global search, then refresh URL + button.\n  function clearAllTableFilters(dt, $table, historyMode) {\n    const $inputs = getFilterInputs($table);\n\n    $inputs.each(function () {\n      this.value = '';\n    });\n\n    dt.search('');\n    dt.columns().search('');\n    dt.draw();\n\n    syncUrlWithCenterCountry($table, historyMode || 'replace');\n    updateClearFiltersButton($table);\n  }\n\n  \/\/ Apply specific center\/country filter values to the table (used by deep links).\n  function applyCenterCountryFilters(dt, $table, filters, options) {\n    const opts = options || {};\n    const $inputs = getFilterInputs($table);\n\n    const center = String(filters.center || '').trim();\n    const country = String(filters.country || '').trim();\n\n    \/\/ Start from a clean slate.\n    dt.search('');\n    dt.columns().search('');\n\n    $inputs.each(function () {\n      this.value = '';\n    });\n\n    if (center) {\n      $inputs.eq(0).val(center);\n      dt.column(0).search(center);\n    }\n\n    if (country) {\n      $inputs.eq(2).val(country);\n      dt.column(2).search(country);\n    }\n\n    dt.draw();\n\n    if (opts.updateUrl !== false) {\n      syncUrlWithCenterCountry($table, opts.historyMode || 'replace');\n    }\n\n    updateClearFiltersButton($table);\n\n    if (opts.scrollToTable) {\n      setTimeout(scrollToTable, 60);\n    }\n  }\n\n  \/\/ On first load, apply any filters already present in the page URL.\n  function applyFiltersFromCurrentUrl(dt, $table, options) {\n    const filters = getUrlFilterValues(window.location.href);\n\n    if (!filters.center && !filters.country) {\n      updateClearFiltersButton($table);\n      return;\n    }\n\n    applyCenterCountryFilters(dt, $table, filters, {\n      updateUrl: false, \/\/ URL already reflects these values\n      scrollToTable: !!(options && options.scrollToTable)\n    });\n  }\n\n  \/\/ Intercept clicks on same-page deep links (e.g. marker popups, related links)\n  \/\/ and apply them as table filters instead of doing a full page reload.\n  function installFilterLinksHandler(dt, $table) {\n    if (window.heResearchFilterLinksHandlerInstalled) return; \/\/ install only once\n    window.heResearchFilterLinksHandlerInstalled = true;\n\n    document.addEventListener('click', function (event) {\n      const link = event.target.closest('a[href]');\n      if (!link) return;\n\n      const href = link.getAttribute('href');\n      if (!href || href.startsWith('javascript:')) return;\n\n      let url;\n      try {\n        url = new URL(href, window.location.href);\n      } catch (e) {\n        return;\n      }\n\n      const sameOrigin = url.origin === window.location.origin;\n      const samePath = normalizePath(url.pathname) === normalizePath(window.location.pathname);\n      const filters = getUrlFilterValues(url);\n\n      \/\/ Only hijack links that point to THIS page and carry filter params.\n      if (!sameOrigin || !samePath) return;\n      if (!filters.center && !filters.country) return;\n\n      event.preventDefault();\n      window.history.pushState({}, '', url.toString());\n\n      applyCenterCountryFilters(dt, $table, filters, {\n        updateUrl: false,\n        scrollToTable: true\n      });\n    });\n\n    \/\/ Support browser back\/forward between filtered states.\n    window.addEventListener('popstate', function () {\n      const filters = getUrlFilterValues(window.location.href);\n\n      if (!filters.center && !filters.country) {\n        clearAllTableFilters(dt, $table, 'replace');\n        return;\n      }\n\n      applyCenterCountryFilters(dt, $table, filters, {\n        updateUrl: false,\n        scrollToTable: false\n      });\n    });\n  }\n\n  \/\/ Some WordPress themes wrap content in .wp-block-cover; unwrap so layout is clean.\n  function unwrapWpBlockCover() {\n    document.querySelectorAll('.wp-block-cover').forEach(function (cover) {\n      while (cover.firstChild) {\n        cover.parentNode.insertBefore(cover.firstChild, cover);\n      }\n      cover.remove();\n    });\n  }\n\n  \/\/ Optional tab wiring: only activates if both tabs and both panels exist.\n  function initTabsIfPresent() {\n    const centersTab = document.getElementById('centers-tab');\n    const programsTab = document.getElementById('programs-tab');\n    const centersPanel = document.getElementById('centers-panel');\n    const programsPanel = document.getElementById('programs-panel');\n\n    if (!centersTab || !programsTab || !centersPanel || !programsPanel) return;\n\n    centersTab.addEventListener('click', function () {\n      centersPanel.classList.add('active');\n      programsPanel.classList.remove('active');\n      centersTab.classList.add('active');\n      programsTab.classList.remove('active');\n      invalidateMapSizeSoon();\n      initCentersTable();\n    });\n\n    programsTab.addEventListener('click', function () {\n      centersPanel.classList.remove('active');\n      programsPanel.classList.add('active');\n      centersTab.classList.remove('active');\n      programsTab.classList.add('active');\n    });\n  }\n\n  \/\/ Create the Leaflet map once and cache it on window.heCentersMap.\n  function initCentersMap() {\n    if (window.heCentersMap) return window.heCentersMap; \/\/ already created\n\n    const el = document.getElementById('he-centers-map');\n    if (!el || typeof L === 'undefined') return null;\n\n    \/\/ World view, locked to a single world copy (no infinite horizontal wrap).\n    const map = L.map(el, {\n      worldCopyJump: false,\n      maxBounds: [[-85, -180], [85, 180]],\n      maxBoundsViscosity: 1.0\n    }).setView([20, 0], 2);\n\n    L.tileLayer('https:\/\/{s}.tile.openstreetmap.org\/{z}\/{x}\/{y}.png', {\n      attribution: '\u00a9 OpenStreetMap contributors',\n      noWrap: true,\n      bounds: [[-85, -180], [85, 180]]\n    }).addTo(map);\n\n    \/\/ All markers live in a single layer group for easy clearing\/reloading.\n    window.heCentersMarkers = L.layerGroup().addTo(map);\n\n    \/\/ Keep the user inside the allowed bounds while dragging.\n    map.on('drag', function () {\n      map.panInsideBounds(map.options.maxBounds, { animate: false });\n    });\n\n    window.heCentersMap = map;\n    return map;\n  }\n\n  \/\/ Fetch the JSON and add one marker per center with a sanitized popup.\n  function loadCentersMarkers() {\n    const map = initCentersMap();\n    if (!map) return;\n\n    if (window.heCentersMarkers) {\n      window.heCentersMarkers.clearLayers(); \/\/ avoid duplicate markers on reload\n    }\n\n    fetch(JSON_URL + '?v=' + Date.now())\n      .then(function (r) {\n        if (!r.ok) throw new Error(r.statusText);\n        return r.json();\n      })\n      .then(function (data) {\n        if (!Array.isArray(data) || !data.length) return;\n\n        data.forEach(function (u) {\n          const lat = Number(u.lat);\n          const lon = Number(u.lon);\n\n          \/\/ Skip rows without valid coordinates.\n          if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;\n\n          const rawName = String(u.name || 'Unnamed').trim();\n          const safeName = sanitizePopupHtml(rawName);\n          const descr = sanitizePopupHtml(u.description || '');\n\n          \/\/ Bold the title if the source did not already include <b>\/<strong>.\n          const titleHtml = \/<(b|strong)\\b\/i.test(safeName)\n            ? safeName\n            : '<strong>' + safeName + '<\/strong>';\n\n          const popupHtml = titleHtml + (descr ? '<br>' + descr : '');\n\n          L.marker([lat, lon])\n            .bindPopup(popupHtml)\n            .addTo(window.heCentersMarkers);\n        });\n      })\n      .catch(function (err) {\n        console.error('JSON load error:', err);\n      });\n  }\n\n  \/\/ Leaflet needs a size recalculation when its container becomes visible\/resizes.\n  function invalidateMapSizeSoon() {\n    if (!window.heCentersMap) return;\n    setTimeout(function () {\n      window.heCentersMap.invalidateSize();\n    }, 150);\n  }\n\n  \/\/ Build (or refresh) the DataTables table from the CSV file.\n  function initCentersTable() {\n    if (!window.jQuery || !jQuery.fn.dataTable || typeof Papa === 'undefined') return;\n\n    const $table = jQuery('#he-centers-table');\n    if (!$table.length) return;\n\n    \/\/ If already initialized, just re-adjust column widths and exit.\n    if (jQuery.fn.dataTable.isDataTable('#he-centers-table')) {\n      const dtExisting = $table.DataTable();\n      setTimeout(function () {\n        dtExisting.columns.adjust().draw(false);\n        updateClearFiltersButton($table);\n      }, 0);\n      return;\n    }\n\n    \/\/ Stream + parse the CSV (header row becomes object keys).\n    Papa.parse(CSV_URL + '?v=' + Date.now(), {\n      download: true,\n      header: true,\n      skipEmptyLines: true,\n      complete: function (results) {\n        const data = Array.isArray(results.data) ? results.data : [];\n        let rowsHtml = '';\n\n        \/\/ Build table rows. Column keys must match the CSV headers exactly.\n        data.forEach(function (row) {\n          \/\/ Skip rows with no center name (e.g. trailing blank lines).\n          if (!row['Research center'] || String(row['Research center']).trim() === '') return;\n\n          const rc = row['Research center'] || '';\n          const host = row['Host organization'] || '';\n          const country = row['Country'] || '';\n          const website = row['Website'] || '';\n          const focus = row['Focus areas'] || '';\n\n          \/\/ Website cell: visible \"Website\" link + hidden full URL for searching.\n          const safeWebsite = safeUrl(website);\n          const websiteCell = safeWebsite\n            ? '<span class=\"table-search-hidden\">' + escapeHtml(website) + '<\/span><a href=\"' + escapeHtml(safeWebsite) + '\" target=\"_blank\" rel=\"noopener\">Website<\/a>'\n            : '';\n\n          rowsHtml +=\n            '<tr>' +\n              '<td>' + escapeHtml(rc) + '<\/td>' +\n              '<td>' + escapeHtml(host) + '<\/td>' +\n              '<td>' + escapeHtml(country) + '<\/td>' +\n              '<td>' + websiteCell + '<\/td>' +\n              '<td>' + escapeHtml(focus) + '<\/td>' +\n            '<\/tr>';\n        });\n\n        $table.find('tbody').html(rowsHtml);\n\n        \/\/ Initialize DataTables. orderCellsTop keeps sorting on the title row\n        \/\/ while the second header row is used for filter inputs.\n        const dt = $table.DataTable({\n          autoWidth: false,\n          paging: true,\n          searching: true,\n          ordering: true,\n          info: true,\n          orderCellsTop: true,\n          order: [[2, 'asc'], [0, 'asc']], \/\/ sort by Country, then Research center\n          pageLength: 50,\n          lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, 'All']]\n        });\n\n        \/\/ Wire each per-column filter input to that column's search.\n        $table.find('thead tr:nth-child(2) th input').each(function (colIndex) {\n          jQuery(this).on('input keyup change', function (e) {\n            e.stopPropagation(); \/\/ don't let typing trigger column sorting\n            dt.column(colIndex).search(this.value).draw();\n            syncUrlWithCenterCountry($table, 'replace');\n            updateClearFiltersButton($table);\n          });\n        });\n\n        \/\/ \"Clear filters\" button.\n        jQuery('#clear-he-filters')\n          .off('click')\n          .on('click', function () {\n            clearAllTableFilters(dt, $table, 'replace');\n          });\n\n        \/\/ Enable deep-linking + apply any filters from the current URL.\n        installFilterLinksHandler(dt, $table);\n        applyFiltersFromCurrentUrl(dt, $table, { scrollToTable: true });\n\n        \/\/ Keep the clear button state correct after every redraw.\n        dt.on('draw', function () {\n          updateClearFiltersButton($table);\n        });\n\n        \/\/ Final width adjustment once layout has settled.\n        setTimeout(function () {\n          dt.columns.adjust().draw(false);\n          updateClearFiltersButton($table);\n        }, 0);\n      },\n      error: function (err) {\n        console.error('CSV parse error:', err);\n      }\n    });\n  }\n\n  \/\/ Entry point: run everything in order.\n  function boot() {\n    unwrapWpBlockCover();\n    initTabsIfPresent();\n    loadCentersMarkers();\n    initCentersTable();\n    invalidateMapSizeSoon();\n  }\n\n  \/\/ Run boot() after the DOM is ready (or immediately if it already is).\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', boot, { once: true });\n  } else {\n    boot();\n  }\n\n  \/\/ On resize, re-adjust table column widths and the map size.\n  window.addEventListener('resize', function () {\n    if (!window.jQuery) return;\n\n    if (jQuery.fn.dataTable.isDataTable('#he-centers-table')) {\n      const dt = jQuery('#he-centers-table').DataTable();\n      dt.columns.adjust().draw(false);\n    }\n\n    invalidateMapSizeSoon();\n  });\n})();\n<\/script>\n\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In 2000, the Center for International Higher Education at Boston College, under the leadership of Philip G. Altbach, completed the first global inventory of higher education centers and programs around the world (Altbach &amp; Engberg, 2000). The CIHE inventory was updated in 2006 (Altbach et. al. 2006) and again in 2013 (Rumbley et al, 2014), [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"ub_ctt_via":"","footnotes":""},"class_list":["post-154","page","type-page","status-publish","hentry"],"featured_image_src":null,"wps_subtitle":"a collaboration between GOHEC and CIHE Boston College","_links":{"self":[{"href":"https:\/\/highered-change.org\/index.php\/wp-json\/wp\/v2\/pages\/154","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/highered-change.org\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/highered-change.org\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/highered-change.org\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/highered-change.org\/index.php\/wp-json\/wp\/v2\/comments?post=154"}],"version-history":[{"count":178,"href":"https:\/\/highered-change.org\/index.php\/wp-json\/wp\/v2\/pages\/154\/revisions"}],"predecessor-version":[{"id":699,"href":"https:\/\/highered-change.org\/index.php\/wp-json\/wp\/v2\/pages\/154\/revisions\/699"}],"wp:attachment":[{"href":"https:\/\/highered-change.org\/index.php\/wp-json\/wp\/v2\/media?parent=154"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}