'/model/{MODEL}/{ID}',
'link_template_data' => NULL, // callable f($model,$id,$row)
'link_template_enclose' => array('{', '}'),
'new_tab' => False,
'model' => NULL, // missing in metadata, needed for callbacks
'detail_available' => false, // true|false|callable f($model,$row){ return true|false; }
'download_url' => false, // false|callable f($model_name, $id){ return string|null; }
'format_input' => [], // format these fields as inputs
'format_input_prefix' => '', // prepended to input name attribute ie 'Model.'
'format_input_postfix' => '', // appended to input name attribute ie '[]'
'allowed_tags'=> '
',
];
/*
* This maps the meta $type to a render method and a display class
* 'class' provides the styles/icons for progress, links, phone etc.
*/
protected $LUT = [
'varchar' => ['format' => 'varchar', 'class' => 'text'],
'name' => ['format' => 'name', 'class' => 'link'], // handled by ref indirectly
'phone' => ['format' => 'varchar', 'class' => 'phone'],
'fax' => ['format' => 'varchar', 'class' => 'phone'],
'url' => ['format' => 'url', 'class' => 'link'],
'int' => ['format' => 'varchar', 'class' => 'number'],
'number' => ['format' => 'varchar', 'class' => 'number'],
'decimal' => ['format' => 'varchar', 'class' => 'number'],
'date' => ['format' => 'date', 'class' => 'date'],
'datetime' => ['format' => 'datetime', 'class' => 'datetime'],
'bool' => ['format' => 'bool', 'class' => 'bool'],
'currency' => ['format' => 'currency', 'class' => 'currency'],
'base_currency' => ['format' => 'currency', 'class' => 'currency'],
'raw_currency' => ['format' => 'currency', 'class' => 'currency'],
'double' => ['format' => 'float', 'class' => 'float'],
'text' => ['format' => 'text', 'class' => 'text'],
'email' => ['format' => 'email', 'class' => 'mailto'],
'progress' => ['format' => 'percent', 'class' => 'progress'],
'percent' => ['format' => 'percent', 'class' => 'percent'],
'enum' => ['format' => 'enum', 'class' => 'enum'],
'status' => ['format' => 'status', 'class' => 'status'],
'multienum' => ['format' => 'multienum', 'class' => 'enum'],
'ref' => ['format' => 'ref', 'class' => 'link'],
'html' => ['format' => 'html', 'class' => 'html'],
// for type id, render the 'for_ref' instead:
'id' => ['format' => 'id', 'class' => 'link'],
// for these, render as varchar for now:
'sequence_number' => ['format' => 'varchar', 'class' => 'number'],
'item_number' => ['format' => 'varchar', 'class' => 'number'],
'age' => ['format' => 'datetime', 'class' => 'datetime'],
'status_color' => ['format' => 'varchar', 'class' => 'text'],
'reminder' => ['format' => 'enum', 'class' => 'enum'],
'file_ref' => ['format' => 'file_ref', 'class' => 'text'],
// TODO: unhandled types mapped to varchar:
'image' => ['format' => 'varchar', 'class' => 'image'],
'star_rating' => ['format' => 'varchar', 'class' => 'text'],
'duration' => ['format' => 'varchar', 'class' => 'text'],
'formula' => ['format' => 'varchar', 'class' => 'text'],
'_custom' => ['format' => '_custom', 'class' => 'link'],
];
/*
* Constructor
*
* @param array $options [ link_template, link_template_data ]
* options include these and possible more:
* link_template, string, ie. "/model/[MODEL]/[ID]"
* link_template_data, callable(field,model,meta) returns array of variables
* new_tab, boolean, if true link will have target=”_blank”
* detail_available, callable(model), if true ref is a link otherwise plaintext.
*/
public function __construct($options) {
$this->field_options = (array) $options + $this->field_options;
}
/*
* renders a field from a model to HTML
*
* @param $field name from the model to render as HTML
* @param $model row returned from 1CRM API
* @param $meta metadata returned from 1CRM API
*/
public function render_field($field, $model, $meta) {
if (!isset($meta["fields"][$field])) return NULL;
$type = @$meta["fields"][$field]["type"];
if(($field == 'date_due' || $field == 'date_start') && $type == 'datetime') {
$type = 'date';
}
if (!isset($this->LUT[$type])) $type = "varchar";
$format = $this->LUT[$type]["format"];
$class = $this->LUT[$type]["class"];
if (isset($meta["fields"][$field]["class"])) $class.=' '.$meta["fields"][$field]["class"];
return call_user_func_array([$this, "render_field_$format"], [$field, $model, $meta, $class]);
}
/* used to create unique IDs to map labels to inputs */
private static final function input_uid(){
static $unique=0;
return 'u'.($unique++);
}
/*
* The model field related rendering methods:
*
* @param $field: the field name/index
* @param $model: hash of field names to values
* @param $meta: model metadata
* @param $class: classname for the top-level tag
*
* returns HTML markup
*/
protected function render_field_varchar($field, $model, $meta, $class) {
$value = htmlspecialchars($model[$field]);
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
return "$value";
}
}
protected function render_field_file_ref($field, $model, $meta, $class) {
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
$value = htmlspecialchars(basename($model[$field]));
if (!empty($this->field_options["model"])) {
if (is_callable($this->field_options['download_url'])) {
$url = call_user_func($this->field_options['download_url'], $this->field_options["model"], $model['id']);
return "$value";
}
}
return "$value";
}
}
protected function render_field__custom($field, $model, $meta, $class) {
if (empty($this->field_options['custom_render']) || !is_callable($this->field_options['custom_render'])) return '';
$render = $this->field_options['custom_render'];
return $render($field, $model, $meta);
}
protected function render_field_url($field, $model, $meta, $class) {
$text = $link = htmlspecialchars($model[$field]);
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else
switch (substr($link, -4)) {
case ".png":
case ".jpg":
case ".gif":
$text = '';
$class = 'url-image';
DEFAULT: break;
}
return "$text";
}
protected function render_field_id($field, $model, $meta, $class) {
if (@$for_ref = $meta["fields"][$field]["for_ref"]) {
// pass off to type ref if possible:
return $this->render_field_ref($for_ref, $model, $meta, $class);
} else {
// otherwise don't show it:
return false;
}
}
protected function render_field_name($field, $model, $meta, $class) {
if ($this->check_format_input($field,$meta)){
$value = htmlspecialchars($model[$field]);
$name = $this->create_input_name($field);
return "";
} else {
// make the "name" type render as its ref link:
if (!@$this->field_options["model"])
return $this->render_field_varchar($field, $model, $meta, $class);
$meta["fields"][$field]+=["bean_name" => $this->field_options["model"]];
$model["name_id"] = $model["id"];
// pass off type name to type ref:
return $this->render_field_ref($field, $model, $meta, $class);
}
}
protected function render_field_ref($field, $model, $meta, $class) {
$value = htmlspecialchars($model[$field]);
if (@$meta["fields"][$field]["detail_link"] && (
$this->field_options['detail_available']===true || (
is_callable($this->field_options["detail_available"]) &&
call_user_func($this->field_options["detail_available"], @$meta["fields"][$field]["bean_name"],$model))))
{
for (;;) {
$data = ["MODEL" => @$meta["fields"][$field]["bean_name"]];
// try to guess it:
if (isset($model[$field . "_id"])) {
$data["ID"] = $model[$field . "_id"];
} else {
// brute-force lookup:
foreach (array_keys($meta["fields"]) as $key) {
if (@$meta["fields"][$key]["for_ref"] === $field) {
$data["ID"] = $model[$key];
break;
}
}
}
if (!empty($this->field_options["model"]) && !empty($this->field_options["id"])) {
if ($this->field_options["model"] == $data['MODEL'] && $this->field_options["id"] == $data['ID'])
break;
}
if (is_callable(@$this->field_options["link_template_data"])) {
$data = call_user_func($this->field_options["link_template_data"], @$data["MODEL"], @$data["ID"],$model) + $data;
}
$URL = $this->render_link_template($this->field_options["link_template"], $data);
return "$value";
}
}
return "$value";
}
protected function render_field_email($field, $model, $meta, $class) {
$value = htmlspecialchars($model[$field]);
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
return "$value";
}
}
protected function render_field_date($field, $model, $meta, $class) {
$value = Locale::format_date($model[$field],'UTC');
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
return "$value";
}
}
protected function render_field_datetime($field, $model, $meta, $class) {
$value = Locale::format_datetime($model[$field], ' ', "UTC");
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
return "$value";
}
}
protected function render_field_bool($field, $model, $meta, $class) {
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
$checked=$model[$field]?'checked=checked':'';
return "".
"";
} else {
$label_h = $model[$field] ? "✔" : "✘";
return "$label_h";
}
}
protected function render_field_currency($field, $model, $meta, $class) {
$value = Locale::format_currency($model[$field]);
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
return "$value";
}
}
protected function render_field_float($field, $model, $meta, $class) {
$value = Locale::format_number($model[$field]);
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
return "$value";
}
}
protected function render_field_text($field, $model, $meta, $class) {
$value = htmlspecialchars($model[$field]);
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
// TODO: confirm replacing linefeeds vs using CSS white-space
// replace linefeeds with breaks:
$value = str_replace(["\n\r", "\r\n", "\r", "\n"], "
", $value);
return "$value";
}
}
protected function render_field_percent($field, $model, $meta, $class) {
// TODO: make the progress 0..limit from meta instead of 0..100%
if (is_double($model[$field])) {
$value = Locale::format_number($model[$field]);
} else {
$value = $model[$field];
}
if ($this->check_format_input($field,$meta)){
$name = $this->create_input_name($field);
return "";
} else {
// the nested divs are for the progress bar, the span for the numerical report
return "