Source of file InlineLinkField.php
Size: 29,202 Bytes - Last Modified: 2021-12-23T10:07:22+00:00
/var/www/docs.ssmods.com/process/src/src/Forms/InlineLinkField.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886 | <?php namespace NSWDPC\InlineLinker; use DNADesign\Elemental\Models\BaseElement; use DNADesign\Elemental\Controllers\ElementalAreaController; use DNADesign\Elemental\Forms\EditFormFactory; use gorriecoe\Link\Models\Link; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\Controller; use SilverStripe\Forms\CompositeField; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FormField; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\OptionsetField; use SilverStripe\Forms\TextField; use SilverStripe\Forms\Tip; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\ValidationException; use SilverStripe\Security\SecurityToken; /** * Subclass for specific composite field handling, currently not in use * Could be useful for future configuration */ class InlineLinkField extends CompositeField { /** * @var gorriecoe\Link\Models\Link|null */ protected $record; /** * @var SilverStripe\ORM\DataObject|null */ protected $parent; /** * Whether the parent is an inline editable Elemental element * null = not detected yet * true = yes, it is * false = no * If true, handle field namespacing, prefixing and replacement of [] in fieldnames etc. * @var boolean|null */ protected $parent_inline_editable = null; /** * @var TextField * */ protected $title_field; /** * @var OptionsetField * */ protected $open_in_new_window_field; /** * @var InlineLink_RemoveAction * */ protected $remove_field; /** * @var SelectionGroup */ protected $selection_group; /** * @var bool */ protected $is_removing_link = false; const FIELD_NAME_TYPE_SEPARATOR = "___"; const FIELD_NAME_REMOVELINK = "RemoveLink"; const FIELD_NAME_TITLE = "Title"; const FIELD_NAME_OPEN_IN_NEW_WINDOW = "OpenInNewWindow"; const FIELD_NAME_TYPE = "Type"; const LINKTYPE_EMAIL = 'Email'; const LINKTYPE_URL = 'URL'; const LINKTYPE_SITETREE = 'SiteTree'; const LINKTYPE_PHONE = 'Phone'; const LINKTYPE_FILE = 'File'; const LINKTYPE_TYPEDEFINED = 'BasedOnType'; public function __construct($name, $title, DataObject $parent) { // push all child fields parent::__construct($this->collectChildFields($name, $title, $parent)); $this->setName($name); } /** * Collect all fields to be used in the CompositeField */ protected function collectChildFields($name, $title, DataObject $parent) : FieldList { // set name and title early $this->name = $name; $this->title = $title; // initialise record and parent $this->parent = $this->record = null; $component = $parent->getComponent($name); if(!($component instanceof Link)) { throw new \InvalidArgumentException(_t( "NSWDPC\\InlineLinker\\InlineLinkField.INVALID_COMPONENT", "Error: component {name} must be an instance of Link", [ 'name' => $this->name ] )); } // set a valid parent $this->parent = $parent; $this->setRecord($component); // determine if in the context of an inline editable Elemental element $inline_editable = $this->hasInlineElementalParent(); if($inline_editable) { $this->setLegend($title); $this->setTag('fieldset'); } else { $this->setTitle($title); $this->setTag('div'); } /** * If there is a current link, * render a header field and the template for the current link * .. and a remove checkbox * A link might exist without a Type, test for that */ $has = $this->hasCurrentLink(); $remove_action = null; if($has) { $remove_action = InlineLink_RemoveAction::create( $this->prefixedFieldName( self::FIELD_NAME_REMOVELINK ), _t( "NSWDPC\\InlineLinker\\InlineLinkField.DELETE_LINK", 'Delete this link' ) ); $this->setRemoveField( $remove_action ); } $link_title_field = InlineLink_TitleField::create( $this->prefixedFieldName( self::FIELD_NAME_TITLE ), _t( "NSWDPC\\InlineLinker\\InlineLinkField.LINK_TITLE", 'Title' ), $this->getRecordTitle() ); $link_openinnewwindow_field = InlineLink_OpenInNewWindowField::create( $this->prefixedFieldName( self::FIELD_NAME_OPEN_IN_NEW_WINDOW), _t( "NSWDPC\\InlineLinker\\InlineLinkField.LINK_OPEN_IN_NEW_WINDOW", 'Open in new tab / window' ), $this->getRecordOpenInNewWindow() ); $this->setTitleField( $link_title_field ); $this->setOpenInNewWindowField( $link_openinnewwindow_field ); $children = FieldList::create(); $children->push( // common fields $link_title_field, ); $children->push( // common fields $link_openinnewwindow_field ); // selection group $children->push( $this->getLinkFields() // link type field collection ); if($remove_action) { $children->push( $remove_action ); } return $children; } /** * This is called just prior to saveInto, set submitted values on child fields * to allow saveInto to update/create a link * * @param mixed $values - these will be all values in $this->getName() index * @param array|DataObject $data * @return $this */ public function setSubmittedValue($values, $data = null) { /** * Due to https://github.com/dnadesign/silverstripe-elemental/issues/381 * and https://github.com/silverstripe/silverstripe-admin/issues/639 * Must rescue data for child fields using namespaced removal method * If the parent is not an inline_editable element, the fields are named e.g "field[name]" * and this becomes easier */ if($inline = $this->hasInlineElementalParent()) { $controller = Controller::curr(); $request = $controller->getRequest(); $post = $request->requestVars(); // Check security token if (!SecurityToken::inst()->checkRequest($request)) { throw new ValidationException( "Failed security token check" ); } // De-namespace the posted values $values = []; $values_all = ElementalAreaController::removeNamespacesFromFields($post, $this->parent->ID); // mogrify them into something we expect if(is_array($values_all)) { // grab any {$this->name}__{index} values into an array foreach($values_all as $name => $value) { $index = $this->getIndexFromName($name); if($index) { $values[ $index ] = $value; } } } } if(!is_array($values)) { // cannot proceed unless we have some values throw new ValidationException(_t( "NSWDPC\\InlineLinker\\InlineLinkField.NO_VALUES_SUPPLIED_SAVE", "No values were supplied to save" )); } // clear data field values $fields = $this->children->dataFields(); foreach($fields as $field) { $field->setSubmittedValue( null ); } // Ensure the checkbox value defaults to 0 $this->getOpenInNewWindowField()->setSubmittedValue(0); // set submitted values foreach($values as $index => $value) { if($index == self::FIELD_NAME_TITLE) { // handle title field $this->getTitleField()->setSubmittedValue( $value ); } else if($index == self::FIELD_NAME_OPEN_IN_NEW_WINDOW) { // handle open in new window field $this->getOpenInNewWindowField()->setSubmittedValue( $value ); } else if($field = $this->children->dataFieldByName( $this->prefixedFieldName( $index ) )) { // set the submitted value on the relevant field $field->setSubmittedValue( $value ); } } } /** * @param InlineLink_TitleField */ public function setTitleField(InlineLink_TitleField $field) { $this->title_field = $field; return $this; } /** * @return InlineLink_TitleField */ public function getTitleField() { return $this->title_field; } /** * @param InlineLink_OpenInNewWindowField */ public function setOpenInNewWindowField(InlineLink_OpenInNewWindowField $field) { $this->open_in_new_window_field = $field; return $this; } /** * @return InlineLink_OpenInNewWindowField */ public function getOpenInNewWindowField() { return $this->open_in_new_window_field; } /** * @param InlineLink_RemoveAction */ public function setRemoveField(InlineLink_RemoveAction $field) { $this->remove_field = $field; return $this; } /** * @return InlineLink_RemoveAction */ public function getRemoveField() { return $this->remove_field; } /** * @return SelectionGroup */ public function getLinkTypeFields() { return $this->selection_group; } /** * This field handles data */ public function hasData() { return true; } public function canSubmitValue() : bool { return true; } /** * This field handles all the saving */ public function collateDataFields(&$list, $saveableOnly = false) { return; } /** * {@inheritdoc} */ public function saveInto(DataObjectInterface $record) { // handle removal $remove_field = $this->getRemoveField(); if($remove_field && ($remove_field->dataValue() == 1) && ($link = $this->getRecord()) ) { if($link && $link->exists()) { // clear all field submitted // avoids re-display with data foreach($this->children->dataFields() as $field) { $field->setSubmittedValue(null); } $link->delete(); // do not proceed return; } } // @var FormField $type_field = $this->children->dataFieldByName( $this->prefixedFieldName( self::FIELD_NAME_TYPE ) ); // no type field if(!$type_field) { throw new ValidationException(_t( "NSWDPC\\InlineLinker\\InlineLinkField.NO_LINK_TYPE_FIELD_ERROR", "The link type could not be determined or is unknown" )); } $type = $type_field->dataValue(); // @var string eg Email if(!$type) { // if there is no type value provided, no link can be created return; } // grab the value field based on the Type selected $value_field = $this->children->dataFieldByName( $this->prefixedFieldName( $type ) ); if(!$value_field) { // maybe 'BasedOnType' multi link field value $value_field = $this->children->dataFieldByName( $this->prefixedFieldName( self::LINKTYPE_TYPEDEFINED) ); } if(!$value_field) { throw new ValidationException(_t( "NSWDPC\\InlineLinker\\InlineLinkField.NO_LINK_VALUE_ERROR", "A value for the link could not be found" )); } //set model options $open_in_new_window = 0; $title =_t( "NSWDPC\\InlineLinker\\InlineLinkField.AUTO_TITLE", "Auto-created title for a link in " . $this->parent->getTitle() ); if($open_in_new_window_field = $this->getOpenInNewWindowField()) { $open_in_new_window = $open_in_new_window_field->dataValue(); } if($title_field = $this->getTitleField()) { $title = $title_field->dataValue(); } if($type) { // apply the value found $link = $this->createOrAssociateLink($type, $value_field); // save Title and OpenInNewWindow $link->Title = $title; $link->OpenInNewWindow = $open_in_new_window; $link->write(); // the link becomes the record $this->setRecord($link); // save the link id to the parent element that has the relation to the link $this->parent->setField($this->getName() . "ID", $link->ID); } } /** * Create or save a link using the value from the form field * @param string $type eg. 'Email' * @param mixed $value eg. 'bob@example.com' * @param FormField $field the Form field holding the data related to the type * @return Link */ protected function createOrAssociateLink(string $type, FormField $field) : Link { $value = $field->dataValue(); // defaults $base = [ 'URL' => null, 'FileID' => null, 'Email' => null, 'Phone' => null, 'SiteTreeID' => null, ]; switch($type) { case self::LINKTYPE_SITETREE: $data = [ 'SiteTreeID' => $value, 'Type' => $type ]; break; case self::LINKTYPE_FILE: // for files, getItemIDs $id_list = $field->getItemIDs(); $file_id = 0;//TODO error? if(is_array($id_list)) { $file_id = reset($id_list); } $data = [ 'FileID' => $file_id, 'Type' => $type ]; break; case self::LINKTYPE_URL: $data = [ 'URL' => $value, 'Type' => $type ]; break; case self::LINKTYPE_EMAIL: $data = [ 'Email' => $value, 'Type' => $type ]; break; case self::LINKTYPE_PHONE: $data = [ 'Phone' => $value, 'Type' => $type ]; break; default: throw new ValidationException(_t( "NSWDPC\\InlineLinker\\InlineLinkField.UNHANDLED_LINK_TYPE_ERROR", "The link of the type '{type}' cannot be saved'", [ 'type' => $type ] )); break; } // apply data over defaults $data = array_merge($base, $data); $link = $this->getRecord(); if($link instanceof Link) { // update the existing link foreach($data as $field => $value) { $link->setField($field, $value); } } else { // new, create a new link $link = Link::create($data); } return $link; } /** * Set the current link record */ public function setRecord(Link $record) { $this->record = $record; } /** * Get the current link record, if any * @return mixed null|\gorriecoe\Link\Models\Link */ public function getRecord() { return $this->record; } /** * Returns whether the type passed in as the current Link.Type * @return bool */ protected function isTypeCurrent($type) : bool { return !empty($this->record->Type) && $this->record->Type == $type; } /** * Determine whether the parent of this field is an elemental element * @return boolean */ public function hasInlineElementalParent() { if(!is_null($this->parent_inline_editable)) { // already detected return $this->parent_inline_editable; } else if(!class_exists("\\DNADesign\\Elemental\\Models\\BaseElement")) { // If there is no silverstripe-elemental module installed, then no need to check... $this->parent_inline_editable = false; return $this->parent_inline_editable; } else { $this->parent_inline_editable = $this->parent && ($this->parent instanceof BaseElement) && $this->parent->config()->get('inline_editable'); return $this->parent_inline_editable; } } /** * Return a prefixed field name, eg. LinkTarget[Email] * @param string $index eg. Email, Type, OpenInNewWindow... * @return string */ public function prefixedFieldName($index) { if($this->hasInlineElementalParent()) { /* * Cannot use index notation due to * https://github.com/dnadesign/silverstripe-elemental/issues/381 * https://github.com/silverstripe/silverstripe-admin/issues/639 */ return $this->getName() . self::FIELD_NAME_TYPE_SEPARATOR . $index; } else { /** * Can use indexed notation - either a non inline editable element * or a normal dataobject edit form */ return $this->getName() . "[{$index}]"; } } /** * Work out the index based on the field name * If the parent is an inline editable element, take that into account * @return string */ protected function getIndexFromName($complete_field_name) { $type = ""; if($this->hasInlineElementalParent()) { // the field name should start with the prefix... if(strpos($complete_field_name, $this->getName() . self::FIELD_NAME_TYPE_SEPARATOR) !== 0) { // invalid field name return ""; } // Get type using separator $parts = explode(self::FIELD_NAME_TYPE_SEPARATOR, $complete_field_name); // 0=parentname 1=type eg. LinkTarget__Index $index = isset($parts[1]) ? $parts[1] : ''; return $index; } else { // Non inline_editable elements or standard modeladmin, using field[index] naming $result = []; $name = $this->getName(); parse_str($complete_field_name, $results); if(isset($results[ $name ])) { $target = $results[ $name ]; $index = key($target); } return $index; } } /** * Return the title of the current record * @return string */ public function getRecordTitle() { $record = $this->getRecord(); $title = trim(isset($record->Title) ? $record->Title : ''); return $title; } /** * Return the OpenInNewWindow value of the current record * @return int */ public function getRecordOpenInNewWindow() { $record = $this->getRecord(); $value = 0; if($record && $record->isInDB()) { $value = ($record->OpenInNewWindow == 1 ? 1: 0); } return $value; } /** * @return LiteralField * @deprecated */ public function CurrentLink() { return $this->getCurrentLinkField(); } /** * @return mixed null|LiteralField */ public function getCurrentLinkField() { $field = $this->getCurrentLinkTemplate(); return $field; } /** * Returns whether a current link exists and it is valid * A valid Link record has a Type value and a URL * @return bool */ public function hasCurrentLink() : bool { return $this->record && $this->record->exists() && $this->record->Type && $this->record->getLinkURL(); } /** * Return a literal field template for the current link * @return mixed null|LiteralField */ protected function getCurrentLinkTemplate($name = "ExistingLinkRecord") { $field = null; if($this->hasCurrentLink()) { $html = $this->record->renderWith('NSWDPC/InlineLinker/CurrentLinkTemplate'); $field = LiteralField::create( $this->prefixedFieldName($name), $html ); } return $field; } /** * Return all available Link Fields * Modify fields via the updateLinkFields extension method * @return CompositeField */ public function getLinkFields() : CompositeField { $record = $this->getRecord(); $type = ''; $file_list = null; $value = ''; if($record && $record->exists()) { $type = $record->Type; // file storage $file_list = ArrayList::create(); $file_list->push( $record->File() ); // Retrieve the link value, based on the record type switch($record->Type) { case self::LINKTYPE_URL: $value = $record->URL; break; case self::LINKTYPE_EMAIL: $value = $record->Email; break; case self::LINKTYPE_PHONE: $value = $record->Phone; break; default: $value = ''; break; } } $fields = CompositeField::create( DropdownField::create( $this->prefixedFieldName(self::FIELD_NAME_TYPE), _t( "NSWDPC\\InlineLinker\\InlineLinkField.THE_LINK_TYPE", "Choose a link type" ), [ self::LINKTYPE_SITETREE => _t("NSWDPC\\InlineLinker\\InlineLinkField.PAGE_TYPE", 'A page on this website'), self::LINKTYPE_URL => _t("NSWDPC\\InlineLinker\\InlineLinkField.URL_TYPE", 'An external URL (including optional #anchor)'), self::LINKTYPE_EMAIL => _t("NSWDPC\\InlineLinker\\InlineLinkField.EMAIL_TYPE", 'An email address'), self::LINKTYPE_PHONE => _t("NSWDPC\\InlineLinker\\InlineLinkField.PHONE_TYPE", 'A phone number'), self::LINKTYPE_FILE => _t("NSWDPC\\InlineLinker\\InlineLinkField.FILE_TYPE", 'A file on this website') ], $type )->setDescription( _t( "NSWDPC\\InlineLinker\\InlineLinkField.THE_LINK_TYPE_DESCRIPTION", "Leave empty for no link" ), )->setValue($type) ->setEmptyString(''),// default to no link InlineLink_URLField::create( $this->prefixedFieldName(self::LINKTYPE_URL), _t( "NSWDPC\\InlineLinker\\InlineLinkField.ENTER_WEBSITE_URL", 'Enter a website URL' ), $record->URL ?: '' )->setTip( new Tip( _t( "NSWDPC\\InlineLinker\\InlineLinkField.ENTER_WEBSITE_URL_RIGHTNOTE", 'Website links should begin with https:// or http://' ) ) )->setDescription( _t( "NSWDPC\\InlineLinker\\InlineLinkField.ENTER_WEBSITE_URL_DESCRIPTION", 'Website links should begin with https:// or http://' ) )->setSignals([ [ 'containerSelector' => '.form-group', 'triggerElement' => $this->getTriggerElement(self::FIELD_NAME_TYPE), 'value' => [ self::LINKTYPE_URL ] ] ]), InlineLink_EmailField::create( $this->prefixedFieldName(self::LINKTYPE_EMAIL), _t( "NSWDPC\\InlineLinker\\InlineLinkField.ENTER_AN_EMAIL_ADDRESS", 'Enter an e-mail address' ), $record->Email ?: '' )->setSignals([ [ 'containerSelector' => '.form-group', 'triggerElement' => $this->getTriggerElement(self::FIELD_NAME_TYPE), 'value' => [ self::LINKTYPE_EMAIL ] ] ]), InlineLink_PhoneField::create( $this->prefixedFieldName(self::LINKTYPE_PHONE), _t( "NSWDPC\\InlineLinker\\InlineLinkField.ENTER_AN_PHONE_NUMBER", 'Enter a phone number' ), $record->Phone ?: '' )->setTip( new Tip( _t( "NSWDPC\\InlineLinker\\InlineLinkField.ENTER_AN_PHONE_NUMBER_RIGHTNOTE", 'Phone numbers should start with the country dialling code' ) ) )->setDescription( _t( "NSWDPC\\InlineLinker\\InlineLinkField.ENTER_AN_PHONE_NUMBER_DESCRIPTION", 'Phone numbers should start with the country dialling code, example +61 499 999 999' ) )->setSignals([ [ 'containerSelector' => '.form-group', 'triggerElement' => $this->getTriggerElement(self::FIELD_NAME_TYPE), 'value' => [ self::LINKTYPE_PHONE ] ] ]), CompositeField::create( InlineLink_SiteTreeField::create( $this->prefixedFieldName(self::LINKTYPE_SITETREE), _t( "NSWDPC\\InlineLinker\\InlineLinkField.CHOOSE_PAGE_ON_THIS_WEBSITE", 'Choose a page on this website or type to start searching' ), SiteTree::class )->setValue( $record->SiteTreeID ?: null ), // ensure we have a signal field SignallerField::create( "signaller_for_" . $this->prefixedFieldName(self::LINKTYPE_SITETREE) )->setSignals([ [ 'containerSelector' => '.composite.form-group--no-label', 'triggerElement' => $this->getTriggerElement(self::FIELD_NAME_TYPE), 'value' => [ self::LINKTYPE_SITETREE ] ] ]) ), CompositeField::create( InlineLink_FileField::create( $this->prefixedFieldName(self::LINKTYPE_FILE), _t( "NSWDPC\\InlineLinker\\InlineLinkField.CHOOSE_A_FILE", 'Upload to or choose a file on this website' ), $file_list ), SignallerField::create( "signaller_for_" . $this->prefixedFieldName(self::LINKTYPE_FILE) )->setSignals([ [ 'containerSelector' => '.composite.form-group--no-label', 'triggerElement' => $this->getTriggerElement(self::FIELD_NAME_TYPE), 'value' => [ self::LINKTYPE_FILE ] ] ]) ) ); $this->extend('updateLinkFields', $fields); return $fields; } /** * Return the element name that will trigger the signals on change * The trigger element name must be namespaced */ protected function getTriggerElement($name) : string { if($inline = $this->hasInlineElementalParent()) { return sprintf(EditFormFactory::FIELD_NAMESPACE_TEMPLATE, $this->parent->ID, $this->prefixedFieldName($name)); } else { return $this->prefixedFieldName($name); } } /** * Returns a readonly version of this field * @return InlineLinkField_Readonly */ public function performReadonlyTransformation() { $title = trim( $this->title ? $this->title : $this->getLegend() ); if(!$title) { $title = null; } $field = new InlineLinkField_Readonly($this->name, $title); $field->setRecord( $this->getRecord() ); $field->setForm($this->form); return $field; } } |