diff --git a/README.md b/README.md index 14d8b40..4ae08e9 100644 --- a/README.md +++ b/README.md @@ -1 +1,53 @@ # silverstripe-superlinker + +Use master/v2.x (compatible with SS 4 & 5). + +This branch is under active development. It **will** change and break, likely including namespaces. + +## CMS fields testing snippets + +```php +// for $has_one relation, testing inline fields +$linkFields = SuperLink::singleton()->getCMSLinkFields('SuperLink' . HasOneEdit::FIELD_SEPARATOR); +$fields->addFieldsToTab('Root.Main', $linkFields->toArray()); + +// for $has_one relation, testing with edit form +$fields->addFieldsToTab('Root.Main', [ + HasOneMiniGridField::create( + 'SuperLink', + 'SuperLink', + $this + ) +]); + +// for $has_many relation, testing with gridfield +$linksField = MiniGridField::create( + 'SuperLinks', + 'Links', + $this +)->setLimit(7)->setShowLimitMessage(true); +$fields->addFieldToTab('Root.Main', $linksField); + +// for the HasOne/MiniGridFields, currently adding these lines provides nicer UI +$config = $linksField->getGridConfig()?->addComponent(new GridField_ActionMenu()); +$linksField->setGridConfig($config); +``` + +## v3 to-dos +- Validations for each link type +- Richer summary fields content +- Update MiniGridField to use GridField_ActionMenu +- Remove yml config currently in place for ease of development (convert to yml.example/readme or similar) +- `DependentDropdownField`/`DependentGroupedDropdownField` no longer detect changes from `TreeDropdownField`, so the Anchors dropdown for `SiteTreeLink` is no longer working. +- Modal for adding rather than `HasOneMiniGridField` +- Resolve indecision around handling, naming and accessors for Title vs LinkText +- Broken or empty link reporting +- Proper i18n/_t()/translations +- Permissions +- Add awareness of link container objects for orphan reporting/pruning (& potentially expanding config to container/relation) +- Migration script from v2 to v3 +- Documentation/readme +- Formats/themes/styles as optional extensions +- Cleverer handling of settings/options +- Can we integrate this with a new TinyMCE plugin/button or existing ss_link? +- Apply display logic (and perhaps field sort) via yml config using linktypes x fieldnames (allowing link types to share fields rather than requiring each class to utilise its own fields) diff --git a/_config/config.yml b/_config/config.yml index 86dc930..63c856b 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -1,3 +1,63 @@ --- Name: fromholdio-superlinker --- + +Fromholdio\SuperLinker\Model\SuperLink: + extensions: + - 'Fromholdio\SuperLinker\Extensions\EmailLink' + - 'Fromholdio\SuperLinker\Extensions\ExternalLink' + - 'Fromholdio\SuperLinker\Extensions\FileLink' + - 'Fromholdio\SuperLinker\Extensions\GlobalAnchorLink' + - 'Fromholdio\SuperLinker\Extensions\NullLink' + - 'Fromholdio\SuperLinker\Extensions\PhoneLink' + - 'Fromholdio\SuperLinker\Extensions\SiteTreeLink' + - 'Fromholdio\SuperLinker\Extensions\SystemLink' + disallowed_types: + - nolink + types: + email: + sort: 2 + external: + sort: 3 + phone: + sort: 4 + nolink: + sort: 5 + globalanchor: + sort: 7 + system: + sort: 6 + sitetree: + sort: 0 + file: + sort: 1 + +Fromholdio\SuperLinker\Model\VersionedSuperLink: + extensions: + - 'Fromholdio\SuperLinker\Extensions\EmailLink' + - 'Fromholdio\SuperLinker\Extensions\ExternalLink' + - 'Fromholdio\SuperLinker\Extensions\FileLink' + - 'Fromholdio\SuperLinker\Extensions\GlobalAnchorLink' + - 'Fromholdio\SuperLinker\Extensions\NullLink' + - 'Fromholdio\SuperLinker\Extensions\PhoneLink' + - 'Fromholdio\SuperLinker\Extensions\SiteTreeLink' + - 'Fromholdio\SuperLinker\Extensions\SystemLink' + disallowed_types: + - nolink + types: + email: + sort: 2 + external: + sort: 3 + phone: + sort: 4 + nolink: + sort: 5 + globalanchor: + sort: 7 + system: + sort: 6 + sitetree: + sort: 0 + file: + sort: 1 diff --git a/composer.json b/composer.json index 42d8d5c..04f186c 100644 --- a/composer.json +++ b/composer.json @@ -13,10 +13,10 @@ "homepage": "https://fromhold.io" }], "require": { - "silverstripe/cms": "~4.0 || ~5.0", - "fromholdio/silverstripe-dependentgroupeddropdownfield": "^1.0.0 || ^2.0.0", - "fromholdio/silverstripe-externalurlfield": "^1.0.3", - "giggsey/libphonenumber-for-php": "^8.12.55" + "silverstripe/cms": "~5.0", + "fromholdio/silverstripe-dependentgroupeddropdownfield": "^2.0.0", + "fromholdio/silverstripe-externalurlfield": "^1.1.0", + "innoweb/silverstripe-international-phone-number-field": "^5.0.0" }, "suggest": { "fromholdio/silverstripe-dbhtmlanchors": "", diff --git a/lang/de.yml b/lang/de.yml index dc832ac..a13c722 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -2,42 +2,46 @@ de: Fromholdio\SuperLinker\Extensions\EmailLink: Body: Inhalt Email: Email - EmailBCC: BCC - EmailCC: CC - MustProvideEmail: 'Sie müssen eine E-Mail-Adresse angeben' Subject: Betreff - Fromholdio\SuperLinker\Extensions\ExternalURLLink: - ExternalURLMustBeComplete: 'Externe URLs müssen vollständig sein, einschließlich http:// oder https://' - URL: URL + Fromholdio\SuperLinker\Extensions\ExternalLink: + ExternalURL: URL Fromholdio\SuperLinker\Extensions\FileLink: + Behaviour: Verhalten + DisplayInBrowser: 'Datei im Browserfenster anzeigen (wenn möglich)' + Download: "Datei direkt auf das Gerät des Benutzers herunterladen" File: Datei - FileRequired: 'Sie müssen eine Datei hochladen oder auswählen, auf die Sie verlinken möchten' Fromholdio\SuperLinker\Extensions\GlobalAnchorLink: - Anchor: Anker - AnchorRequired: 'Sie müssen einen Anker auswählen' - Fromholdio\SuperLinker\Extensions\NoLink: - NoLink: '- Kein Link -' - TitleRequired: 'Sie müssen einen Titel angeben' + GlobalAnchor: 'Globaler Anker' Fromholdio\SuperLinker\Extensions\PhoneLink: - Phone: Telefonnummer - PhoneRequired: 'Sie müssen eine Telefonnummer angeben' + PhoneNumber: 'Telefonnummer' Fromholdio\SuperLinker\Extensions\SiteTreeLink: - Anchor: Anker - Page: Seite - PageRequired: 'Sie müssen eine Seite auswählen, auf die Sie verlinken möchten' + PageAnchorOptional: 'Seitenanker (optional)' + PageOnThisWebsite: 'Seite auf dieser Webseite' SelectAPage: 'Seite auswählen' - SelectAnchor: 'Anker auswählen (optional)' + SelectAnAnchor: 'Anker auswählen' + Fromholdio\SuperLinker\Extensions\SuperLinkIconExtension: + LinkIcon: Symbol Fromholdio\SuperLinker\Extensions\SystemLink: SystemLink: 'System-Link' - SystemLinkRequired: 'Sie müssen einen Systemlink auswählen' Fromholdio\SuperLinker\Model\SuperLink: - CustomLinkText: 'Link Text' - DoNoFollow: 'Weisen Sie Suchmaschinen an, diesem Link nicht zu folgen' - DoOpenNewWindow: 'Link in neuem Fenster öffnen' - NewWindow: 'Neues Fenster' - OptionalWillBeGenerated: 'Optional. Wird automatisch generiert, wenn es leer gelassen wird.' - TabBehaviour: Verhalten - TabTarget: Ziel - URL: URL - URLInvalid: 'Sie müssen eine gültige URL angeben' - URLRequired: 'Sie müssen eine URL angeben' + PLURALNAME: Links + PLURALS: + one: 'Ein Link' + other: '{count} Links' + SINGULARNAME: Link + Fromholdio\SuperLinker\Model\SuperLinkTrait: + DoNoFollow: 'Suchmaschinen bitten diesen Link zu ignorieren' + DoOpenInNew: 'In neuem Fenster öffnen' + LinkText: Text + LinkType: Typ + MainTab: Basis + NotConfigured: 'Nicht konfiguriert' + OptionalAutoGenerated: 'Optional. Wird automatisch aus dem Link generiert, wenn dieses Feld leer bleibt.' + OptionsGroup: Optionen + SelectLinkType: 'Linktyp auswählen' + Fromholdio\SuperLinker\Model\VersionedSuperLink: + PLURALNAME: Links + PLURALS: + one: 'Ein Link' + other: '{count} Links' + SINGULARNAME: Link diff --git a/lang/en.yml b/lang/en.yml index d43bf32..ba89114 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -2,42 +2,46 @@ en: Fromholdio\SuperLinker\Extensions\EmailLink: Body: Body Email: Email - EmailBCC: BCC - EmailCC: CC - MustProvideEmail: 'You must provide an Email Address' Subject: Subject - Fromholdio\SuperLinker\Extensions\ExternalURLLink: - ExternalURLMustBeComplete: 'External URLs must be complete including http:// or https://' - URL: URL + Fromholdio\SuperLinker\Extensions\ExternalLink: + ExternalURL: URL Fromholdio\SuperLinker\Extensions\FileLink: + Behaviour: Behaviour + DisplayInBrowser: 'Display file in browser window (when possible)' + Download: "Download file directly to user's device" File: File - FileRequired: 'You must upload or select a file to link to' Fromholdio\SuperLinker\Extensions\GlobalAnchorLink: - Anchor: Anchor - AnchorRequired: 'You must select an anchor' - Fromholdio\SuperLinker\Extensions\NoLink: - NoLink: '- No Link -' - TitleRequired: 'You must provide a Title' + GlobalAnchor: 'Global anchor' Fromholdio\SuperLinker\Extensions\PhoneLink: - Phone: Phone - PhoneRequired: 'You must provide a phone number' + PhoneNumber: 'Phone number' Fromholdio\SuperLinker\Extensions\SiteTreeLink: - Anchor: Anchor - Page: Page - PageRequired: 'You must select a page to link to' + PageAnchorOptional: 'Page anchor (optional)' + PageOnThisWebsite: 'Page on this website' SelectAPage: 'Select a page' - SelectAnchor: 'Select an anchor (optional)' + SelectAnAnchor: 'Select an anchor' + Fromholdio\SuperLinker\Extensions\SuperLinkIconExtension: + LinkIcon: Icon Fromholdio\SuperLinker\Extensions\SystemLink: SystemLink: 'System Link' - SystemLinkRequired: 'You must select a system link' Fromholdio\SuperLinker\Model\SuperLink: - CustomLinkText: 'Link Text' - DoNoFollow: 'Instruct search engines not to follow this link' - DoOpenNewWindow: 'Open link in a new window' - NewWindow: 'New Window' - OptionalWillBeGenerated: 'Optional. Will be auto-generated if left blank.' - TabBehaviour: Behaviour - TabTarget: Target - URL: URL - URLInvalid: 'You must provide a valid URL' - URLRequired: 'You must provide a URL' + PLURALNAME: Links + PLURALS: + one: 'A Link' + other: '{count} Links' + SINGULARNAME: Link + Fromholdio\SuperLinker\Model\SuperLinkTrait: + DoNoFollow: 'Ask search engines to ignore' + DoOpenInNew: 'Open in new tab' + LinkText: Text + LinkType: Type + MainTab: Main + NotConfigured: 'Not configured' + OptionalAutoGenerated: 'Optional. Will be auto-generated from link if left blank.' + OptionsGroup: Options + SelectLinkType: 'Select link type' + Fromholdio\SuperLinker\Model\VersionedSuperLink: + PLURALNAME: Links + PLURALS: + one: 'A Link' + other: '{count} Links' + SINGULARNAME: Link diff --git a/lang/fr.yml b/lang/fr.yml index 58230ce..cc5feaa 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -1,43 +1,47 @@ fr: Fromholdio\SuperLinker\Extensions\EmailLink: - Body: 'Corps' - Email: 'E-mail' - EmailBCC: BCC - EmailCC: CC - MustProvideEmail: 'Vous devez fournir une adresse e-mail' - Subject: 'Sujet' - Fromholdio\SuperLinker\Extensions\ExternalURLLink: - ExternalURLMustBeComplete: 'Les URL externes doivent être complètes, y compris http:// ou https://' - URL: URL + Body: Corps + Email: E-mail + Subject: Sujet + Fromholdio\SuperLinker\Extensions\ExternalLink: + ExternalURL: URL Fromholdio\SuperLinker\Extensions\FileLink: + Behaviour: comportement + DisplayInBrowser: 'Afficher le fichier dans la fenêtre du navigateur (si possible)' + Download: "Télécharger le fichier directement sur l'appareil de l'utilisateur" File: Fichier - FileRequired: 'Vous devez télécharger ou sélectionner un fichier à lier' Fromholdio\SuperLinker\Extensions\GlobalAnchorLink: - Anchor: Ancre - AnchorRequired: 'Vous devez sélectionner une ancre' - Fromholdio\SuperLinker\Extensions\NoLink: - NoLink: '- Pas de lien -' - TitleRequired: 'Vous devez fournir un titre' + GlobalAnchor: 'Ancre du site' Fromholdio\SuperLinker\Extensions\PhoneLink: - Phone: Téléphone - PhoneRequired: 'Vous devez fournir un numéro de téléphone' + PhoneNumber: 'Numéro de téléphone' Fromholdio\SuperLinker\Extensions\SiteTreeLink: - Anchor: Ancre - Page: Page - PageRequired: 'Vous devez sélectionner une page à lier' + PageAnchorOptional: 'Ancre de page (facultatif)' + PageOnThisWebsite: 'Page sur ce site' SelectAPage: 'Sélectionnez une page' - SelectAnchor: 'Sélectionnez une ancre (facultatif)' + SelectAnAnchor: 'Sélectionnez une ancre' + Fromholdio\SuperLinker\Extensions\SuperLinkIconExtension: + LinkIcon: Symbole Fromholdio\SuperLinker\Extensions\SystemLink: SystemLink: 'Lien système' - SystemLinkRequired: 'Vous devez sélectionner un lien système' Fromholdio\SuperLinker\Model\SuperLink: - CustomLinkText: 'Texte du lien' - DoNoFollow: 'Demandez aux moteurs de recherche de ne pas suivre ce lien' - DoOpenNewWindow: 'Ouvrir le lien dans une nouvelle fenêtre' - NewWindow: 'Nouvelle fenêtre' - OptionalWillBeGenerated: 'Facultatif. Sera généré automatiquement si laissé vide.' - TabBehaviour: Comportement - TabTarget: Cible - URL: URL - URLInvalid: 'Vous devez fournir une URL valide' - URLRequired: 'Vous devez fournir une URL' + PLURALNAME: Liens + PLURALS: + one: 'Un lien' + other: '{count} liens' + SINGULARNAME: Lien + Fromholdio\SuperLinker\Model\SuperLinkTrait: + DoNoFollow: "Demandez aux moteurs de recherche d'ignorer" + DoOpenInNew: 'Ouvrir dans une nouvelle fenêtre' + LinkText: Texte + LinkType: Type + MainTab: Base + NotConfigured: 'Pas configuré' + OptionalAutoGenerated: 'Facultatif. Sera généré automatiquement à partir du lien si ce champ est laissé vide.' + OptionsGroup: Options + SelectLinkType: 'Sélectionnez le type de lien' + Fromholdio\SuperLinker\Model\VersionedSuperLink: + PLURALNAME: Liens + PLURALS: + one: 'Un lien' + other: '{count} liens' + SINGULARNAME: Lien diff --git a/src/Extensions/EmailLink.php b/src/Extensions/EmailLink.php index 347ad1e..97433a0 100644 --- a/src/Extensions/EmailLink.php +++ b/src/Extensions/EmailLink.php @@ -2,100 +2,74 @@ namespace Fromholdio\SuperLinker\Extensions; +use SilverStripe\Core\Convert; use SilverStripe\Forms\EmailField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\TextField; -use SilverStripe\ORM\DataExtension; -use SilverStripe\ORM\ValidationResult; -class EmailLink extends DataExtension +class EmailLink extends SuperLinkTypeExtension { - private static $singular_name = 'Email Link'; - private static $plural_name = 'Email Links'; - - private static $multi_add_title = 'Email address'; - - private static $enable_url_field_validation = false; + private static $extension_link_type = 'email'; + + private static $types = [ + 'email' => [ + 'label' => 'Email address', + 'cc' => false, + 'bcc' => false, + 'subject' => true, + 'body' => true, + 'settings' => [ + 'open_in_new' => false, + 'no_follow' => false + ] + ] + ]; private static $db = [ - 'Email' => 'Varchar(320)', - 'EmailCC' => 'Varchar(320)', - 'EmailBCC' => 'Varchar(320)', - 'Subject' => 'Varchar(255)', - 'Body' => 'Text' + 'Email' => 'Varchar', + 'EmailCC' => 'Varchar', + 'EmailBCC' => 'Varchar', + 'EmailSubject' => 'Varchar(255)', + 'EmailBody' => 'Text' ]; - public function updateLinkFields(FieldList &$fields) - { - $fields = FieldList::create( - EmailField::create('Email', _t(__CLASS__.'.Email', 'Email')), - EmailField::create('EmailCC', _t(__CLASS__.'.EmailCC', 'CC')), - EmailField::create('EmailBCC', _t(__CLASS__.'.EmailBCC', 'BCC')), - TextField::create('Subject', _t(__CLASS__.'.Subject', 'Subject')), - TextareaField::create('Body', _t(__CLASS__.'.Body', 'Body')) - ); - } - - public function updateValidate(ValidationResult &$result) - { - if (!$this->owner->Email) { - $result->addFieldError('Email', _t(__CLASS__.'.MustProvideEmail', 'You must provide an Email Address')); - } - } - - public function updateGenerateLinkText(&$text) + public function updateDefaultTitle(?string &$title): void { - $text = $this->owner->Email; + if (!$this->isLinkTypeMatch()) return; + $title = $this->getOwner()->getField('Email'); } - public function updateHasTarget(&$hasTarget) + public function updateURL(?string &$url): void { - $email = $this->getOwner()->Email; - $hasTarget = $email && !empty($email); - } - - public function updateIsSiteURL(bool &$isSiteURL) - { - $isSiteURL = true; - } - - public function updateLink(&$link) - { - if (!$this->owner->Email) { - $link = null; + if (!$this->isLinkTypeMatch()) return; + $email = $this->getOwner()->getField('Email'); + if (empty($email)) { + $url = null; return; } - - $link = 'mailto:' . $this->owner->Email; - - $parts = []; - - if ($this->owner->EmailCC) { - $parts[] = 'cc=' . $this->owner->EmailCC; - } - if ($this->owner->EmailBCC) { - $parts[] = 'bcc=' . $this->owner->EmailBCC; - } - if ($this->owner->Subject) { - $parts[] = 'subject=' . urlencode($this->owner->Subject); + $url = 'mailto:' . $email; + $urlParts = [ + 'cc' => $this->getOwner()->getField('EmailCC'), + 'bcc' => $this->getOwner()->getField('EmailBCC'), + 'subject' => $this->getOwner()->getField('EmailSubject'), + 'body' => $this->getOwner()->getField('EmailBody') + ]; + $urlParts = array_filter($urlParts); + if (!empty($urlParts)) { + $prefix = '?'; + foreach ($urlParts as $key => $value) { + $url .= $prefix . $key . '=' . Convert::raw2mailto($value); + $prefix = '&'; + } } - if ($this->owner->Body) { - $parts[] = 'body=' . urlencode($this->owner->Body); - } - - if (count($parts) > 0) { - $link .= '&' . implode('&', $parts); - } - } - - public function updateAbsoluteLink(&$link) - { - $link = $this->owner->Link(); } - public function updateLinkTarget(&$target) + public function updateCMSLinkTypeFields(FieldList $fields, string $type, string $fieldPrefix): void { - $target = $this->owner->dbObject('Email'); + if (!$this->isLinkTypeMatch($type)) return; + $fields->push(EmailField::create($fieldPrefix . 'Email', _t(__CLASS__ . '.Email', 'Email'))); + $fields->push(TextField::create($fieldPrefix . 'EmailSubject', _t(__CLASS__ . '.Subject', 'Subject'))); + $fields->push(TextareaField::create($fieldPrefix . 'EmailBody', _t(__CLASS__ . '.Body', 'Body'))); } } diff --git a/src/Extensions/ExternalLink.php b/src/Extensions/ExternalLink.php new file mode 100644 index 0000000..fea8dde --- /dev/null +++ b/src/Extensions/ExternalLink.php @@ -0,0 +1,52 @@ + [ + 'label' => 'External URL', + 'query_string' => true, + 'fragment' => true, + 'is_internal_url_allowed' => false, + 'allowed_schemes' => [ + 'https' => true, + 'http' => true, + 'ftp' => false + ] + ] + ]; + + private static $db = [ + 'ExternalURL' => 'Varchar(2083)' + ]; + + public function updateDefaultTitle(?string &$title): void + { + if (!$this->isLinkTypeMatch()) return; + $title = $this->getOwner()->getURL(); + } + + public function updateURL(?string &$url): void + { + if (!$this->isLinkTypeMatch()) return; + /** @var ExternalURLField $urlField */ + $urlField = $this->getOwner()->dbObject('ExternalURL'); + $url = $urlField->URL(); + } + + public function updateCMSLinkTypeFields(FieldList $fields, string $type, string $fieldPrefix): void + { + if (!$this->isLinkTypeMatch($type)) return; + $fields->push(ExternalURLField::create( + $fieldPrefix . 'ExternalURL', + _t(__CLASS__ . '.ExternalURL', 'URL') + )); + } +} diff --git a/src/Extensions/ExternalURLLink.php b/src/Extensions/ExternalURLLink.php deleted file mode 100644 index c57439b..0000000 --- a/src/Extensions/ExternalURLLink.php +++ /dev/null @@ -1,54 +0,0 @@ -getOwner()->Link())) { - $this->getOwner()->URL = $this->getOwner()->AbsoluteLink(); - } - - $fields->replaceField( - 'URL', - $urlField = ExternalURLField::create('URL', _t(__CLASS__.'.URL', 'URL')) - ); - - $urlField->setConfig('removeparts', [ - 'query' => !$this->owner->isQueryStringAllowed(), - 'fragment' => !$this->owner->isAnchorAllowed() - ]); - } - - public function updateValidate(ValidationResult &$result) - { - if (!Director::is_absolute_url($this->owner->URL)) { - $result->addFieldError('URL', _t(__CLASS__.'.ExternalURLMustBeComplete', 'External URLs must be complete including http:// or https://')); - } - } - - public function updateLink(&$link) - { - $link = $this->getOwner()->URL; - } - - public function updateAbsoluteLink(&$link) - { - $link = $this->getOwner()->URL; - } -} diff --git a/src/Extensions/FileLink.php b/src/Extensions/FileLink.php index c2efff6..14deb6a 100644 --- a/src/Extensions/FileLink.php +++ b/src/Extensions/FileLink.php @@ -3,75 +3,143 @@ namespace Fromholdio\SuperLinker\Extensions; use SilverStripe\AssetAdmin\Forms\UploadField; -use SilverStripe\Forms\FieldList; -use SilverStripe\ORM\DataExtension; use SilverStripe\Assets\File; -use SilverStripe\ORM\ValidationResult; +use SilverStripe\Assets\Folder; +use SilverStripe\Forms\DropdownField; +use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\TreeDropdownField; -class FileLink extends DataExtension +class FileLink extends SuperLinkTypeExtension { - private static $singular_name = 'File Link'; - private static $plural_name = 'File Links'; - - private static $multi_add_title = 'Download a file'; + private static $extension_link_type = 'file'; + + private static $types = [ + 'file' => [ + 'label' => 'Download a file', + 'use_upload_field' => false, + 'allow_uploads' => false, + 'starting_folder_path' => '', + 'allow_force_download' => true, + 'settings' => [ + 'no_follow' => false + ] + ] + ]; - private static $enable_url_field_validation = false; + private static $db = [ + 'DoForceDownload' => 'Boolean' + ]; private static $has_one = [ - 'File' => File::class + 'File' => File::class ]; - public function updateLinkFields(FieldList &$fields) - { - $fields = FieldList::create( - $uploadField = UploadField::create('File', _t(__CLASS__.'.File', 'File')) - ); - - $uploadField->setAllowedMaxFileNumber(1); - } + private static $owns = [ + 'File' + ]; - public function updateValidate(ValidationResult &$result) + public function getLinkedFile(): ?File { - if (!$this->owner->FileID) { - $result->addFieldError('File', _t(__CLASS__.'.FileRequired', 'You must upload or select a file to link to')); - } + if (!$this->isLinkTypeMatch()) return null; + /** @var ?File $file */ + $file = $this->getOwner()->getComponent('File'); + return $file?->exists() && !($file instanceof Folder) + ? $file + : null; } - public function updateAttributes(array &$attributes) + public function updateDefaultTitle(?string &$title): void { - $fileName = $this->owner->generateLinkText(); - $extension = $this->owner->File()->getExtension(); - - if ($fileName && $extension) { - $attributes['download'] = $fileName . '.' . $extension; - } else { - $attributes['download'] = true; - } + if (!$this->isLinkTypeMatch()) return; + $title = $this->getOwner()->getLinkedFile()?->getTitle(); } - public function updateGenerateLinkText(&$text) + public function updateURL(?string &$url): void { - $text = $this->owner->File()->Title; + if (!$this->isLinkTypeMatch()) return; + $url = $this->getOwner()->getLinkedFile()?->Link(); } - public function updateHasTarget(&$hasTarget) + public function updateAbsoluteURL(?string &$url): void { - $target = $this->getOwner()->File(); - $hasTarget = $target && $target->exists() && $target->canView(); + if (!$this->isLinkTypeMatch()) return; + $url = $this->getOwner()->getLinkedFile()?->AbsoluteLink(); } - public function updateLink(&$link) + public function isDownloadForced(): bool { - $link = $this->owner->File()->getURL(); + if (!$this->isLinkTypeMatch()) return false; + return (bool) $this->getOwner()->getField('DoForceDownload'); } - public function updateAbsoluteLink(&$link) + public function updateDefaultAttributes(array &$attrs): void { - $link = $this->owner->File()->getAbsoluteURL(); + if (!$this->isLinkTypeMatch()) return; + if ($this->getOwner()->isDownloadForced()) { + $attrs['download'] = $this->getOwner()->File()?->getField('Name'); + } } - public function updateLinkTarget(&$target) + public function updateCMSLinkTypeFields(FieldList $fields, string $type, string $fieldPrefix): void { - $target = $this->File(); + if (!$this->isLinkTypeMatch($type)) return; + + $folderPath = $this->getOwner()->getTypeConfigValue('starting_folder_path', $type); + if (!empty($folderPath)) { + $folder = Folder::find_or_make($folderPath); + } + + if ($this->getOwner()->getTypeConfigValue('use_upload_field', $type)) + { + $fileField = UploadField::create( + $fieldPrefix . 'File', + _t(__CLASS__ . '.File', 'File') + ); + if (!$this->getOwner()->getTypeConfigValue('allow_uploads', $type)) { + $fileField->setUploadEnabled(false); + } + if (!is_null($folderPath)) { + $fileField->setFolderName($folderPath); + } + } + else { + $fileField = TreeDropdownField::create( + $fieldPrefix . 'FileID', + _t(__CLASS__ . '.File', 'File'), + File::class, + 'ID', + 'Title' + ); + $fileField->setFilterFunction(function($item) { + if (is_a($item, Folder::class)) { + if ($item->Children()->count() < 1) { + return false; + } + } + return true; + }); + $fileField->setDisableFunction(function($item) { + return is_a($item, Folder::class); + }); + if (!is_null($folderPath)) { + $folderID = empty($folder) ? 0 : (int) $folder->getField('ID'); + $fileField->setTreeBaseID($folderID); + } + } + $fields->push($fileField); + + if (!$this->getOwner()->getTypeConfigValue('allow_force_download', $type)) { + return; + } + + $doForceDownloadField = DropdownField::create( + $fieldPrefix . 'DoForceDownload', + _t(__CLASS__ . '.Behaviour', 'Behaviour'), + [ + 1 => _t(__CLASS__ . '.Download', 'Download file directly to user\'s device'), + 0 => _t(__CLASS__ . '.DisplayInBrowser', 'Display file in browser window (when possible)'), + ] + ); + $fields->push($doForceDownloadField); } } diff --git a/src/Extensions/GlobalAnchorLink.php b/src/Extensions/GlobalAnchorLink.php index 1836dcd..2be68a9 100644 --- a/src/Extensions/GlobalAnchorLink.php +++ b/src/Extensions/GlobalAnchorLink.php @@ -2,97 +2,76 @@ namespace Fromholdio\SuperLinker\Extensions; +use Fromholdio\GlobalAnchors\GlobalAnchors; use SilverStripe\CMS\Controllers\ContentController; -use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Forms\DropdownField; -use SilverStripe\Forms\FieldList; -use SilverStripe\ORM\DataExtension; -use SilverStripe\ORM\ValidationResult; -class GlobalAnchorLink extends DataExtension +class GlobalAnchorLink extends SuperLinkTypeExtension { - private static $singular_name = 'Global Anchor Link'; - private static $plural_name = 'Global Anchor Links'; - - private static $multi_add_title = 'Global Anchor'; - - private static $allow_anchor = true; - - private static $enable_url_field_validation = false; - - public function updateLinkFields(FieldList &$fields) + private static $extension_link_type = 'globalanchor'; + + private static $types = [ + 'globalanchor' => [ + 'label' => 'Global anchor', + 'settings' => [ + 'open_in_new' => false, + 'no_follow' => false + ] + ] + ]; + + private static $db = [ + 'GlobalAnchorKey' => 'Varchar(30)' + ]; + + public function getLinkedGlobalAnchor(): ?string { - $fields = FieldList::create( - DropdownField::create( - 'Anchor', - _t(__CLASS__.'.Anchor', 'Anchor'), - $this->owner->getGlobalAnchors() - ) - ); + if (!$this->isLinkTypeMatch()) return null; + $anchors = GlobalAnchors::get_anchors(); + $key = $this->getOwner()->getField('GlobalAnchorKey'); + return isset($anchors[$key]) ? $key : null; } - public function updateValidate(ValidationResult &$result) + public function updateDefaultTitle(?string &$title): void { - if (!$this->owner->Anchor) { - $result->addFieldError('Anchor', _t(__CLASS__.'.AnchorRequired', 'You must select an anchor')); - } - } - - public function updateGenerateLinkText(&$text) - { - $text = $this->owner->getGlobalAnchor($this->owner->Anchor); + if (!$this->isLinkTypeMatch()) return; + $anchor = $this->getOwner()->getLinkedGlobalAnchor(); + $title = GlobalAnchors::get_anchor_title($anchor); } - public function updateHasTarget(&$hasTarget) + public function updateURL(?string &$url): void { - $anchor = $this->owner->getGlobalAnchor($this->owner->Anchor); - $hasTarget = $anchor && !empty($anchor); + if (!$this->isLinkTypeMatch()) return; + $anchor = $this->getOwner()->getLinkedGlobalAnchor(); + $url = empty($anchor) ? null : '#' . $anchor; } - public function updateIsSiteURL(bool &$isSiteURL) + public function updateAbsoluteURL(?string &$url): void { - $isSiteURL = true; - } - - public function updateLink(&$link) - { - if (!$this->owner->Anchor || !$this->owner->isAnchorAllowed()) { - $link = null; + if (!$this->isLinkTypeMatch()) return; + $anchor = $this->getOwner()->getLinkedGlobalAnchor(); + if (empty($anchor)) { + $url = null; return; } - - $link = '#' . $this->owner->Anchor; + $curr = Controller::curr(); + $link = $curr instanceof ContentController + ? $curr->Link() + : Director::absoluteBaseURL(); + $url = Controller::join_links($link, '#' . $anchor); } - public function updateAbsoluteLink(&$link) + public function updateCMSLinkTypeFields($fields, $type, $fieldPrefix): void { - if (!$this->owner->Anchor || !$this->owner->isAnchorAllowed()) { - $link = null; - return; - } - - $currentController = Controller::curr(); - if (is_a($currentController, ContentController::class)) { - $currentPage = $currentController->data(); - if ($currentPage && is_a($currentPage, SiteTree::class)) { - $link = Controller::join_links( - $currentPage->AbsoluteLink(), - '#' . $this->owner->Anchor - ); - return; - } - } - - $link = Controller::join_links( - Director::absoluteBaseURL(), - '#' . $this->owner->Anchor + if ($type !== 'globalanchor') return; + $fields->push( + DropdownField::create( + $fieldPrefix . 'GlobalAnchorKey', + _t(__CLASS__ . '.GlobalAnchor', 'Global anchor'), + GlobalAnchors::get_anchors() + ) ); } - - public function updateLinkTarget(&$target) - { - $target = $this->owner->dbObject('Anchor'); - } } diff --git a/src/Extensions/NoLink.php b/src/Extensions/NoLink.php deleted file mode 100644 index 5f93c98..0000000 --- a/src/Extensions/NoLink.php +++ /dev/null @@ -1,71 +0,0 @@ -dataFieldByName('CustomLinkText'); - $titleField->setDescription(''); - } - - public function updateLinkFields(FieldList &$fields) - { - $fields->removeByName('URL'); - } - - public function updateBehaviourFields(&$fields) - { - $fields = FieldList::create(); - } - - public function updateValidate(ValidationResult &$result) - { - if (!$this->getOwner()->Title) { - $result->addFieldError('Title', _t(__CLASS__.'.TitleRequired', 'You must provide a Title')); - } - } - - public function updateGenerateLinkText(&$text) - { - $text = _t(__CLASS__.'.NoLink', '- No Link -'); - } - - public function updateHasTarget(&$hasTarget) - { - $text = $this->getOwner()->Title; - $hasTarget = $text && !empty($text); - } - - public function updateIsSiteURL(bool &$isSiteURL) - { - $isSiteURL = false; - } - - public function updateLink(&$link) - { - $link = null; - } - - public function updateAbsoluteLink(&$link) - { - $link = null; - } -} diff --git a/src/Extensions/NullLink.php b/src/Extensions/NullLink.php new file mode 100644 index 0000000..6ee3306 --- /dev/null +++ b/src/Extensions/NullLink.php @@ -0,0 +1,24 @@ + [ + 'label' => 'Text only (no link)', + 'settings' => [ + 'open_in_new' => false, + 'no_follow' => false + ] + ] + ]; + + public function updateIsLinkEmpty(bool &$value): void + { + if (!$this->isLinkTypeMatch()) return; + $value = false; + } +} diff --git a/src/Extensions/PhoneLink.php b/src/Extensions/PhoneLink.php index ed567e5..3c45864 100644 --- a/src/Extensions/PhoneLink.php +++ b/src/Extensions/PhoneLink.php @@ -2,96 +2,51 @@ namespace Fromholdio\SuperLinker\Extensions; -use libphonenumber\PhoneNumberFormat; -use libphonenumber\PhoneNumberUtil; +use Innoweb\InternationalPhoneNumberField\Forms\InternationalPhoneNumberField; +use Innoweb\InternationalPhoneNumberField\ORM\DBPhone; use SilverStripe\Forms\FieldList; -use SilverStripe\Forms\TextField; -use SilverStripe\ORM\DataExtension; -use SilverStripe\ORM\ValidationResult; -class PhoneLink extends DataExtension +class PhoneLink extends SuperLinkTypeExtension { - private static $singular_name = 'Phone Link'; - private static $plural_nbame = 'Phone Links'; - - private static $multi_add_title = 'Phone number'; - - private static $enable_url_field_validation = false; - - private static $default_country = 'AU'; + private static $extension_link_type = 'phone'; + + private static $types = [ + 'phone' => [ + 'label' => 'Phone number', + 'settings' => [ + 'open_in_new' => false, + 'no_follow' => false + ] + ] + ]; private static $db = [ - 'Phone' => 'Varchar(30)' + 'PhoneNumber' => 'Phone' ]; - public function updateLinkFields(FieldList &$fields) - { - $fields = FieldList::create( - TextField::create('Phone', _t(__CLASS__.'.Phone', 'Phone')) - ); - } - - public function updateValidate(ValidationResult &$result) - { - if (!$this->owner->Phone) { - $result->addFieldError('Phone', _t(__CLASS__.'.PhoneRequired', 'You must provide a phone number')); - } - } - - public function updateGenerateLinkText(&$text) - { - if (!$this->owner->Phone) { - return null; - } - - $phoneUtil = PhoneNumberUtil::getInstance(); - $phone = $phoneUtil->parse( - $this->owner->Phone, - $this->owner->getDefaultCountry() - ); - - $text = $phoneUtil->format($phone, PhoneNumberFormat::E164); - } - - public function updateHasTarget(&$hasTarget) - { - $phone = $this->getOwner()->Phone; - $hasTarget = $phone && !empty($phone); - } - - public function updateIsSiteURL(bool &$isSiteURL) - { - $isSiteURL = true; - } - - public function updateLink(&$link) - { - if (!$this->owner->Phone) { - $link = null; - return; - } - - $phoneUtil = PhoneNumberUtil::getInstance(); - $phone = $phoneUtil->parse( - $this->owner->Phone, - $this->owner->getDefaultCountry() - ); - - $link = $phoneUtil->format($phone, PhoneNumberFormat::RFC3966); - } - - public function updateAbsoluteLink(&$link) + public function updateDefaultTitle(?string &$title): void { - $link = $this->owner->Link(); + if (!$this->isLinkTypeMatch()) return; + /** @var DBPhone $phoneField */ + $phoneField = $this->getOwner()->dbObject('PhoneNumber'); + $title = $phoneField->International(); } - public function updateLinkTarget(&$target) + public function updateURL(?string &$url): void { - $target = $this->owner->dbObject('Phone'); + if (!$this->isLinkTypeMatch()) return; + /** @var DBPhone $phoneField */ + $phoneField = $this->getOwner()->dbObject('PhoneNumber'); + $phoneNumber = $phoneField->URL(); + $url = empty($phoneNumber) ? null : $phoneNumber; } - public function getDefaultCountry() + public function updateCMSLinkTypeFields(FieldList $fields, string $type, string $fieldPrefix): void { - return $this->owner->config()->get('default_country'); + if (!$this->isLinkTypeMatch($type)) return; + $fields->push(InternationalPhoneNumberField::create( + $fieldPrefix . 'PhoneNumber', + _t(__CLASS__ . '.PhoneNumber', 'Phone number') + )); } } diff --git a/src/Extensions/SiteTreeLink.php b/src/Extensions/SiteTreeLink.php index 59269ef..9026f42 100644 --- a/src/Extensions/SiteTreeLink.php +++ b/src/Extensions/SiteTreeLink.php @@ -3,140 +3,153 @@ namespace Fromholdio\SuperLinker\Extensions; use Fromholdio\DependentGroupedDropdownField\Forms\DependentGroupedDropdownField; +use Fromholdio\GlobalAnchors\GlobalAnchors; use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Control\Controller; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TreeDropdownField; -use SilverStripe\ORM\DataExtension; -use SilverStripe\ORM\ValidationResult; -class SiteTreeLink extends DataExtension +class SiteTreeLink extends SuperLinkTypeExtension { - private static $singular_name = 'Site Tree Link'; - private static $plural_name = 'Site Tree Links'; - - private static $multi_add_title = 'Page on this website'; - - private static $allow_anchor = true; + private static $extension_link_type = 'sitetree'; + + private static $types = [ + 'sitetree' => [ + 'label' => 'Page on this website', + 'allow_anchor' => true, + 'settings' => [ + 'no_follow' => false + ] + ] + ]; - private static $enable_url_field_validation = false; + private static $db = [ + 'SiteTreeAnchor' => 'Varchar(255)' + ]; private static $has_one = [ - 'SiteTree' => SiteTree::class + 'SiteTree' => SiteTree::class ]; - public function updateLinkFields(FieldList &$fields) + public function getLinkedSiteTree(): ?SiteTree { - $globalAnchors = $this->getOwner()->getGlobalAnchors(); + if (!$this->isLinkTypeMatch()) return null; + /** @var ?SiteTree $siteTree */ + $siteTree = $this->getOwner()->getComponent('SiteTree'); + return $siteTree?->exists() ? $siteTree : null; + } - $anchorSource = function($siteTreeID) use ($globalAnchors) { + public function getLinkedSiteTreeAnchor(): ?string + { + if (!$this->isLinkTypeMatch()) return null; + return $this->getOwner()->getField('SiteTreeAnchor'); + } - $anchors = []; + public function getAvailableSiteTreeAnchors(int|string|null $siteTreeID): array + { + $anchors = []; - $siteTree = SiteTree::get()->byID($siteTreeID); - if ($siteTree && $siteTree->exists()) { - $contentAnchors = $siteTree->dbObject('Content')->getAnchors(); - if ($contentAnchors) { - $anchors['Page Content'] = $contentAnchors; - } - } + /** @var ?SiteTree $siteTree */ + $siteTree = SiteTree::get()->find('ID', $siteTreeID ?? -1); - if ($globalAnchors && !empty($globalAnchors)) { - $anchors['Global Template'] = $globalAnchors; - } + $contentAnchors = $siteTree?->getAnchorsOnPage(); + $this->getOwner()->invokeWithExtensions( + 'updateAvailableSiteTreeContentAnchors', + $contentAnchors, $siteTree + ); + if (!empty($contentAnchors)) { + $anchors['Page content'] = array_combine($contentAnchors, $contentAnchors); + } - return $anchors; - }; + $globalAnchors = GlobalAnchors::get_anchors(); + if (!empty($globalAnchors)) { + $anchors['Global anchors'] = $globalAnchors; + } - $fields = FieldList::create( - $siteTreeField = TreeDropdownField::create( - 'SiteTreeID', - _t(__CLASS__.'.Page', 'Page'), - SiteTree::class - ) - ->setEmptyString(_t(__CLASS__.'.SelectAPage', 'Select a page')) - ->setHasEmptyDefault(true) - , - DependentGroupedDropdownField::create( - 'Anchor', - _t(__CLASS__.'.Anchor', 'Anchor'), - $anchorSource - ) - ->setDepends($siteTreeField) - ->setEmptyString(_t(__CLASS__.'.SelectAnchor', 'Select an anchor (optional)')) + $this->getOwner()->invokeWithExtensions( + 'updateAvailableSiteTreeAnchors', + $anchors, $siteTree ); + return $anchors; } - public function updateValidate(ValidationResult &$result) + public function updateIsCurrent(bool &$value): void { - if (!$this->owner->SiteTreeID) { - $result->addFieldError('SiteTreeID', _t(__CLASS__.'.PageRequired', 'You must select a page to link to')); - } + if (!$this->isLinkTypeMatch()) return; + $siteTree = $this->getOwner()->getLinkedSiteTree(); + if (!$siteTree) return; + $value = $siteTree->isCurrent(); } - public function updateGenerateLinkText(&$text) + public function updateIsSection(bool &$value): void { - $text = $this->owner->SiteTree()->Title; + if (!$this->isLinkTypeMatch()) return; + $siteTree = $this->getOwner()->getLinkedSiteTree(); + if (!$siteTree) return; + $value = $siteTree->isSection(); } - public function updateLink(&$link, &$queryString, &$anchor) + public function updateDefaultTitle(?string &$title): void { - $link = Controller::join_links( - $this->owner->SiteTree()->Link(), - $queryString ? '?' . $queryString : null, - $anchor ? '#' . $anchor : null - ); + if (!$this->isLinkTypeMatch()) return; + $title = $this->getOwner()->getLinkedSiteTree()?->getTitle(); } - public function updateAbsoluteLink(&$link, &$queryString, &$anchor) + public function updateURL(?string &$url): void { - $link = Controller::join_links( - $this->owner->SiteTree()->AbsoluteLink(), - $queryString ? '?' . $queryString : null, - $anchor ? '#' . $anchor : null - ); + if (!$this->isLinkTypeMatch()) return; + $url = $this->getOwner()->getLinkedSiteTree()?->Link(); + $anchor = $this->getOwner()->getLinkedSiteTreeAnchor(); + if (!empty($anchor)) $url .= '#' . $anchor; } - public function updateLinkTarget(&$target) + public function updateAbsoluteURL(?string &$url): void { - $target = $this->owner->SiteTree(); + if (!$this->isLinkTypeMatch()) return; + $url = $this->getOwner()->getLinkedSiteTree()?->AbsoluteLink(); } - public function updateHasTarget(&$hasTarget) + public function getAllowedLinkedSiteTreeRoot(): ?SiteTree { - $target = $this->getOwner()->SiteTree(); - $hasTarget = $target && $target->exists() && $target->canView(); + $siteTree = null; + $this->getOwner()->invokeWithExtensions('updateAllowedLinkedSiteTreeRoot', $siteTree); + return $siteTree; } - public function updateLinkOrCurrent(&$linkOrCurrent) + public function updateCMSLinkTypeFields(FieldList $fields, string $type, string $fieldPrefix): void { - $siteTree = $this->getOwner()->SiteTree(); - if ($siteTree) { - $linkOrCurrent = $siteTree->LinkOrCurrent(); - } - } + if (!$this->isLinkTypeMatch($type)) return; - public function updateLinkOrSection(&$linkOrSection) - { - $siteTree = $this->getOwner()->SiteTree(); - if ($siteTree) { - $linkOrSection = $siteTree->LinkOrSection(); - } - } + $siteTreeField = TreeDropdownField::create( + $fieldPrefix . 'SiteTreeID', + _t(__CLASS__ . '.PageOnThisWebsite', 'Page on this website'), + SiteTree::class + ); + $siteTreeField->setEmptyString('-- ' . _t(__CLASS__ . '.SelectAPage', 'Select a page') . ' --'); + $siteTreeField->setHasEmptyDefault(true); + $fields->push($siteTreeField); - public function updateLinkingMode(&$linkingMode) - { - $siteTree = $this->getOwner()->SiteTree(); - if ($siteTree) { - $linkingMode = $siteTree->LinkingMode(); + $siteTreeRoot = $this->getOwner()->getAllowedLinkedSiteTreeRoot(); + if (!is_null($siteTreeRoot)) { + $siteTreeField->setTreeBaseID($siteTreeRoot->getField('ID')); } - } - public function updateInSection(&$inSection) - { - $siteTree = $this->getOwner()->SiteTree(); - if ($siteTree) { - $inSection = $siteTree->InSection(); + if (!$this->getOwner()->getTypeConfigValue('allow_anchor', $type)) { + return; } + + $siteTreeLink = $this->getOwner(); + $anchorSource = function(int|string|null $siteTreeID) use ($siteTreeLink) { + return $siteTreeLink->getAvailableSiteTreeAnchors($siteTreeID); + }; + + $anchorField = DependentGroupedDropdownField::create( + $fieldPrefix . 'SiteTreeAnchor', + _t(__CLASS__ . '.PageAnchorOptional', 'Page anchor (optional)'), + $anchorSource + ); + $anchorField + ->setDepends($siteTreeField) + ->setEmptyString('-- ' . _t(__CLASS__ . '.SelectAnAnchor', 'Select an anchor') . ' --'); + $fields->push($anchorField); } } diff --git a/src/Extensions/SuperLinkIconExtension.php b/src/Extensions/SuperLinkIconExtension.php new file mode 100644 index 0000000..d6615e2 --- /dev/null +++ b/src/Extensions/SuperLinkIconExtension.php @@ -0,0 +1,78 @@ + true + ]; + + private static $icon_folder_path = null; + private static $icon_allowed_extensions = []; + private static $icon_allowed_categories = ['image/supported']; + + private static $has_one = [ + 'LinkIcon' => Image::class + ]; + + private static $owns = [ + 'LinkIcon' + ]; + + private static $cascade_duplicates = [ + 'LinkIcon' + ]; + + public function getIcon(): ?Image + { + if (!$this->getOwner()->isSettingEnabled('icon')) return null; + + /** @var ?Image $icon */ + $icon = $this->getOwner()->getComponent('LinkIcon'); + return $icon?->exists() ? $icon : null; + } + + public function updateCMSLinkFieldsBeforeTypes(FieldList $fields, string $fieldPrefix): void + { + if (!$this->getOwner()->isSettingEnabled('icon')) return; + + $iconField = UploadField::create( + $fieldPrefix . 'LinkIcon', + _t(__CLASS__ . '.LinkIcon', 'Icon') + ); + $folderPath = $this->getOwner()->config()->get('icon_folder_path'); + if (!is_null($folderPath)) { + $iconField->setFolderName($folderPath); + } + $iconExtensions = $this->getOwner()->config()->get('icon_allowed_extensions'); + if (!empty($iconExtensions)) { + $iconField->setAllowedExtensions($iconExtensions); + } + else { + $iconCategories = $this->getOwner()->config()->get('icon_allowed_categories'); + if (!empty($iconCategories)) { + $iconField->setAllowedFileCategories(...$iconCategories); + } + } + $fields->push($iconField); + } + + + /** + * @return SuperLink|VersionedSuperLink + */ + public function getOwner() + { + /** @var SuperLink $owner */ + $owner = parent::getOwner(); + return $owner; + } +} diff --git a/src/Extensions/SuperLinkTypeExtension.php b/src/Extensions/SuperLinkTypeExtension.php new file mode 100644 index 0000000..7023c20 --- /dev/null +++ b/src/Extensions/SuperLinkTypeExtension.php @@ -0,0 +1,106 @@ +get( + static::class, + 'extension_link_type', + Config::UNINHERITED + ); + if (empty($type)) { + throw new \UnexpectedValueException( + 'Missing configuration, please set string value for ' + . static::class . '::$extension_link_type' + ); + } + return $type; + } + + protected function isLinkTypeMatch(?string $type = null): bool + { + if (empty($type)) $type = $this->getOwner()->getType(); + return $type === $this->getExtensionLinkType(); + } + + + /** + * @return SuperLink|VersionedSuperLink + */ + public function getOwner() + { + /** @var SuperLink $owner */ + $owner = parent::getOwner(); + return $owner; + } +} diff --git a/src/Extensions/SystemLink.php b/src/Extensions/SystemLink.php index e1a62fb..5ef1935 100644 --- a/src/Extensions/SystemLink.php +++ b/src/Extensions/SystemLink.php @@ -2,135 +2,55 @@ namespace Fromholdio\SuperLinker\Extensions; -use Fromholdio\SuperLinker\Model\SuperLink; use Fromholdio\SystemLinks\SystemLinks; -use SilverStripe\Control\Director; -use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; -use SilverStripe\ORM\DataExtension; -use SilverStripe\ORM\ValidationResult; +use SilverStripe\View\ArrayData; -class SystemLink extends DataExtension +class SystemLink extends SuperLinkTypeExtension { - private static $singular_name = 'System Link'; - private static $plural_name = 'System Links'; - - private static $multi_add_title = 'System link'; - - private static $enable_url_field_validation = false; - - private static $system_links_source_class = SuperLink::class; + private static $extension_link_type = 'system'; + + private static $types = [ + 'system' => [ + 'label' => 'System link', + 'settings' => [ + 'no_follow' => false + ] + ] + ]; private static $db = [ - 'Key' => 'Varchar(30)' + 'SystemLinkKey' => 'Varchar(30)' ]; - public function updateLinkFields(FieldList &$fields) + public function getLinkedSystemLink(): ?ArrayData { - $links = $this->owner->getSystemLinks(); - if (!$links || empty($links)) { - $fields = FieldList::create(); - return; - } - - $keySource = []; - foreach ($links as $key => $link) { - $keySource[$key] = $link['title']; - } - - $fields = FieldList::create( - DropdownField::create( - 'Key', - _t(__CLASS__.'.SystemLink', 'System Link'), - $keySource - ) - ->setHasEmptyDefault(false) - ); + if (!$this->isLinkTypeMatch()) return null; + return SystemLinks::get_link($this->getOwner()->getField('SystemLinkKey')); } - public function updateValidate(ValidationResult &$result) + public function updateDefaultTitle(?string &$title): void { - if (!$this->owner->Key) { - $result->addFieldError('Key', _t(__CLASS__.'.SystemLinkRequired', 'You must select a system link')); - } + if (!$this->isLinkTypeMatch()) return; + $title = $this->getOwner()->getLinkedSystemLink()?->getField('Title'); } - public function updateGenerateLinkText(&$text) + public function updateURL(?string &$url): void { - $link = $this->owner->getSystemLink($this->owner->Key); - if (isset($link['title'])) { - $text = $link['title']; - } else { - $text = null; - } + if (!$this->isLinkTypeMatch()) return; + $url = $this->getOwner()->getLinkedSystemLink()?->getField('URL'); } - public function updateHasTarget(&$hasTarget) + public function updateCMSLinkTypeFields(FieldList $fields, string $type, string $fieldPrefix): void { - $link = $this->owner->getSystemLink($this->owner->Key); - $hasTarget = isset($link['url']); - } - - public function updateIsSiteURL(bool &$isSiteURL) - { - $isSiteURL = true; - } - - public function updateLink(&$link) - { - $link = $this->owner->getSystemLink($this->owner->Key); - if (isset($link['url'])) { - $link = $link['url']; - } else { - $link = null; - } - } - - public function updateAbsoluteLink(&$absoluteLink) - { - $link = $this->owner->Link(); - if (!$link) { - $absoluteLink = null; - } - - if (Director::is_absolute_url($link)) { - $absoluteLink = $link; - } else { - $absoluteLink = Director::absoluteURL($link); - } - } - - public function getSystemLinks() - { - $sourceClass = $this->owner->config()->get('system_links_source_class'); - $exists = ModuleLoader::inst()->getManifest() - ->moduleExists('fromholdio/silverstripe-systemlinks'); - if ($exists || $sourceClass === SystemLinks::class) { - $links = SystemLinks::get_raw_links(); - } else { - $links = Config::inst()->get($sourceClass, 'system_links'); - } - - if ($this->owner->hasMethod('updateSystemLinks')) { - $links = $this->owner->updateSystemLinks($links); - } - - return $links; - } - - public function getSystemLink($key) - { - $links = $this->owner->getSystemLinks(); - if (isset($links[$key])) { - return $links[$key]; - } - return null; - } - - public function updateLinkTarget(&$target) - { - $target = $this->owner->getSystemLink($this->owner->Key); + if (!$this->isLinkTypeMatch($type)) return; + $fields->push( + DropdownField::create( + $fieldPrefix . 'SystemLinkKey', + _t(__CLASS__ . '.SystemLink', 'System Link'), + SystemLinks::get_map() + ) + ); } } diff --git a/src/Model/SuperLink.php b/src/Model/SuperLink.php index 53e0567..ba79a81 100644 --- a/src/Model/SuperLink.php +++ b/src/Model/SuperLink.php @@ -2,496 +2,11 @@ namespace Fromholdio\SuperLinker\Model; -use Fromholdio\GlobalAnchors\GlobalAnchors; -use SilverStripe\Control\Controller; -use SilverStripe\Control\Director; -use SilverStripe\Core\Convert; -use SilverStripe\Core\Manifest\ModuleLoader; -use SilverStripe\Forms\CheckboxField; -use SilverStripe\Forms\FieldGroup; -use SilverStripe\Forms\FieldList; -use SilverStripe\Forms\LiteralField; -use SilverStripe\Forms\Tab; -use SilverStripe\Forms\TabSet; -use SilverStripe\Forms\TextField; use SilverStripe\ORM\DataObject; -use SilverStripe\ORM\ValidationResult; -use SilverStripe\Versioned\Versioned; class SuperLink extends DataObject { - protected $attributes = []; + use SuperLinkTrait; private static $table_name = 'SuperLink'; - private static $singular_name = 'Link'; - private static $plural_name = 'Links'; - - private static $allow_anchor = false; - private static $allow_query_string = false; - - private static $enable_url_field_validation = true; - private static $enable_custom_link_text = true; - private static $enable_tabs = false; - - private static $extensions = [ - Versioned::class - ]; - - private static $db = [ - 'URL' => 'Varchar(2083)', - 'DoOpenNewWindow' => 'Boolean', - 'DoNoFollow' => 'Boolean', - 'Anchor' => 'Varchar(255)', - 'QueryString' => 'Varchar(255)', - 'CustomLinkText' => 'Varchar(2000)' - ]; - - private static $defaults = [ - 'DoOpenNewWindow' => false, - 'DoNoFollow' => false - ]; - - private static $casting = [ - 'Title' => 'Varchar', - 'Href' => 'HTMLFragment', - 'AttributesHTML' => 'HTMLFragment' - ]; - - public function Link() - { - $queryString = ($this->isQueryStringAllowed() && $this->QueryString) - ? $this->QueryString - : null; - - $anchor = ($this->isAnchorAllowed() && $this->Anchor) - ? $this->Anchor - : null; - - $link = Controller::join_links( - $this->URL, - $queryString ? '?' . $queryString : null, - $anchor ? '#' . $anchor : null - ); - - $this->extend('updateLink', $link, $queryString, $anchor); - return $link; - } - - public function AbsoluteLink() - { - $queryString = ($this->isQueryStringAllowed() && $this->QueryString) - ? $this->QueryString - : null; - - $anchor = ($this->isAnchorAllowed() && $this->Anchor) - ? $this->Anchor - : null; - - $url = $this->URL ?? ''; - - if (Director::is_absolute_url($url)) { - $absoluteURL = $url; - } else { - $absoluteURL = Director::absoluteURL($url); - } - - $link = Controller::join_links( - $absoluteURL, - $queryString ? '?' . $queryString : null, - $anchor ? '#' . $anchor : null - ); - - $this->extend('updateAbsoluteLink', $link, $queryString, $anchor); - return $link; - } - - public function LinkOrCurrent() - { - $linkOrCurrent = null; - $this->extend('updateLinkOrCurrent', $linkOrCurrent); - return $linkOrCurrent; - } - - public function LinkOrSection() - { - $linkOrSection = null; - $this->extend('updateLinkOrSection'); - return $linkOrSection; - } - - public function LinkingMode() - { - $linkingMode = null; - $this->extend('updateLinkingMode', $linkingMode); - return $linkingMode; - } - - public function InSection($sectionName) - { - $inSection = null; - $this->extend('updateInSection', $inSection); - return $inSection; - } - - public function HasLink() - { - $hasLink = true; - $this->extend('updateHasLink', $hasLink); - return $hasLink; - } - - public function HasTarget() - { - $url = $this->URL; - $hasTarget = $url && !empty($url); - $this->extend('updateHasTarget', $hasTarget); - return $hasTarget; - } - - public function getLinkText() - { - if ($this->isCustomLinkTextEnabled() && $this->CustomLinkText) { - $text = $this->CustomLinkText; - } else { - $text = $this->generateLinkText(); - } - - $this->extend('updateLinkText', $text); - return $text; - } - - public function getTitle() - { - if ($this->dbObject('Title') !== null) { - return $this->dbObject('Title'); - } - $title = $this->getLinkText(); - $this->extend('updateTitle', $title); - return $title; - } - - public function generateLinkText() - { - $text = $this->URL; - $this->extend('updateGenerateLinkText', $text); - return $text; - } - - public function getHref($forceAbsolute = false) - { - $href = ($forceAbsolute) ? $this->AbsoluteLink() : $this->Link(); - $this->extend('updateHref', $href, $forceAbsolute); - return $href; - } - - public function setAttribute($name, $value) - { - $this->attributes[$name] = $value; - return $this; - } - - public function getAttribute($name) - { - $attributes = $this->getAttributes(); - - if (isset($attributes[$name])) { - return $attributes[$name]; - } - return null; - } - - public function getAttributes() - { - $attributes = []; - - $attributes['href'] = $this->getHref(); - - if (!$this->isSiteURL()) { - $attributes['rel'][] = 'noopener'; - } - - if ($this->DoOpenNewWindow) { - $attributes['target'] = '_blank'; - } - - if ($this->DoNoFollow) { - $attributes['rel'][] = 'nofollow'; - } - - $attributes = array_merge($attributes, $this->attributes); - - $this->extend('updateAttributes', $attributes); - return $attributes; - } - - public function getAttributesHTML($excluded = null) - { - $attributes = $this->getAttributes(); - - // Remove excluded - $excluded = (is_string($excluded)) ? func_get_args() : null; - if ($excluded) { - $attributes = array_diff_key($attributes, array_flip($excluded)); - } - - // Create markup - $parts = []; - foreach ($attributes as $name => $value) { - - if ($value === null) continue; - - if (is_array($value)) { - $partValue = implode(' ', $value); - } else if ($value === true) { - $partValue = false; - } else { - $partValue = $value; - } - - $attribute = $name; - if ($partValue !== false) { - $attribute .= '="' . Convert::raw2att($partValue) . '"'; - } - $parts[] = $attribute; - } - - $this->extend('updateAttributesHTML', $parts, $attributes); - return implode(' ', $parts); - } - - public function isSiteURL() - { - $isSiteURL = Director::is_site_url($this->Link()); - $this->extend('updateIsSiteURL', $isSiteURL); - return $isSiteURL; - } - - public function isAnchorAllowed() - { - $allowed = (bool) $this->config()->get('allow_anchor'); - $this->extend('updateIsAnchorAllowed', $allowed); - return $allowed; - } - - public function isQueryStringAllowed() - { - $allowed = (bool) $this->config()->get('allow_query_string'); - $this->extend('updateIsQueryStringAllowed', $allowed); - return $allowed; - } - - public function isURLFieldValidationEnabled() - { - $enabled = (bool) $this->config()->get('enable_url_field_validation'); - $this->extend('updateIsURLFieldValidationEnabled', $enabled); - return $enabled; - } - - public function isCustomLinkTextEnabled() - { - $enabled = (bool) $this->config()->get('enable_custom_link_text'); - $this->extend('updateIsCustomLinkTextEnabled', $enabled); - return $enabled; - } - - public function getCMSFields() - { - $fields = FieldList::create( - $rootTabSet = TabSet::create('Root') - ); - - $isCustomLinkTextEnabled = $this->isCustomLinkTextEnabled(); - if ($isCustomLinkTextEnabled) { - $customLinkTextField = TextField::create( - 'CustomLinkText', - _t(__CLASS__.'.CustomLinkText', 'Link Text') - ); - if (!$this->isInDB()) { - $customLinkTextField->setDescription(_t(__CLASS__.'.OptionalWillBeGenerated', 'Optional. Will be auto-generated if left blank.')); - } - if ($this->generateLinkText()) { - $customLinkTextField->setAttribute('placeholder', $this->generateLinkText()); - } - } - - if ($this->config()->get('enable_tabs')) { - - $mainTabSet = TabSet::create('Main'); - - $linkFields = $this->getLinkFields()->toArray(); - $hasLinkFields = ($linkFields && count($linkFields) > 0); - if ($hasLinkFields) { - $targetTab = Tab::create('SuperLinkTargetTab', _t(__CLASS__.'.TabTarget', 'Target')); - if ($isCustomLinkTextEnabled) { - $targetTab->push($customLinkTextField); - } - foreach ($linkFields as $field) { - $targetTab->push($field); - } - $mainTabSet->push($targetTab); - } - - $behaviourFields = $this->getBehaviourFields()->toArray(); - $hasBehaviourFields = ($behaviourFields && count($behaviourFields) > 0); - if ($hasBehaviourFields) { - $behaviourTab = Tab::create('SuperLinkBehaviourTab', _t(__CLASS__.'.TabBehaviour', 'Behaviour')); - if (!$hasBehaviourFields && $isCustomLinkTextEnabled) { - $behaviourTab->push($customLinkTextField); - } - foreach ($behaviourFields as $field) { - $behaviourTab->push($field); - } - $mainTabSet->push($behaviourTab); - } - - if ($hasLinkFields || $hasBehaviourFields) { - $rootTabSet->push($mainTabSet); - } - } - else { - - $mainTab = Tab::create('Main'); - - if ($isCustomLinkTextEnabled) { - $mainTab->push($customLinkTextField); - } - - foreach ($this->getLinkFields()->toArray() as $field) { - $mainTab->push($field); - } - - foreach ($this->getBehaviourFields()->toArray() as $field) { - $mainTab->push($field); - } - - $rootTabSet->push($mainTab); - } - - $this->extend('updateCMSFields', $fields); - return $fields; - } - - public function getLinkFields() - { - $fields = FieldList::create( - TextField::create('URL', _t(__CLASS__.'.URL', 'URL')) - ); - - $this->extend('updateLinkFields', $fields); - return $fields; - } - - public function getBehaviourFields() - { - $fields = FieldList::create( - FieldGroup::create( - _t(__CLASS__.'.NewWindow', 'New Window'), - CheckboxField::create( - 'DoOpenNewWindow', - _t(__CLASS__.'.DoOpenNewWindow', 'Open link in a new window') - ) - ), - FieldGroup::create( - 'SEO', - CheckboxField::create( - 'DoNoFollow', - _t(__CLASS__.'.DoNoFollow', 'Instruct search engines not to follow this link') - ) - ) - ); - $this->extend('updateBehaviourFields', $fields); - return $fields; - } - - public function validate() - { - $result = ValidationResult::create(); - - if ($this->isURLFieldValidationEnabled()) { - if (!$this->URL) { - $result->addFieldError('URL', _t(__CLASS__.'.URLRequired', 'You must provide a URL')); - } else if (!filter_var($this->URL, FILTER_VALIDATE_URL)) { - $result->addFieldError('URL', _t(__CLASS__.'.URLInvalid', 'You must provide a valid URL')); - } - } - - $this->extend('updateValidate', $result); - return $result; - } - - public function saveURL($value) - { - $parts = parse_url($value); - - if (isset($parts['fragment'])) { - if ($this->isAnchorAllowed()) { - $this->Anchor = $parts['fragment']; - } - unset($parts['fragment']); - } - - if (isset($parts['query'])) { - if ($this->isQueryStringAllowed()) { - $this->QueryString = $parts['query']; - } - unset($parts['query']); - } - - $this->URL = rtrim(http_build_url($parts), '/'); - - $this->extend('updateSaveURL', $value); - } - - public function onBeforeWrite() - { - parent::onBeforeWrite(); - - if (!$this->isAnchorAllowed()) { - $this->Anchor = null; - } - if (!$this->isQueryStringAllowed()) { - $this->QueryString = null; - } - } - - public function getLinkTarget() - { - $target = $this->dbObject('URL'); - $this->extend('updateLinkTarget', $target); - return $target; - } - - public function getMultiAddTitle() - { - $title = $this->config()->get('multi_add_title'); - $this->extend('updateMultiAddTitle', $title); - return $title; - } - - public function getGlobalAnchors() - { - $anchors = null; - - $isGlobalAnchorsEnabled = ModuleLoader::inst() - ->getManifest() - ->moduleExists('fromholdio/silverstripe-globalanchors'); - if ($isGlobalAnchorsEnabled) { - $anchors = GlobalAnchors::get_anchors(); - } - $this->extend('updateGlobalAnchors', $anchors); - return $anchors; - } - - public function getGlobalAnchor($key) - { - $anchors = $this->getGlobalAnchors(); - if (!$anchors) return null; - if (!isset($anchors[$key])) return null; - return $anchors[$key]; - } - - public function forTemplate() - { - return; - } } diff --git a/src/Model/SuperLinkTrait.php b/src/Model/SuperLinkTrait.php new file mode 100644 index 0000000..0b94bd6 --- /dev/null +++ b/src/Model/SuperLinkTrait.php @@ -0,0 +1,714 @@ + true, + 'open_in_new' => true, + 'no_follow' => true + ]; + + private static $linking_mode_default = 'link'; + private static $linking_mode_section = 'section'; + private static $linking_mode_current = 'current'; + + private static $link_type_field_class = DropdownField::class; + private static $link_type_attr_name = 'data-superlinker-type'; + + private static $db = [ + 'LinkText' => 'Varchar', + 'LinkType' => 'Varchar(20)', + 'DoOpenInNew' => 'Boolean', + 'DoNoFollow' => 'Boolean' + ]; + + private static $casting = [ + 'AttributesHTML' => 'HTMLFragment', + 'getAttributesHTML' => 'HTMLFragment', + ]; + + + /** + * Link text / title + * ---------------------------------------------------- + */ + + public function getTitle(): string + { + if ($this->isLinkTextEnabled()) { + $title = $this->getField('LinkText'); + $this->extend('updateTitle', $title); + } + if (empty($title)) { + $title = $this->getDefaultTitle(); + } + return $title; + } + + public function getDefaultTitle(): string + { + $title = null; + $this->extend('updateDefaultTitle', $title); + return empty($title) + ? $this->getTypeLabel() ?? _t(__CLASS__ . '.NotConfigured', 'Not configured') + : $title; + } + + public function isLinkTextEnabled(?string $type = null): bool + { + if (empty($type)) $type = $this->getType(); + return $this->isTypeSettingEnabled('link_text', $type); + } + + + /** + * Link health checks + * ---------------------------------------------------- + */ + + public static function excludeInvalidLinks(SS_List $links): ArrayList + { + $list = ArrayList::create(); + foreach ($links as $link) { + if ($link->isLinkValid()) $list->push($link); + } + return $list; + } + + public function isLinkValid(): bool + { + $isValid = !empty($this->getType()) + && $this->isTypeValid() + && $this->isTypeAvailable() + && !$this->isLinkOrphaned() + && !$this->isLinkEmpty(); + $this->extend('updateIsLinkValid', $isValid); + return $isValid; + } + + public function isLinkOrphaned(): bool + { +// $container = $this->getContainerObject(); +// $isOrphaned = (bool) $container?->exists(); + $isOrphaned = false; + $this->extend('updateIsLinkOrphaned', $isOrphaned); + return $isOrphaned; + } + + public function isLinkEmpty(): bool + { + $isEmpty = empty($this->getURL()); + $this->extend('updateIsLinkEmpty', $isEmpty); + return $isEmpty; + } + + + /** + * Link Types + * ---------------------------------------------------- + */ + + public function getType(): ?string + { + return $this->getField('LinkType'); + } + + public function getTypeLabel(?string $type = null): ?string + { + if (empty($type)) $type = $this->getType(); + if (empty($type)) return null; + return $this->getTypeConfigValue('label', $type); + } + + protected function isTypeValid(?string $type = null): bool + { + if (empty($type)) $type = $this->getType(); + if (empty($type)) return false; + return in_array($type, $this->getAllTypes()); + } + + protected function isTypeAvailable(?string $type = null): bool + { + if (empty($type)) $type = $this->getType(); + if (empty($type)) return false; + return in_array($type, $this->getAvailableTypes()); + } + + protected function getAvailableTypes(bool $isLabelRequired = true): array + { + $availableTypes = []; + $allTypes = $this->getAllTypes(); + + $allowedTypes = $this->getAllowedTypes(); + if (empty($allowedTypes)) { + $availableTypes = array_combine($allTypes, $allTypes); + } + else { + foreach ($allTypes as $allType) { + if (in_array($allType, $allowedTypes)) { + $availableTypes[$allType] = $allType; + } + } + } + + $disallowedTypes = $this->getDisallowedTypes(); + if (!empty($disallowedTypes)) { + foreach ($disallowedTypes as $disallowedType) { + unset($availableTypes[$disallowedType]); + } + } + + $this->extend('updateAvailableTypes', $availableTypes); + + if ($isLabelRequired) { + foreach ($availableTypes as $availableType) { + if (empty($this->getTypeLabel($availableType))) { + unset($availableTypes[$availableType]); + } + } + } + + return array_values($availableTypes); + } + + protected function getAllTypes(): array + { + $types = static::config()->get('types') ?? []; + $types = array_keys(array_filter($types)); + return $this->doSortTypes($types); + } + + protected function getAllowedTypes(): array + { + $types = static::config()->get('allowed_types') ?? []; + $this->extend('updateAllowedTypes', $types); + return $types; + } + + protected function getDisallowedTypes(): array + { + $types = static::config()->get('disallowed_types') ?? []; + $this->extend('updateDisallowedTypes', $types); + return $types; + } + + protected function doSortTypes(array $types): array + { + $sorterFn = function($typeA, $typeB) { + $sortA = $this->getTypeConfigValue('sort', $typeA); + $sortB = $this->getTypeConfigValue('sort', $typeB); + if ($sortA === $sortB) { + return 0; + } + return ($sortA < $sortB) ? -1 : 1; + }; + usort($types, $sorterFn); + return $types; + } + + + /** + * Link element HTML attributes + * ---------------------------------------------------- + */ + + public function getDefaultAttributes(): array + { + $attrs = [ + 'href' => $this->getHrefValue(), + 'target' => $this->getTargetValue(), + 'rel' => $this->getRelValue(), + 'class' => $this->getClassValue() + ]; + $type = $this->getType(); + $typeAttrName = static::config()->get('link_type_attr_name'); + if (!empty($typeAttrName)) { + $attrs[$typeAttrName] = Convert::raw2att($type); + } + $this->extend('updateDefaultAttributes', $attrs); + return array_filter($attrs); + } + + + /** + * URL / Href attr + * ---------------------------------------------------- + */ + + public function getHrefValue(): ?string + { + $href = $this->getURL(); + $this->extend('updateHrefValue', $href); + return $href; + } + + public function getURL(): ?string + { + $url = null; + $this->extend('updateURL', $url); + return $url; + } + + public function getAbsoluteURL(): ?string + { + $url = $this->getURL(); + if (!empty($url) && Director::is_relative_url($url)) { + $url = Director::absoluteURL($url); + } + $this->extend('updateAbsoluteURL', $url); + return $url; + } + + + /** + * Target attr + * ---------------------------------------------------- + */ + + public function getTargetValue(): ?string + { + $target = null; + if ($this->isOpenInNew()) { + $target = '_blank'; + } + $this->extend('updateTargetValue', $target); + return $target; + } + + public function isOpenInNewEnabled(?string $type = null): bool + { + if (empty($type)) $type = $this->getType(); + return $this->isTypeSettingEnabled('open_in_new', $type); + } + + public function isOpenInNew(): bool + { + $do = $this->isOpenInNewEnabled() && $this->getField('DoOpenInNew'); + $this->extend('updateIsOpenInNew', $do); + return $do; + } + + + /** + * Rel attr + * ---------------------------------------------------- + */ + + public function getRelValue(): ?string + { + $parts = $this->getRelValueParts(); + return empty($parts) ? null : implode(' ', $parts); + } + + protected function getRelValueParts(): array + { + $relParts = []; + if ($this->isNoFollow()) { + $relParts[] = 'nofollow'; + } + if ($this->isNoOpener()) { + $relParts[] = 'noopener'; + } + $this->extend('updateRelValueParts', $relParts); + return $relParts; + } + + public function isNoFollowEnabled(?string $type = null): bool + { + if (empty($type)) $type = $this->getType(); + return $this->isTypeSettingEnabled('no_follow', $type); + } + + public function isNoFollow(): bool + { + $do = $this->isNoFollowEnabled() && $this->getField('DoNoFollow'); + $this->extend('updateIsNoFollow', $do); + return $do; + } + + public function isNoOpener(): bool + { + $url = $this->getURL(); + $do = !empty($url) && !Director::is_site_url($url); + $this->extend('updateIsNoOpener', $do); + return $do; + } + + + /** + * Class attr + * ---------------------------------------------------- + */ + + protected array $extraCSSClasses = []; + + public function getClassValue(): ?string + { + $value = implode(' ', $this->extraCSSClasses); + $this->extend('updateClassValue', $value); + return $value; + } + + public function addExtraCSSClass(string $class): self + { + $newClasses = explode(' ', $class); + foreach ($newClasses as $newClass) { + $this->extraCSSClasses[$newClass] = $newClass; + } + return $this; + } + + public function removeExtraCSSClass(string $class): self + { + $removeClasses = explode(' ', $class); + foreach ($removeClasses as $removeClass) { + unset($this->extraCSSClasses[$removeClass]); + } + return $this; + } + + + /** + * Template helpers + * ---------------------------------------------------- + */ + + public function isCurrent(): bool + { + $isCurrent = false; + $this->extend('updateIsCurrent', $isCurrent); + return $isCurrent; + } + + public function isSection(): bool + { + $isSection = false; + $this->extend('updateIsSection', $isSection); + return $isSection; + } + + public function LinkOrCurrent(): string + { + return $this->isCurrent() + ? static::config()->get('linking_mode_current') + : static::config()->get('linking_mode_default'); + } + + public function LinkOrSection(): string + { + return $this->isSection() + ? static::config()->get('linking_mode_section') + : static::config()->get('linking_mode_default'); + } + + public function LinkingMode(): string + { + return $this->isCurrent() + ? static::config()->get('linking_mode_current') + : $this->LinkOrSection(); + } + + + /** + * Rendering + * ---------------------------------------------------- + */ + + public function forTemplate(): DBHTMLText + { + $html = $this->isLinkValid() + ? $this->renderWith($this->getRenderTemplates()) + : ''; + $this->extend('updateForTemplate', $html); + return $html; + } + + protected function getRenderTemplates(?string $suffix = null): array + { + $classes = ClassInfo::ancestry($this->getField('ClassName')); + $classes = array_reverse($classes); + $baseClass = self::class; + + $type = $this->getType() ?? ''; + if (!empty($type)) $type = '_' . $type; + $templates = []; + foreach ($classes as $key => $class) { + if (!empty($type)) { + $templates[$class][] = $class . $type . $suffix; + } + $templates[$class][] = $class . $suffix; + if ($class === $baseClass) { + break; + } + } + + $this->extend('updateRenderTemplates', $templates, $suffix); + return $templates; + } + + + /** + * Data processing and validation methods + * ---------------------------------------------------- + */ + + + + /** + * Link Type config values & settings booleans + * ---------------------------------------------------- + */ + + public function getTypeConfigValue(string $key, string $type): mixed + { + return $this->getTypeConfigData($type)[$key] ?? null; + } + + public function getTypeConfigData(string $type): array + { + return static::config()->get('types')[$type] ?? []; + } + + public function isSettingEnabled(string $key): bool + { + return static::config()->get('settings')[$key] ?? false; + } + + public function isTypeSettingEnabled(string $key, ?string $type): bool + { + $isEnabled = $this->isSettingEnabled($key); + if ($isEnabled && !empty($type)) { + $isTypeEnabled = $this->getTypeConfigValue('settings', $type)[$key] ?? null; + if ($isTypeEnabled === false) { + $isEnabled = false; + } + } + return $isEnabled; + } + + public function getTypesByEnabledSetting(string $key, bool $onlyAvailableTypes = true): array + { + $resultTypes = []; + $types = $onlyAvailableTypes ? $this->getAvailableTypes() : $this->getAllTypes(); + foreach ($types as $type) { + $resultTypes[$type] = $this->isTypeSettingEnabled($key, $type); + } + return array_keys(array_filter($resultTypes)); + } + + + /** + * CMS Fields + * ---------------------------------------------------- + */ + + public function getCMSFields(): FieldList + { + $linkFieldsWrapper = Wrapper::create( + $this->getCMSLinkFields() + ); + $linkFieldsWrapper->setName('LinkMainFieldsWrapper'); + + $fields = FieldList::create( + TabSet::create( + 'Root', + $tab = Tab::create('Main', $linkFieldsWrapper) + ) + ); + $tab->setTitle(_t(__CLASS__ . '.MainTab', 'Main')); + + $this->extend('updateCMSFields', $fields); + return $fields; + } + + public function getCMSLinkFields(string $fieldPrefix = ''): FieldList + { + $fields = FieldList::create(); + + $linkTypeField = $this->getLinkTypeField($fieldPrefix); + if (empty($linkTypeField)) return $fields; + $fields->push($linkTypeField); + + $linkTextTypes = $this->getTypesByEnabledSetting('link_text'); + if (!empty($linkTextTypes)) + { + $linkTextField = TextField::create( + $fieldPrefix . 'LinkText', + _t(__CLASS__ . '.LinkText', 'Text') + ); + $linkTextField->setDescription( + _t(__CLASS__ . '.OptionalAutoGenerated', 'Optional. Will be auto-generated from link if left blank.') + ); + if ($this->isInDB()) { + $linkTextField->setAttribute( + 'placeholder', + $this->getDefaultTitle() + ); + } + $this->applySettingFieldDisplayLogic($linkTextField, $linkTextTypes, $fieldPrefix); + $fields->push($linkTextField); + } + + $this->extend('updateCMSLinkFieldsBeforeTypes', $fields, $fieldPrefix); + + $types = $this->getAvailableTypes(); + foreach ($types as $type) { + $typeFields = $this->getCMSLinkTypeFields($type, $fieldPrefix); + if ($typeFields->count() < 1) continue; + $typeWrapper = Wrapper::create($typeFields); + $typeWrapper->setName($fieldPrefix . 'TypeWrapper_' . $type); + $typeWrapper->hideUnless($fieldPrefix. 'LinkType')->isEqualTo($type); + $fields->push($typeWrapper); + } + + $optionsGroup = FieldGroup::create(); + $optionsGroup->setTitle(_t(__CLASS__ . '.OptionsGroup', 'Options')); + $optionsGroup->setName($fieldPrefix . 'OptionsGroup'); + $optionsGroupTypes = []; + + $this->extend('updateCMSLinkFieldsAfterTypes', $fields, $fieldPrefix); + + $openNewTypes = $this->getTypesByEnabledSetting('open_in_new'); + if (!empty($openNewTypes)) + { + $openNewField = CheckboxField::create( + $fieldPrefix . 'DoOpenInNew', + _t(__CLASS__ . '.DoOpenInNew', 'Open in new tab') + ); + $this->applySettingFieldDisplayLogic( + $openNewField, + $openNewTypes, + $fieldPrefix + ); + $optionsGroup->push($openNewField); + $optionsGroupTypes += $openNewTypes; + } + + $noFollowTypes = $this->getTypesByEnabledSetting('no_follow'); + if (!empty($noFollowTypes)) + { + $noFollowField = CheckboxField::create( + $fieldPrefix . 'DoNoFollow', + _t(__CLASS__ . '.DoNoFollow', 'Ask search engines to ignore') + ); + $this->applySettingFieldDisplayLogic( + $noFollowField, + $noFollowTypes, + $fieldPrefix + ); + $optionsGroup->push($noFollowField); + $optionsGroupTypes += $noFollowTypes; + } + + if ($optionsGroup->FieldList()->count() > 0) + { + $optionsGroupWrapper = Wrapper::create($optionsGroup); + $this->applySettingFieldDisplayLogic( + $optionsGroupWrapper, + $optionsGroupTypes, + $fieldPrefix + ); + $fields->push($optionsGroupWrapper); + } + + $this->extend('updateCMSLinkFields', $fields, $fieldPrefix); + return $fields; + } + + protected function getCMSLinkTypeFields(string $type, string $fieldPrefix = ''): FieldList + { + $fields = FieldList::create(); + $this->extend('updateCMSLinkTypeFields', $fields, $type, $fieldPrefix); + return $fields; + } + + protected function getLinkTypeField(string $fieldPrefix = ''): ?SingleSelectField + { + $field = null; + $class = $this->getLinkTypeFieldClassName(); + if (is_a($class, SingleSelectField::class, true)) + { + $source = []; + $types = $this->getAvailableTypes(); + foreach ($types as $type) { + $source[$type] = $this->getTypeLabel($type); + } + $this->extend('updateLinkTypeFieldSource', $source); + if (!empty($source)) + { + $field = $class::create( + $fieldPrefix . 'LinkType', + _t(__CLASS__ . '.LinkType', 'Type'), + $source + ); + if (is_a($class, DropdownField::class, true)) + { + $field->setHasEmptyDefault(true); + $field->setEmptyString('-- ' . _t(__CLASS__ . '.SelectLinkType', 'Select link type') . ' --'); + } + } + } + $this->extend('updateLinkTypeField', $field, $class, $source, $fieldPrefix); + return $field; + } + + protected function getLinkTypeFieldClassName(): string + { + $class = static::config()->get('link_type_field_class'); + $this->extend('updateLinkTypeFieldClassName', $class); + return $class; + } + + public function applySettingFieldDisplayLogic( + FormField $field, + array $types, + string $fieldPrefix = '' + ): void + { + foreach ($types as $type) { + $criteria = empty($criteria) + ? $field->displayIf($fieldPrefix . 'LinkType')->isEqualTo($type) + : $criteria->orIf($fieldPrefix . 'LinkType')->isEqualTo($type); + } + if (!empty($criteria)) $criteria->end(); + } + + + /** + * Permissions + * ---------------------------------------------------- + */ +} diff --git a/src/Model/VersionedSuperLink.php b/src/Model/VersionedSuperLink.php new file mode 100644 index 0000000..772225b --- /dev/null +++ b/src/Model/VersionedSuperLink.php @@ -0,0 +1,17 @@ +$Title