560 lines
13 KiB
PHP
560 lines
13 KiB
PHP
<?php
|
|
|
|
/**
|
|
* AUTOR: Clemens Schwaighofer
|
|
* CREATED: 2023/6/1
|
|
* DESCRIPTION:
|
|
* html builder: element
|
|
* nested and connected objects
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace CoreLibs\Template\HtmlBuilder;
|
|
|
|
use CoreLibs\Template\HtmlBuilder\General\Settings;
|
|
use CoreLibs\Template\HtmlBuilder\General\Error;
|
|
use CoreLibs\Template\HtmlBuilder\General\HtmlBuilderExcpetion;
|
|
|
|
class Element
|
|
{
|
|
/** @var string */
|
|
private string $tag = '';
|
|
/** @var string */
|
|
private string $id = '';
|
|
/** @var string */
|
|
private string $name = '';
|
|
/** @var string */
|
|
private string $content = '';
|
|
/** @var array<string> */
|
|
private array $css = [];
|
|
/** @var array<string,mixed> */
|
|
private array $options = [];
|
|
/** @var array<Element> list of elements */
|
|
private array $sub = [];
|
|
|
|
/**
|
|
* create new html element
|
|
*
|
|
* @param string $tag html tag (eg div, button, etc)
|
|
* @param string $id html tag id, used also for name if name
|
|
* not set in $options
|
|
* @param string $content content text inside, eg <div>Content</div>
|
|
* if sub elements exist, they are added after content
|
|
* @param array<string> $css array of css names, put style in $options
|
|
* @param array<string,string> $options Additional element options in
|
|
* key = value format
|
|
* eg: onClick => 'something();'
|
|
* id, css are skipped
|
|
* name only set on input/button
|
|
* @throws HtmlBuilderExcpetion
|
|
*/
|
|
public function __construct(
|
|
string $tag,
|
|
string $id = '',
|
|
string $content = '',
|
|
array $css = [],
|
|
array $options = []
|
|
) {
|
|
// exit if not valid tag
|
|
try {
|
|
$this->setTag($tag);
|
|
} catch (HtmlBuilderExcpetion $e) {
|
|
throw new HtmlBuilderExcpetion('Could not create Element', 0, $e);
|
|
}
|
|
$this->setId($id);
|
|
$this->setName($options['name'] ?? '');
|
|
$this->setContent($content);
|
|
$this->addCss(...$css);
|
|
$this->setOptions($options);
|
|
}
|
|
|
|
/**
|
|
* set tag
|
|
*
|
|
* @param string $tag
|
|
* @return void
|
|
* @throws HtmlBuilderExcpetion
|
|
*/
|
|
public function setTag(string $tag): void
|
|
{
|
|
// tag must be letters only
|
|
if (!preg_match("/^[A-Za-z]+$/", $tag)) {
|
|
Error::setError(
|
|
'201',
|
|
'invalid or empty tag',
|
|
['tag' => $tag]
|
|
);
|
|
throw new HtmlBuilderExcpetion('Invalid or empty tag: ' . $tag);
|
|
}
|
|
$this->tag = $tag;
|
|
}
|
|
|
|
/**
|
|
* get the tag name
|
|
*
|
|
* @return string HTML element tag
|
|
*/
|
|
public function getTag(): string
|
|
{
|
|
return $this->tag;
|
|
}
|
|
|
|
/**
|
|
* set the element id
|
|
*
|
|
* @param string $id
|
|
* @return void
|
|
*/
|
|
public function setId(string $id): void
|
|
{
|
|
// invalid id and name check too
|
|
// be strict: [a-zA-Z0-9], -, _
|
|
// cannot start with digit, two hyphens or a hyphen with a digit:
|
|
// 0abc
|
|
// __abc
|
|
// _0abc
|
|
if (
|
|
!empty($id) &&
|
|
!preg_match("/^[A-Za-z][\w-]*$/", $id)
|
|
) {
|
|
Error::setWarning(
|
|
'202',
|
|
'possible invalid id',
|
|
['id' => $id, 'tag' => $this->getTag()]
|
|
);
|
|
// TODO: shoud throw error
|
|
}
|
|
$this->id = $id;
|
|
}
|
|
|
|
/**
|
|
* get the html tag id
|
|
*
|
|
* @return string HTML element id
|
|
*/
|
|
public function getId(): string
|
|
{
|
|
return $this->id;
|
|
}
|
|
|
|
/**
|
|
* Set name for elements
|
|
* only for elements that need it (input/button/form)
|
|
*
|
|
* @param string $name
|
|
* @return void
|
|
*/
|
|
public function setName(string $name): void
|
|
{
|
|
if (
|
|
!empty($name) &&
|
|
!preg_match("/^[A-Za-z][\w-]*$/", $name)
|
|
) {
|
|
Error::setWarning(
|
|
'203',
|
|
'possible invalid name',
|
|
['name' => $name, 'id' => $this->getId(), 'tag' => $this->getTag()]
|
|
);
|
|
// TODO: shoud throw error
|
|
}
|
|
$this->name = $name;
|
|
}
|
|
|
|
/**
|
|
* get the name if set
|
|
*
|
|
* @return string Optional HTML name (eg for input)
|
|
*/
|
|
public function getName(): string
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
/**
|
|
* Set new content for element
|
|
*
|
|
* @param string $content
|
|
* @return void
|
|
*/
|
|
public function setContent(string $content): void
|
|
{
|
|
$this->content = $content;
|
|
}
|
|
|
|
/**
|
|
* get the elment text content (not sub elements)
|
|
*
|
|
* @return string HTML content text as is
|
|
*/
|
|
public function getContent(): string
|
|
{
|
|
return $this->content;
|
|
}
|
|
|
|
/**
|
|
* set or update options
|
|
*
|
|
* @param array<string,mixed> $options
|
|
* @return void
|
|
*/
|
|
public function setOptions(array $options): void
|
|
{
|
|
foreach ($options as $key => $value) {
|
|
if (empty($key)) {
|
|
Error::setError(
|
|
'110',
|
|
'Cannot set option with empty key',
|
|
['id' => $this->getId(), 'tag' => $this->getTag()]
|
|
);
|
|
// TODO: shoud throw error
|
|
continue;
|
|
}
|
|
// if data is null
|
|
if ($value === null) {
|
|
if (isset($this->options[$key])) {
|
|
unset($this->options[$key]);
|
|
} else {
|
|
Error::setError(
|
|
'210',
|
|
'Cannot set option with null value',
|
|
['id' => $this->getId(), 'tag' => $this->getTag()]
|
|
);
|
|
}
|
|
// TODO: shoud throw error
|
|
continue;
|
|
}
|
|
$this->options[$key] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* get the options array
|
|
* also holds "name" option
|
|
* anything like: style, javascript, value or any other html tag option
|
|
* right side can be empty but not null
|
|
*
|
|
* @return array<string,string> get options as list html option name and value
|
|
*/
|
|
public function getOptions(): array
|
|
{
|
|
return $this->options;
|
|
}
|
|
|
|
// Sub Elements
|
|
|
|
/**
|
|
* get the sub elements (array of Elements)
|
|
*
|
|
* @return array<Element> Array of Elements (that can have sub elements)
|
|
*/
|
|
public function getSub(): array
|
|
{
|
|
return $this->sub;
|
|
}
|
|
|
|
/**
|
|
* add one or many sub elements (add at the end)
|
|
*
|
|
* @param Element $sub One or many elements to add
|
|
* @return void
|
|
* @throws HtmlBuilderExcpetion
|
|
*/
|
|
public function addSub(Element ...$sub): void
|
|
{
|
|
foreach ($sub as $_sub) {
|
|
// if one of the elements is the same as this class, ignore it
|
|
// with this we avoid self reference loop
|
|
if ($_sub == $this) {
|
|
Error::setError(
|
|
'100',
|
|
'Cannot assign Element to itself, this would create an infinite loop',
|
|
['id' => $this->getId(), 'tag' => $this->getTag()]
|
|
);
|
|
throw new HtmlBuilderExcpetion('Cannot assign Element to itself, this would create an infinite loop');
|
|
}
|
|
array_push($this->sub, $_sub);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove an element from the sub array
|
|
* By pos in array or id set on first level
|
|
*
|
|
* @param int|string $id String id name or int pos number in array
|
|
* @return void
|
|
*/
|
|
public function removeSub(int|string $id): void
|
|
{
|
|
// find element with id and remove it
|
|
// or when number find pos in sub and remove it
|
|
if (is_int($id)) {
|
|
if (!isset($this->sub[$id])) {
|
|
return;
|
|
}
|
|
unset($this->sub[$id]);
|
|
return;
|
|
}
|
|
// only on first level
|
|
foreach ($this->sub as $pos => $el) {
|
|
if (
|
|
$el->getId() === $id
|
|
) {
|
|
unset($this->sub[$pos]);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* remove all sub elements
|
|
*
|
|
* @return void
|
|
*/
|
|
public function resetSub(): void
|
|
{
|
|
$this->sub = [];
|
|
}
|
|
|
|
// CSS Elements
|
|
|
|
/**
|
|
* get the current set css elements
|
|
*
|
|
* @return array<string> list of css element entries
|
|
*/
|
|
public function getCss(): array
|
|
{
|
|
return $this->css;
|
|
}
|
|
|
|
/**
|
|
* add one or many new css elements
|
|
* Note that we can chain: add/remove/reset
|
|
*
|
|
* @param string ...$css one or more css strings to add
|
|
* @return Element Current element for chaining
|
|
*/
|
|
public function addCss(string ...$css): Element
|
|
{
|
|
// should do check for empty/invalid css
|
|
$_set_css = [];
|
|
foreach ($css as $_css) {
|
|
if (empty($_css)) {
|
|
Error::setError(
|
|
'204',
|
|
'cannot have empty css string',
|
|
);
|
|
// TODO: shoud throw error
|
|
continue;
|
|
}
|
|
// -?[_A-Za-z][_A-Za-z0-9-]*
|
|
if (!preg_match("/^-?[_A-Za-z][_A-Za-z0-9-]*$/", $_css)) {
|
|
Error::setWarning(
|
|
'205',
|
|
'possible invalid css string',
|
|
['css' => $_css, 'id' => $this->id, 'tag' => $this->tag]
|
|
);
|
|
// TODO: shoud throw error
|
|
}
|
|
$_set_css[] = $_css;
|
|
}
|
|
$this->css = array_unique(array_merge($this->css, $_set_css));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* remove one or more css elements
|
|
* Note that we can chain: add/remove/reset
|
|
*
|
|
* @param string ...$css one or more css strings to remove
|
|
* @return Element Current element for chaining
|
|
*/
|
|
public function removeCss(string ...$css): Element
|
|
{
|
|
$this->css = array_diff($this->css, $css);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* unset all css elements
|
|
* Note that we can chain: add/remove/reset
|
|
*
|
|
* @return Element
|
|
*/
|
|
public function resetCss(): Element
|
|
{
|
|
$this->css = [];
|
|
return $this;
|
|
}
|
|
|
|
// build output from tree
|
|
|
|
/**
|
|
* build html string from the current element tree (self)
|
|
* or from the Element tree given as parameter
|
|
* if $add_nl is set then new lines are added before each sub element added
|
|
* no indet is done (tab or other)
|
|
*
|
|
* @param Element|null $tree Different Element tree to build
|
|
* if not set (null), self is used
|
|
* @param bool $add_nl [default=false] Optional output string line breaks
|
|
* @return string HTML as string
|
|
*/
|
|
public function buildHtml(Element $tree = null, bool $add_nl = false): string
|
|
{
|
|
// print "D01: " . microtime(true) . "<br>";
|
|
if ($tree === null) {
|
|
$tree = $this;
|
|
}
|
|
$line = '<' . $tree->getTag();
|
|
|
|
if ($tree->getId()) {
|
|
$line .= ' id="' . $tree->getId() . '"';
|
|
if (in_array($tree->getTag(), Settings::NAME_ELEMENTS)) {
|
|
$line .= ' name="'
|
|
. (!empty($tree->getName()) ? $tree->getName() : $tree->getId())
|
|
. '"';
|
|
}
|
|
}
|
|
if (count($tree->getCss())) {
|
|
$line .= ' class="' . join(' ', $tree->getCss()) . '"';
|
|
}
|
|
foreach ($tree->getOptions() as $key => $item) {
|
|
// skip
|
|
if (in_array($key, Settings::SKIP_OPTIONS)) {
|
|
continue;
|
|
}
|
|
$line .= ' ' . $key . '="' . $item . '"';
|
|
}
|
|
$line .= '>';
|
|
if (strlen($tree->getContent()) > 0) {
|
|
$line .= $tree->getContent();
|
|
}
|
|
// sub nodes
|
|
foreach ($tree->getSub() as $sub) {
|
|
if ($add_nl === true) {
|
|
$line .= "\n";
|
|
}
|
|
$line .= $tree->buildHtml($sub, $add_nl);
|
|
if ($add_nl === true) {
|
|
$line .= "\n";
|
|
}
|
|
}
|
|
|
|
// close line if needed
|
|
if (!in_array($tree->getTag(), Settings::NO_CLOSE)) {
|
|
$line .= '</' . $tree->getTag() . '>';
|
|
}
|
|
|
|
return $line;
|
|
}
|
|
|
|
// this is static
|
|
|
|
/**
|
|
* Builds a single string from an array of elements
|
|
* a new line can be added before each new element if $add_nl is set to true
|
|
*
|
|
* @param array<Element> $list array of Elements, uses buildHtml internal
|
|
* @param bool $add_nl [default=false] Optional output string line breaks
|
|
* @return string HTML as string
|
|
*/
|
|
public static function buildHtmlFromList(array $list, bool $add_nl = false): string
|
|
{
|
|
$output = '';
|
|
foreach ($list as $el) {
|
|
if (!empty($output) && $add_nl === true) {
|
|
$output .= "\n";
|
|
}
|
|
$output .= $el->buildHtml();
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
// so we can call builder statically
|
|
|
|
/**
|
|
* Search element tree for id and add
|
|
* if id is empty add at element given in parameter $base
|
|
*
|
|
* @param Element $base Element to attach to
|
|
* @param Element $attach Element to attach (single)
|
|
* @param string $id Optional id, if empty then attached at the end
|
|
* If set will loop through ALL sub elements until
|
|
* matching id found. if not found, not added
|
|
* @return Element Element with attached sub element
|
|
*/
|
|
public static function addElementWithId(
|
|
Element $base,
|
|
Element $attach,
|
|
string $id = ''
|
|
): Element {
|
|
// no id or matching id
|
|
if (
|
|
empty($id) ||
|
|
$base->getId() == $id
|
|
) {
|
|
$base->addSub($attach);
|
|
return $base;
|
|
}
|
|
// find id in 'id' in all 'sub'
|
|
foreach ($base->getSub() as $el) {
|
|
self::addElementWithId($el, $attach, $id);
|
|
}
|
|
|
|
return $base;
|
|
}
|
|
|
|
/**
|
|
* add one or more elemens to $base
|
|
*
|
|
* @param Element $base Element to attach to
|
|
* @param Element ...$attach Element or Elements to attach
|
|
* @return Element Element with attached sub elements
|
|
*/
|
|
public static function addElement(
|
|
Element $base,
|
|
Element ...$attach
|
|
): Element {
|
|
// we must make sure we do not self attach
|
|
$base->addSub(...$attach);
|
|
return $base;
|
|
}
|
|
|
|
/**
|
|
* Static call version for building
|
|
* not recommended to be used, rather use "Element->buildHtml()"
|
|
* wrapper for buildHtml
|
|
*
|
|
* @param ?Element $tree Element tree to build
|
|
* if not set returns empty string
|
|
* @param bool $add_nl [default=false] Optional output string line break
|
|
* @return string build html as string
|
|
* @deprecated Do not use, use Element->buildHtml() instead
|
|
*/
|
|
public static function printHtmlFromObject(Element $tree = null, bool $add_nl = false): string
|
|
{
|
|
// nothing ->bad
|
|
if ($tree === null) {
|
|
return '';
|
|
}
|
|
return $tree->buildHtml(add_nl: $add_nl);
|
|
}
|
|
|
|
/**
|
|
* Undocumented function
|
|
* wrapper for buildHtmlFromList
|
|
*
|
|
* @param array<Element> $list array of Elements to build string from
|
|
* @param bool $add_nl [default=false] Optional output string line break
|
|
* @return string build html as string
|
|
*/
|
|
public static function printHtmlFromArray(array $list, bool $add_nl = false): string
|
|
{
|
|
return self::buildHtmlFromList($list, $add_nl);
|
|
}
|
|
}
|
|
|
|
// __END__
|