@@ -170,7 +170,7 @@ class Session(ABC):
170170 id : str
171171 input : Inputs
172172 output : Outputs
173- clientdata : Inputs
173+ clientdata : ClientData
174174 user : str | None
175175 groups : list [str ] | None
176176
@@ -520,7 +520,7 @@ def __init__(
520520
521521 self .input : Inputs = Inputs (dict ())
522522 self .output : Outputs = Outputs (self , self .ns , outputs = dict ())
523- self .clientdata : Inputs = Inputs ( dict () )
523+ self .clientdata : ClientData = ClientData ( self )
524524
525525 self .user : str | None = None
526526 self .groups : list [str ] | None = None
@@ -694,16 +694,8 @@ def _manage_inputs(self, data: dict[str, object]) -> None:
694694 # The keys[0] value is already a fully namespaced id; make that explicit by
695695 # wrapping it in ResolvedId, otherwise self.input will throw an id
696696 # validation error.
697- k = keys [0 ]
698- self .input [ResolvedId (k )]._set (val )
699697
700- # shiny.js sets a special class of inputs (clientdata) for things like the
701- # URL, output sizes, etc. On the frontend, these all have the prefix
702- # ".clientdata_". For example, this is where .clientdata_url_search is set:
703- # https://github.com/rstudio/shiny/blob/58e1521/srcts/src/shiny/index.ts#L631-L632
704- if k .startswith (".clientdata_" ):
705- k2 = k .split ("_" , 1 )[1 ]
706- self .clientdata [ResolvedId (k2 )]._set (val )
698+ self .input [ResolvedId (keys [0 ])]._set (val )
707699
708700 self .output ._manage_hidden ()
709701
@@ -1367,6 +1359,219 @@ def __dir__(self):
13671359 return list (self ._map .keys ())
13681360
13691361
1362+ @add_example ()
1363+ class ClientData :
1364+ """
1365+ Reactively read client data from the browser.
1366+
1367+ This class provides access to client data values, such as the URL components, the
1368+ pixel ratio of the device, and the properties of outputs.
1369+
1370+ Raises
1371+ ------
1372+ RuntimeError
1373+ If a method is called outside of a reactive context.
1374+ """
1375+
1376+ def __init__ (self , session : Session ) -> None :
1377+ self ._session : Session = session
1378+
1379+ def url_hash (self ) -> str :
1380+ """
1381+ Reactively read the hash part of the URL.
1382+ """
1383+ return self ._read_input ("url_hash" )
1384+
1385+ def url_hash_initial (self ) -> str :
1386+ """
1387+ Reactively read the initial hash part of the URL.
1388+ """
1389+ return self ._read_input ("url_hash_initial" )
1390+
1391+ def url_hostname (self ) -> str :
1392+ """
1393+ Reactively read the hostname part of the URL.
1394+ """
1395+ return self ._read_input ("url_hostname" )
1396+
1397+ def url_pathname (self ) -> str :
1398+ """
1399+ The pathname part of the URL.
1400+ """
1401+ return self ._read_input ("url_pathname" )
1402+
1403+ def url_port (self ) -> int :
1404+ """
1405+ Reactively read the port part of the URL.
1406+ """
1407+ return cast (int , self ._read_input ("url_port" ))
1408+
1409+ def url_protocol (self ) -> str :
1410+ """
1411+ Reactively read the protocol part of the URL.
1412+ """
1413+ return self ._read_input ("url_protocol" )
1414+
1415+ def url_search (self ) -> str :
1416+ """
1417+ Reactively read the search part of the URL.
1418+ """
1419+ return self ._read_input ("url_search" )
1420+
1421+ def pixelratio (self ) -> float :
1422+ """
1423+ Reactively read the pixel ratio of the device.
1424+ """
1425+ return cast (int , self ._read_input ("pixelratio" ))
1426+
1427+ def output_height (self , id : str ) -> float | None :
1428+ """
1429+ Reactively read the height of an output.
1430+
1431+ Parameters
1432+ ----------
1433+ id
1434+ The id of the output.
1435+
1436+ Returns
1437+ -------
1438+ float | None
1439+ The height of the output, or None if the output does not exist (or does not
1440+ report its height).
1441+ """
1442+ return cast (float , self ._read_output (id , "height" ))
1443+
1444+ def output_width (self , id : str ) -> float | None :
1445+ """
1446+ Reactively read the width of an output.
1447+
1448+ Parameters
1449+ ----------
1450+ id
1451+ The id of the output.
1452+
1453+ Returns
1454+ -------
1455+ float | None
1456+ The width of the output, or None if the output does not exist (or does not
1457+ report its width).
1458+ """
1459+ return cast (float , self ._read_output (id , "width" ))
1460+
1461+ def output_hidden (self , id : str ) -> bool | None :
1462+ """
1463+ Reactively read whether an output is hidden.
1464+
1465+ Parameters
1466+ ----------
1467+ id
1468+ The id of the output.
1469+
1470+ Returns
1471+ -------
1472+ bool | None
1473+ Whether the output is hidden, or None if the output does not exist.
1474+ """
1475+ return cast (bool , self ._read_output (id , "hidden" ))
1476+
1477+ def output_bg_color (self , id : str ) -> str | None :
1478+ """
1479+ Reactively read the background color of an output.
1480+
1481+ Parameters
1482+ ----------
1483+ id
1484+ The id of the output.
1485+
1486+ Returns
1487+ -------
1488+ str | None
1489+ The background color of the output, or None if the output does not exist (or
1490+ does not report its bg color).
1491+ """
1492+ return cast (str , self ._read_output (id , "bg" ))
1493+
1494+ def output_fg_color (self , id : str ) -> str | None :
1495+ """
1496+ Reactively read the foreground color of an output.
1497+
1498+ Parameters
1499+ ----------
1500+ id
1501+ The id of the output.
1502+
1503+ Returns
1504+ -------
1505+ str | None
1506+ The foreground color of the output, or None if the output does not exist (or
1507+ does not report its fg color).
1508+ """
1509+ return cast (str , self ._read_output (id , "fg" ))
1510+
1511+ def output_accent_color (self , id : str ) -> str | None :
1512+ """
1513+ Reactively read the accent color of an output.
1514+
1515+ Parameters
1516+ ----------
1517+ id
1518+ The id of the output.
1519+
1520+ Returns
1521+ -------
1522+ str | None
1523+ The accent color of the output, or None if the output does not exist (or
1524+ does not report its accent color).
1525+ """
1526+ return cast (str , self ._read_output (id , "accent" ))
1527+
1528+ def output_font (self , id : str ) -> str | None :
1529+ """
1530+ Reactively read the font(s) of an output.
1531+
1532+ Parameters
1533+ ----------
1534+ id
1535+ The id of the output.
1536+
1537+ Returns
1538+ -------
1539+ str | None
1540+ The font family of the output, or None if the output does not exist (or
1541+ does not report its font styles).
1542+ """
1543+ return cast (str , self ._read_output (id , "font" ))
1544+
1545+ def _read_input (self , key : str ) -> str :
1546+ self ._check_current_context (key )
1547+
1548+ id = ResolvedId (f".clientdata_{ key } " )
1549+ if id not in self ._session .input :
1550+ raise ValueError (
1551+ f"ClientData value '{ key } ' not found. Please report this issue."
1552+ )
1553+
1554+ return self ._session .input [id ]()
1555+
1556+ def _read_output (self , id : str , key : str ) -> str | None :
1557+ self ._check_current_context (f"output_{ key } " )
1558+
1559+ input_id = ResolvedId (f".clientdata_output_{ id } _{ key } " )
1560+ if input_id in self ._session .input :
1561+ return self ._session .input [input_id ]()
1562+ else :
1563+ return None
1564+
1565+ @staticmethod
1566+ def _check_current_context (key : str ) -> None :
1567+ try :
1568+ reactive .get_current_context ()
1569+ except RuntimeError :
1570+ raise RuntimeError (
1571+ f"session.clientdata.{ key } () must be called from within a reactive context."
1572+ )
1573+
1574+
13701575# ======================================================================================
13711576# Outputs
13721577# ======================================================================================
0 commit comments