Select Git revision
Forked from
Stud.IP / Stud.IP
Source project has a limited visibility.
-
Closes #1599 Merge request studip/studip!1201
Closes #1599 Merge request studip/studip!1201
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
Icon.class.php 12.36 KiB
<?php
/**
* Icon class is used to create icon objects which can be rendered as
* svg. Output will be html. Optionally, the icon can be rendered
* as a css background.
*
* @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
* @copyright Stud.IP Core Group
* @license GPL2 or any later version
* @since 3.2
*/
class Icon
{
const SVG = 1;
const CSS_BACKGROUND = 4;
const INPUT = 256;
const DEFAULT_SIZE = 16;
const DEFAULT_COLOR = 'blue';
const DEFAULT_ROLE = 'clickable';
const ROLE_INFO = 'info';
const ROLE_CLICKABLE = 'clickable';
const ROLE_ACCEPT = 'accept';
const ROLE_STATUS_GREEN = 'status-green';
const ROLE_INACTIVE = 'inactive';
const ROLE_NAVIGATION = 'navigation';
const ROLE_NEW = 'new';
const ROLE_ATTENTION = 'attention';
const ROLE_STATUS_RED = 'status-red';
const ROLE_INFO_ALT = 'info_alt';
const ROLE_SORT = 'sort';
const ROLE_STATUS_YELLOW = 'status-yellow';
protected $shape;
protected $role;
protected $attributes = [];
/**
* This is the magical Role to Color mapping.
*/
private static $roles_to_colors = [
self::ROLE_INFO => 'black',
self::ROLE_CLICKABLE => 'blue',
self::ROLE_ACCEPT => 'green',
self::ROLE_STATUS_GREEN => 'green',
self::ROLE_INACTIVE => 'grey',
self::ROLE_NAVIGATION => 'blue',
self::ROLE_NEW => 'red',
self::ROLE_ATTENTION => 'red',
self::ROLE_STATUS_RED => 'red',
self::ROLE_INFO_ALT => 'white',
self::ROLE_SORT => 'blue',
self::ROLE_STATUS_YELLOW => 'yellow'
];
// return the color associated to a role
private static function roleToColor($role)
{
if (!isset(self::$roles_to_colors[$role])) {
throw new \InvalidArgumentException('Unknown role: "' . $role . '"');
}
return self::$roles_to_colors[$role];
}
// return the roles! associated to a color
private static function colorToRoles($color)
{
static $colors_to_roles;
if (!$colors_to_roles) {
foreach (self::$roles_to_colors as $r => $c) {
$colors_to_roles[$c][] = $r;
}
}
if (!isset($colors_to_roles[$color])) {
throw new \InvalidArgumentException('Unknown color: "' . $color . '"');
}
return $colors_to_roles[$color];
}
/**
* Create a new Icon object.
*
* This is just a factory method. You could easily just call the
* constructor instead.
*
* @param String $shape Shape of the icon, may contain a mixed definition
* like 'seminar'
* @param String $role Role of the icon, defaults to Icon::DEFAULT_ROLE
* @param Array $attributes Additional attributes like 'title';
* only use semantic ones describing
* this icon regardless of its later
* rendering in a view
* @return Icon object
*/
public static function create($shape, $role = Icon::DEFAULT_ROLE, $attributes = [])
{
// $role may be omitted
if (is_array($role)) {
$attributes = $role;
$role = Icon::DEFAULT_ROLE;
}
return new self($shape, $role, $attributes);
}
/**
* Constructor of the object.
*
* @param String $shape Shape of the icon, may contain a mixed definition
* like 'seminar'
* @param String $role Role of the icon, defaults to Icon::DEFAULT_ROLE
* @param Array $attributes Additional attributes like 'title';
* only use semantic ones describing
* this icon regardless of its later
* rendering in a view
*/
public function __construct($shape, $role = Icon::DEFAULT_ROLE, array $attributes = [])
{
// only defined roles
if (!isset(self::$roles_to_colors[$role])) {
throw new \InvalidArgumentException('Creating an Icon without proper role: "' . $role . '"');
}
// only semantic attributes
if ($non_semantic = array_filter(array_keys($attributes), function ($attr) {
return !in_array($attr, ['title']);
})) {
// DEPRECATED
// TODO starting with the v3.6 the following line should
// be enabled to prevent non-semantic attributes in this position
# throw new \InvalidArgumentException('Creating an Icon with non-semantic attributes:' . json_encode($non_semantic));
}
$this->shape = $shape;
$this->role = $role;
$this->attributes = $attributes;
}
/**
* Returns the `shape` -- the string describing the shape of this instance.
* @return String the shape of this Icon
*/
public function getShape()
{
return $this->shapeToPath($this->shape);
}
/**
* Returns the `role` -- the string describing the role of this instance.
* @return String the role of this Icon
*/
public function getRole()
{
return $this->role;
}
/**
* Returns the semantic `attributes` of this instance, e.g. the title of this Icon
* @return Array the semantic attribiutes of the Icon
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Returns whether this icon intends to signal attention.
*
* @todo This is currently just a heuristic based on the associated icon
* role. Although this is sufficient for the current requirements,
* it could probably in a better, more suitable way.
*
* @return bool
* @since Stud.IP 5.0
*/
public function signalsAttention()
{
return $this->roleToColor($this->role) === 'red';
}
/**
* Function to be called whenever the object is converted to
* string. Internally the same as calling Icon::asImg
*
* @return String representation
*/
public function __toString()
{
return $this->asImg();
}
/**
* Renders the icon inside an img html tag.
*
* @param int $size Optional; Defines the dimension in px of the rendered icon; FALSE prevents any
* width or height attributes
* @param Array $view_attributes Optional; Additional attributes to pass
* into the rendered output
* @return String containing the html representation for the icon.
*/
public function asImg($size = null, $view_attributes = [])
{
if (is_array($size)) {
list($view_attributes, $size) = [$size, null];
}
return sprintf(
'<img %s>',
arrayToHtmlAttributes(
$this->prepareHTMLAttributes($size, $view_attributes)
)
);
}
/**
* Renders the icon inside an input html tag.
*
* @param int $size Optional; Defines the dimension in px of the rendered icon; FALSE prevents any
* width or height attributes
* @param Array $view_attributes Optional; Additional attributes to pass
* into the rendered output
* @return String containing the html representation for the icon.
*/
public function asInput($size = null, $view_attributes = [])
{
if (is_array($size)) {
list($view_attributes, $size) = [$size, null];
}
return sprintf(
'<input type="image" %s>',
arrayToHtmlAttributes(
$this->prepareHTMLAttributes($size, $view_attributes)
)
);
}
/**
* Renders the icon as a set of css background rules.
*
* @param int $size Optional; Defines the size in px of the rendered icon
* @return String containing the html representation for css backgrounds
*/
public function asCSS($size = null)
{
if (self::isStatic($this->shape)) {
return sprintf(
'background-image:url(%1$s);background-size:%2$upx %2$upx;',
$this->shapeToPath($this->shape),
$this->get_size($size)
);
}
return sprintf(
'background-image:url(%1$s);background-size:%2$upx %2$upx;',
$this->get_asset_svg(),
$this->get_size($size)
);
}
/**
* Returns a path to the SVG matching the icon.
*
* @return String containing the html representation for css backgrounds
*/
public function asImagePath()
{
return $this->prepareHTMLAttributes(false, [])['src'];
}
/**
* Returns a new Icon with a changed shape
* @param mixed $shape New value of `shape`
* @return Icon A new Icon with a new `shape`
*/
public function copyWithShape($shape)
{
$clone = clone $this;
$clone->shape = $shape;
return $clone;
}
/**
* Returns a new Icon with a changed role
* @param mixed $role New value of `role`
* @return Icon A new Icon with a new `role`
*/
public function copyWithRole($role)
{
$clone = clone $this;
$clone->role = $role;
return $clone;
}
/**
* Returns a new Icon with new attributes
* @param mixed $attributes New value of `attributes`
* @return Icon A new Icon with a new `attributes`
*/
public function copyWithAttributes($attributes)
{
$clone = clone $this;
$clone->attributes = $attributes;
return $clone;
}
/**
* Prepares the html attributes for use assembling HTML attributes
* from given shape, role, size, semantic and view attributes
*
* @param int $size Size of the icon
* @param array $attributes Additional attributes
* @return Array containing the merged attributes
*/
private function prepareHTMLAttributes($size, $attributes)
{
$dimensions = [];
if ($size !== false) {
$size = $this->get_size($size);
$dimensions = ['width' => $size, 'height' => $size];
}
$result = array_merge($this->attributes, $attributes, $dimensions, [
'src' => self::isStatic($this->shape) ? $this->shape : $this->get_asset_svg(),
]);
if (!isset($result['alt']) && !isset($result['title'])) {
//Add an empty alt attribute to prevent screen readers from
//reading the URL of the icon:
$result['alt'] = '';
}
$classNames = 'icon-role-' . $this->role;
if (!self::isStatic($this->shape)) {
$classNames .= ' icon-shape-' . $this->shape;
}
$result['class'] = isset($result['class']) ? $result['class'] . ' ' . $classNames : $classNames;
return $result;
}
/**
* Get the correct asset for an SVG icon.
*
* @return String containing the url of the corresponding asset
*/
protected function get_asset_svg()
{
return Assets::url('images/icons/' . self::roleToColor($this->role) . '/' . $this->shapeToPath($this->shape) . '.svg');
}
/**
* Get the size of the icon. If a size was passed as a parameter and
* inside the attributes array during icon construction, the size from
* the attributes will be used.
*
* @param int $size size of the icon
* @return int Size of the icon in pixels
*/
protected function get_size($size)
{
$size = $size ?: Icon::DEFAULT_SIZE;
if (isset($this->attributes['size'])) {
$parts = explode('@', $this->attributes['size'], 2);
$size = $parts[0];
$temp = $parts[1] ?? null;
unset($this->attributes['size']);
}
return (int)$size;
}
// an icon is static if it starts with 'http'
private static function isStatic($shape)
{
return mb_strpos($shape, 'http') === 0;
}
// transforms a shape w/ possible additions (`shape`) to a path `(addition/)?shape`
private function shapeToPath()
{
return self::isStatic($this->shape)
? $this->shape :
join('/', array_reverse(explode('+', preg_replace('/\.svg$/', '', $this->shape))));
}
}