Skip to content
Snippets Groups Projects
Icon.class.php 16.1 KiB
Newer Older
<?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 RENDERING_MODE_IMG = 'img';
    const RENDERING_MODE_EMBED = 'embed';
    const RENDERING_MODE_USE = 'use';

    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
     * @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);
    }

    protected static $rendering_mode = self::RENDERING_MODE_IMG;
    protected static $used_shapes = [];
    protected static $used_extras = [];

    public static function setRenderingMode($mode)
    {
        self::$rendering_mode = $mode;
    }

    public static function renderUsedIcons()
    {
        if (count(self::$used_shapes) === 0) {
            return '';
        }

        $icons = [];
        foreach (self::$used_shapes as $shape) {
            $icons[] = sprintf(
                '<symbol id="shape-%s">%s</symbol>',
                $shape,
                file_get_contents($GLOBALS['STUDIP_BASE_PATH'] . '/resources/icons/' . $shape . '.svg')
            );
        }
        foreach (self::$used_extras as $extra) {
            $icons[] = sprintf(
                '<mask id="mask-%s">%s</mask>',
                $extra,
                file_get_contents($GLOBALS['STUDIP_BASE_PATH'] . '/resources/icons/extras/' . $extra . '-mask.xml')
            );
            $icons[] = sprintf(
                '<symbol id="extra-%s">%s</symbol>',
                $extra,
                file_get_contents($GLOBALS['STUDIP_BASE_PATH'] . '/resources/icons/extras/' . $extra . '-path.xml')
            );
        }
        return sprintf(
            '<svg shape-rendering="geometricPrecision" style="position: absolute; pointer-events: none; right: 0; bottom: 0;"><defs>%s</defs></svg>',
            implode('', $icons)
        );
    }

    /**
     * Constructor of the object.
     *
     * @param String $shape      Shape of the icon, may contain a mixed definition
     * @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;
    }

Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
    /**
     * 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)) {
            [$view_attributes, $size] = [$size, null];

        if (self::isStatic($this->shape) || self::$rendering_mode === self::RENDERING_MODE_IMG) {
            return sprintf(
                '<img %s>',
                arrayToHtmlAttributes(
                    $this->prepareHTMLAttributes($size, $view_attributes)
                )
            );
        }

        if (self::$rendering_mode === self::RENDERING_MODE_USE) {
            $shape = $this->shape;

            $extra = false;
            if (strpos($shape, '+') !== false) {
                list($shape, $extra) = explode('+', $shape, 2);

                if (!in_array($extra, self::$used_extras)) {
                    self::$used_extras[] = $extra;
                }
            }

            if (!in_array($shape, self::$used_shapes)) {
                self::$used_shapes[] = $shape;
            }

            $attributes = $this->prepareHTMLAttributes($size, $view_attributes);

            $title = $attributes['title'] ?? false;
            unset($attributes['src'], $attributes['title']);

            if ($extra === false) {
                return sprintf(
                    '<svg %s>%s<use href="#shape-%s"/></svg>',
                    arrayToHtmlAttributes($attributes),
                    $title ? '<title>' . $title . '</title>' : '',
                    $shape
                );
            }

            return sprintf(
                '<svg %s>%s<use href="#extra-%s"/><use mask="url(#mask-%s)" href="#shape-%s"/></svg>',
                arrayToHtmlAttributes($attributes),
                $title ? '<title>' . $title . '</title>' : '',
                $extra,
                $extra,
                $shape
            );
        }

        if (self::$rendering_mode === self::RENDERING_MODE_EMBED) {
            $attributes = $this->prepareHTMLAttributes($size, $view_attributes);
            unset($attributes['src']);
            return sprintf(
                '<svg %s><g>%s</g></svg>',
                arrayToHtmlAttributes($attributes),
                file_get_contents($this->get_asset_svg())
            );
        }

        throw new Exception('Invalid rendering mode');
    }

    /**
     * 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)) {
            [$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_icon_url(),
            $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_icon_url(),
        ]);

        if (!isset($result['alt']) && !isset($result['title'])) {
Moritz Strohm's avatar
Moritz Strohm committed
            //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;
    }

    protected function get_icon_url()
    {
        return implode('/', [
            rtrim($GLOBALS['ABSOLUTE_URI_STUDIP'], '/'),
            'icon.php',
            self::roleToColor($this->role),
            $this->shapeToPath($this->shape),
        ]);
    }

    /**
     * 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))));
    }
}