diff options
Diffstat (limited to 'lib/classes/Icon.php')
| -rw-r--r-- | lib/classes/Icon.php | 395 |
1 files changed, 395 insertions, 0 deletions
diff --git a/lib/classes/Icon.php b/lib/classes/Icon.php new file mode 100644 index 0000000..6c586a0 --- /dev/null +++ b/lib/classes/Icon.php @@ -0,0 +1,395 @@ +<?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 + public 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->shapeToPath($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() + { + if (self::isStatic($this->shape)) { + return $this->shape; + } + $shape = array_reverse(explode('/', $this->shape))[0]; + $shape = explode('+', $shape)[0]; + return $shape; + } +} |
