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';
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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',
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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);
}
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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
* like 'seminar'
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
* @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)) {
[$view_attributes, $size] = [$size, null];
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
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);
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
$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];
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
}
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;',
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
$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'])) {
//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))));
}
}