mustache_templates-8.x-1.0-beta4/src/Helpers/MustacheRenderTemplate.php
src/Helpers/MustacheRenderTemplate.php
<?php
namespace Drupal\mustache\Helpers;
use Drupal\Core\Url;
use Drupal\mustache\Exception\MustacheException;
/**
* A helper class for building arrays to render Mustache templates.
*/
class MustacheRenderTemplate {
/**
* The render array representation.
*
* @var array
*/
protected $render = ['#type' => 'mustache'];
/**
* MustacheRenderTemplate constructor.
*
* @param string $template
* The name of the Mustache template. It may be either defined
* by a hook implementation of hook_mustache_templates(), or
* is solely existent as a template file inside the theme.
* Example: The template name "foo_bar" is a template file being
* located inside the 'templates' theme directory as "foo-bar.mustache.tpl".
* Alternatively, use the second parameter $inline_content to specify the
* template directly with inline content instead of using template files.
* A template name must always be specified, regardless of being a
* a registered file or an inline template.
* @param string $inline_content
* (Optional) Use this argument to specify the content of the template
* inline, instead of relying to a template file. This might be useful for
* quick template builds or dynamically generated template contents.
*/
public function __construct($template, $inline_content = NULL) {
$this->render['#template'] = $template;
if (isset($inline_content)) {
$this->render['#inline'] = $inline_content;
}
}
/**
* Creates a new MustacheRenderTemplate instance.
*
* @param string $template
* The name of the Mustache template. It may be either defined
* by a hook implementation of hook_mustache_templates(), or
* is solely existent as a template file inside the theme.
* Example: The template name "foo_bar" is a template file being
* located inside the 'templates' theme directory as "foo-bar.mustache.tpl".
* Alternatively, use the second parameter $inline_content to specify the
* template directly with inline content instead of using template files.
* A template name must always be specified, regardless of being a
* a registered file or an inline template.
* @param string $inline_content
* (Optional) Use this argument to specify the content of the template
* inline, instead of relying to a template file. This might be useful for
* quick template builds or dynamically generated template contents.
*
* @return static
*/
public static function build($template, $inline_content = NULL) {
return new static($template, $inline_content);
}
/**
* Set the data used for rendering.
*
* @param mixed $data
* An array or object which holds the data for PHP rendering.
* @param array|null $select
* (Optional) The array holding the keys for nested data selection.
*
* @return $this
* The instance itself.
*/
public function usingData($data, array $select = NULL) {
$this->render['#data'] = $data;
if (isset($select)) {
$this->selectingSubsetFromData($select);
}
return $this;
}
/**
* Define the url from where to receive Json-encoded data.
*
* @param \Drupal\Core\Url|string $url
* The url as object or string.
* @param array|null $select
* (Optional) The array holding the keys for nested data selection.
*
* @return $this
* The instance itself.
*/
public function usingDataFromUrl($url, array $select = NULL) {
if (!($url instanceof Url) && !is_string($url)) {
throw new MustacheException(t('The $url param must be a Url object or string.'));
}
if (is_string($url)) {
try {
$url = Url::fromUri($url);
}
catch (\InvalidArgumentException $e) {
$url = Url::fromUserInput($url);
}
}
$this->render['#data'] = $url;
if (isset($select)) {
$this->selectingSubsetFromData($select);
}
return $this;
}
/**
* Specifies the subset to select out of the data.
*
* @param array $select
* The array holding the keys for nested data selection.
*
* @return $this
* The instance itself.
*/
public function selectingSubsetFromData(array $select) {
$this->render['#select'] = $select;
return $this;
}
/**
* Explicitly define partials that are being used by the scoped template.
*
* Explicitly defining partials before render execution, makes sure that
* the corresponding templates are available at client-side runtime. For
* server-side only rendering with Mustache.php, this step is not required,
* because registered templates can be dynamically loaded at server-side
* runtime. It can still be useful for server-side rendering when using inline
* templates, that are not available as standalone template files.
*
* Please note, that partials can (and should) be explicitly defined via
* hook_mustache_templates() within the 'default' values, if the scoped
* template is defined by a module. That way partials will always be included
* and cached, also for client-side rendering, and will not accidentally be
* omitted when the scoped template is being used.
*
* @param array $partials
* An array keyed by partial / template name or a simple sequence of
* template names. Values can be either the (same) name of the template that
* is registered by a module or available in a theme, or a render array that
* is adding the template as inline Javascript. For the latter case, a
* render array of type 'mustache_template_js_inline' can be used. You can
* also set the second parameter $inline_only to TRUE and use an inline
* template content as simple string value.
* @param bool $inline_only
* (Optional) Set to TRUE if you explicitly exclude registered templates,
* and use the given list of partials as inline templates only. Example:
* usingPartials(['my_inline_partial' => '{{foo}}'], TRUE)
*
* @return $this
* The instance itself.
*/
public function usingPartials(array $partials, $inline_only = FALSE) {
$this->render['#partials'] = [];
$this->render['#inline_partials'] = [];
foreach ($partials as $key => $value) {
if ($inline_only || is_array($value)) {
$this->render['#inline_partials'][$key] = $value;
}
else {
$this->render['#partials'][$key] = $value;
}
}
return $this;
}
/**
* Define a render array to use as a placeholder.
*
* @param array $placeholder
* An arbitrary render array of the placeholder.
*
* @return $this
* The instance itself.
*/
public function withPlaceholder(array $placeholder) {
$this->render['#placeholder'] = $placeholder;
return $this;
}
/**
* Enable token processing.
*
* @param array $data
* (Optional) An array of Token data, like Token::replace() would expect.
* The processing takes care of global context data and entities being
* viewed, so there is no need to set them here.
* @param array $options
* (Optional) An array of Token options, like Token::replace() would expect.
* If not set otherwise, empty tokens are being cleared by default.
*
* @return $this
* The instance itself.
*
* @see \Drupal\Core\Utility\Token
*/
public function withTokens(array $data = [], array $options = []) {
$this->render['#with_tokens'] = [
'data' => $data,
'options' => $options,
];
return $this;
}
/**
* Bind a form whose values should be included as data.
*
* The form values will be directly provided within the "form" key of the
* data that is being passed to the template rendering. For example, if you
* have an input field of name "search", then you can access that value in the
* template with {{form.search}}. When a url is set for fetching Json-encoded
* data (::usingDataFromUrl), then the form values will be added as query
* string parameters. This enables your Json endpoint to return any requested
* data for the given form input.
*
* It should be noted, when working with Drupal forms, mostly the form will
* have a redirect after form submission. When a redirect happens, then the
* form values are no longer available. The form redirect may be either
* disabled, or the relevant arguments may be attached to the redirect url,
* then extracted and (manually) passed to the data array.
*
* @param \Drupal\Core\Form\FormStateInterface|array $form_state_or_array
* The form state or an associative array with a 'values' key, which is an
* associative array of submitted form values. When using an array and
* client synchronization, optionally set a 'selector' key that will be used
* as the CSS selector to identify the current form element in the DOM, and
* to use whose current values during client synchronization. Example array:
* @code
* ['selector' => '#myformid', 'values' => ['myname' => 'myvalue']]
* // If you have client synchronization enabled, but no form is present
* // on that page, skip the 'selector' key:
* ['values' => ['myname' => 'myvalue']]
* @endcode
* If client synchronization is enabled and a selector is defined, the form
* will be automatically bound (when rendered) and the synchronization does
* require the form to be present in the DOM. If you do not have any
* server-side form state or values, and you solely want to use client
* synchronization, you can use SynchronizationOptions::usingFormValues()
* instead to set the CSS selector of the form element.
*
* @return $this
* The instance itself.
*/
public function usingFormValues($form_state_or_array) {
$this->render['#form'] = $form_state_or_array;
return $this;
}
/**
* Determine and configure client-side DOM content synchronization.
*
* @return \Drupal\mustache\Helpers\SynchronizationOptions
* The synchronization options to define.
*/
public function withClientSynchronization() {
return new SynchronizationOptions($this->render);
}
/**
* Whether server-side data may be exposed into the browser client.
*
* This may be handy for example if you want to access or compare with
* previously loaded data values.
*
* When this setting is enabled, the data that was retreived internally, or
* fetched from an external HTTP endpoint, will be bypassed to the browser
* client. That raw data would then be accessible and fully visible, thus
* only use this option if you know what you are doing.
*
* @param bool $expose
* (Optional) Whether exposure shall be enabled (TRUE) or not (FALSE).
* By default, bypassing of server-side data is not enabled.
*
* @return $this
* The instance itself.
*/
public function exposeData($expose = TRUE) {
$this->render['#expose'] = $expose;
return $this;
}
/**
* Get the render array result.
*
* @return array
* The render array result.
*/
public function &toRenderArray() {
return $this->render;
}
}
/**
* Class SynchronizationOptions.
*
* @internal
*/
final class SynchronizationOptions {
/**
* The render array representation of the parent instance.
*
* @var array
*/
private $render;
/**
* The synchronization item as array.
*
* @var array
*/
private $item;
/**
* SynchronizationOptions constructor.
*
* @param array &$render_array
* The corresponding render array representation.
*/
public function __construct(array &$render_array) {
$this->render = &$render_array;
if (!isset($render_array['#sync']['items'])) {
$render_array['#sync']['items'] = [];
}
$i = count($render_array['#sync']['items']);
$render_array['#sync']['items'][$i] = [
'period' => 0,
'delay' => 0,
];
$this->item = &$render_array['#sync']['items'][$i];
}
/**
* Define the url from where to receive Json-encoded data.
*
* @param \Drupal\Core\Url|string $url
* The url as object or string.
* @param array|null $select
* (Optional) The array holding the keys for nested data selection.
*
* @return $this
* The instance itself.
*/
public function usingDataFromUrl($url, array $select = NULL) {
if (!($url instanceof Url) && !is_string($url)) {
throw new MustacheException(t('The $url param must be a Url object or string.'));
}
if (is_string($url)) {
try {
$url = Url::fromUri($url);
}
catch (\InvalidArgumentException $e) {
$url = Url::fromUserInput($url);
}
}
$this->item['url'] = $url;
if (isset($select)) {
$this->selectingSubsetFromData($select);
}
return $this;
}
/**
* Specifies the subset to select out of the data.
*
* @param array $select
* The array holding the keys for nested data selection.
*
* @return $this
* The instance itself.
*/
public function selectingSubsetFromData(array $select) {
$this->item['select'] = $select;
return $this;
}
/**
* Determine the interval in milliseconds to delay synchronization.
*
* @param int $milliseconds
* The interval in milliseconds to delay the synchronization start.
*
* @return $this
* The instance itself.
*/
public function startsDelayed($milliseconds) {
$this->item['delay'] = $milliseconds;
return $this;
}
/**
* Determine the interval in milliseconds to repeat synchronization.
*
* @param int $milliseconds
* The interval in milliseconds to repeat synchronization.
*
* @return $this
* The instance itself.
*/
public function periodicallyRefreshesAt($milliseconds) {
$this->item['period'] = $milliseconds;
return $this;
}
/**
* Determine the maximum number of synchronizations.
*
* @param int $n
* The maximum number of times to run the synchronization.
*
* @return $this
* The instance itself.
*/
public function upToNTimes($n) {
$this->item['limit'] = $n;
return $this;
}
/**
* Determine that the synchronization is being run only once.
*
* @return $this
* The instance itself.
*/
public function once() {
return $this->upToNTimes(1);
}
/**
* Determine that the number of synchronizations is not limited.
*
* @return $this
* The instance itself.
*/
public function unlimited() {
return $this->upToNTimes(-1);
}
/**
* Determine and configure a triggering element (optional).
*
* @param string $css_selector
* The CSS element selector.
*
* @return \Drupal\mustache\Helpers\TriggeringElementOptions
* The configurable options for the triggering element.
*/
public function startsWhenElementWasTriggered($css_selector) {
return new TriggeringElementOptions($css_selector, $this->render, $this->item);
}
/**
* Determine the HTML tag to use (optional).
*
* By default, a div tag would wrap the rendered template.
*
* @param string $html_tag
* The HTML tag, e.g. span.
*
* @return $this
* The instance itself.
*/
public function withWrapperTag($html_tag) {
$this->render['#sync']['wrapper_tag'] = $html_tag;
return $this;
}
/**
* Determine to insert synchronized content into somewhere else.
*
* By default, synchronization happens by automatically wrapping the template
* contents with a div or another tag type specified with ::withWrapperTag().
* This option changes that behavior by inserting into another element.
*
* @param string $css_selector
* The CSS selector that identifies the insertion target. It should be made
* sure that this element actually exists in the DOM.
*
* @return $this
* The instance itself.
*/
public function insertsInto($css_selector) {
$this->render['#sync']['into'] = $css_selector;
return $this;
}
/**
* Set custom HTML attributes to the wrapper tag.
*
* @param array $attributes
* The HTML attributes to set.
*
* @return $this
* The instance itself.
*/
public function withWrapperAttributes(array $attributes) {
$this->render['#attributes'] = $attributes;
return $this;
}
/**
* Enables DOM tree transformation using morphdom.
*
* When using client side synchronization, by default the DOM content is being
* completely replaced. While replacing an existing DOM tree with an entirely
* new DOM tree is fast, all of the internal state associated with the
* existing DOM nodes will be lost. Instead of replacing the existing DOM tree
* with a new DOM tree you can use morphdom to to transform the existing DOM
* tree to match the new DOM tree, minimizing the number of changes to the
* existing DOM tree.
*
* Please note that you cannot use the "morph" setting together with the
* "adjacent" setting (::insertsAt()). When "adjacent" is set, the "morph"
* setting will be ignored.
*
* @param bool $morph
* Set to TRUE to enable morph DOM tree transformation.
*
* @return $this
* The instance itself.
*/
public function morphing($morph = TRUE) {
$this->item['morph'] = $morph;
return $this;
}
/**
* Set whether inner script content should be executed.
*
* Warning: Executing arbitrary scripts from untrusted sources
* might lead your site to be vulnerable for XSS and other attacks.
*
* When enabled, Drupal attach and detach behaviors will
* be executed too. In case you don't wish to include Drupal
* behaviors, you need to explicitly disable them via
* ::executesDrupalBehaviors(FALSE).
*
* @param bool $eval
* Set to TRUE to enable inner script execution.
* By default, inner script execution is not enabled.
*
* @return $this
* The instance itself.
*/
public function executesInnerScripts($eval = TRUE) {
$this->item['eval'] = $eval;
return $this;
}
/**
* Enable or disable Drupal's attach and detach behaviors.
*
* By default, Drupal behaviors are not enabled, unless
* script execution is not enabled.
*
* @param bool $behaviors
* Set to TRUE to enable behaviors, or FALSE
* to explicitly disable them, e.g. when eval is TRUE.
*
* @return $this
* The instance itself.
*/
public function executesDrupalBehaviors($behaviors = TRUE) {
$this->item['behaviors'] = $behaviors;
return $this;
}
/**
* Determine whether and how the rendered content should be inserted.
*
* By default, the inner HTML of the wrapping element's would be
* completely replaced by the synchronized content. When a position
* is specified though, it will be inserted accordingly.
*
* @param string $position
* Can be either 'beforebegin', 'afterbegin', 'beforeend'
* or 'afterend'. Have a look at the Javascript function
* Element.insertAdjacentHTML for more information about
* inserting HTML with the position parameter.
*
* @return $this
* The instance itself.
*/
public function insertsAt($position) {
$this->item['adjacent'] = $position;
return $this;
}
/**
* Enable automatic incrementing.
*
* @return \Drupal\mustache\Helpers\IncrementOptions
* The increment options.
*/
public function increments() {
return new IncrementOptions($this->render, $this->item);
}
/**
* Bind a form whose values should be included as data.
*
* The form values will be directly provided within the "form" key of the
* data that is being passed to the template rendering. For example, if you
* have an input field of name "search", then you can access that value in the
* template with {{form.search}}. When a url is set for fetching Json-encoded
* data (::usingDataFromUrl), then the form values will be added as query
* string parameters. This enables your Json endpoint to return any requested
* data for the given form input.
*
* @param string|array $selector_or_array
* The CSS selector that identifies the form element in the DOM. Only one
* form can be used, thus the first found form element that matches the
* given selector will be used. Alternatively you can specify an associative
* array whose keys are 'selector', 'el' and 'values' to prepopulate any
* known form values. Set the 'selector' key to NULL if there is no form
* present in the DOM. This might be useful when you have a page for
* submitted form values, but on that page the form itself is not present.
* Please note that the 'el' key must be present and set to NULL, as this
* key will be used for assigning the DOM element at client synchronization.
* Example array:
* @code
* ['selector' => NULL, 'el' => NULL, 'values' => ['myname' => 'myvalue']]
* @endcode
*
* @return $this
* The instance itself.
*/
public function usingFormValues($selector_or_array = 'form') {
$this->item['form'] = $selector_or_array;
return $this;
}
/**
* Bind values of an object that should be included as data.
*
* The values will be directly provided within the "object" key of the data
* that is being passed to the template rendering. When a URL is set for
* fetching Json-encoded data (::usingDataFromUrl), then the object values
* will be added as query string parameters.
*
* The following precedence will be taken into account:
* - If the data array contains data matching the given selection, that one
* will be used.
* - Otherwise, if a global object (namely a property of window) exists, that
* one will be used.
*
* @param array|string $select
* Either a string with dot notation or a nested array that holds a sequence
* of keys for data selection. Examples:
* @code
* 'drupalSettings.ajaxPageState.libraries'
* 'drupalSettings.ajaxPageState.libraries+another.one'
* [['drupalSettings', 'ajaxPageState', 'libraries']]
* [['drupalSettings', 'ajaxPageState', 'libraries'], ['another', 'one']]
* @endcode
*
* @return $this
* The instance itself.
*/
public function usingObjectValues($select) {
$selects = is_array($select) ? $select : explode('+', $select);
foreach ($selects as $i => $select) {
if (is_string($select)) {
$selects[$i] = explode('.', $select);
}
}
if ($selects) {
$this->item['object'] = $selects;
}
return $this;
}
/**
* Define a maximum allowed age of fetched data.
*
* Define a value in milliseconds greater than 0, so that previously
* fetched data can be reused instead of being fetched again from the Json
* endpoint. The size of the data cache is very limited though, so that
* redundant fetch calls might still occur when there are many variants of
* urls to be used.
*
* @param int $max_age
* The maximum allowed age of data in milliseconds. The value must be
* unsigned, i.e. it must be equal to or greater than 0.
*
* @return $this
* The instance itself.
*/
public function dataMayBeOfMaxAge($max_age) {
$this->item['max_age'] = $max_age;
return $this;
}
/**
* Get the render array result.
*
* @return array
* The render array result.
*/
public function &toRenderArray() {
return $this->render;
}
}
/**
* Class IncrementOptions.
*
* @internal
*/
final class IncrementOptions {
/**
* The render array representation of the parent instance.
*
* @var array
*/
protected $render;
/**
* The sync item as array representation.
*
* @var array
*/
protected $syncItem;
/**
* The increment options.
*
* @var array
*/
protected $increment;
/**
* IncrementOptions constructor.
*
* @param array &$render_array
* The corresponding render array representation.
* @param array &$sync_item
* The sync item as array representation.
*/
public function __construct(array &$render_array, array &$sync_item) {
$this->render = &$render_array;
$this->syncItem = &$sync_item;
if (!isset($sync_item['increment'])) {
$sync_item['increment'] = [];
}
$this->increment = &$sync_item['increment'];
}
/**
* Define the url parameter to increment.
*
* @param string $key
* The url parameter. Default parameter would be 'page'.
*
* @return $this
* The instance itself.
*/
public function atParamKey($key) {
$this->increment['key'] = $key;
return $this;
}
/**
* Define the step size to increment.
*
* Example: When set to 5, the parameter value
* is being incremented by +5.
*
* @param int $step
* The step size to set. Default step size is 1.
*
* @return $this
* The instance itself.
*/
public function withStepSize($step) {
$this->increment['step'] = $step;
return $this;
}
/**
* Define an offset from where to start incrementing.
*
* Example: When offset is set to 100 and step size is
* set to 1, the first increment would be 101.
*
* @param int $offset
* The offset value to set. The default offset value is 0.
*
* @return $this
* The instance itself.
*/
public function startingAt($offset) {
$this->increment['offset'] = $offset;
return $this;
}
/**
* Define the maximum number of increments.
*
* @param int $n
* The limit of increments. Default is unlimited (-1).
*
* @return $this
* The instance itself.
*/
public function upToNTimes($n) {
$this->increment['max'] = $n;
return $this;
}
/**
* Define that the number of increments is unlimited.
*
* @return $this
* The instance itself.
*/
public function unlimited() {
return $this->upToNTimes(-1);
}
/**
* Define that the increment is a loop.
*
* When enabled, the increment starts back at the offset, once
* it has reached its increment limit. It would also restart
* increments once it has received an empty or "not found" result
* from the specified url endpoint.
*
* @param bool $loops
* Set to TRUE to enable, or FALSE to turn off looping.
* Default is enabled.
*
* @return $this
* The instance itself.
*/
public function loops($loops = TRUE) {
$this->increment['loop'] = $loops;
return $this;
}
}
/**
* Class TriggeringElementOptions.
*
* @internal
*/
final class TriggeringElementOptions {
/**
* The CSS element selector.
*
* @var string
*/
protected $selector;
/**
* The render array representation of the parent instance.
*
* @var array
*/
protected $render;
/**
* The sync item as array representation.
*
* @var array
*/
protected $syncItem;
/**
* The part of the render array which holds the trigger options.
*
* @var array
*/
protected $trigger;
/**
* TriggeringElementOptions constructor.
*
* @param string $css_selector
* The CSS element selector.
* @param array &$render_array
* The corresponding render array representation.
* @param array &$sync_item
* The sync item as array representation.
*/
public function __construct($css_selector, array &$render_array, array &$sync_item) {
$this->render = &$render_array;
$this->syncItem = &$sync_item;
$this->selector = $css_selector;
if (!isset($sync_item['trigger'])) {
$sync_item['trigger'] = [];
}
$i = count($sync_item['trigger']);
$sync_item['trigger'][$i] = [$css_selector, 'load', 1];
$this->trigger = &$sync_item['trigger'][$i];
}
/**
* Determine the Javascript event which would trigger at the element.
*
* @param string $event
* The Javascript triggering event, e.g. 'click'.
*
* @return $this
* The instance itself.
*/
public function atEvent($event) {
$this->trigger[1] = $event;
return $this;
}
/**
* Determine how many times the synchronization is being triggered.
*
* @param int $n
* The maximum number of times to trigger the synchronization.
*
* @return $this
* The instance itself.
*/
public function upToNTimes($n) {
$this->trigger[2] = $n;
return $this;
}
/**
* Determine that the synchronization is being triggered only once.
*
* @return $this
* The instance itself.
*/
public function once() {
return $this->upToNTimes(1);
}
/**
* Determine that the synchronization is always being triggered.
*
* @return $this
* The instance itself.
*/
public function always() {
return $this->upToNTimes(-1);
}
/**
* Get the render array result.
*
* @return array
* The render array result.
*/
public function &toRenderArray() {
return $this->render;
}
}
