'Page Edit Link', 'summary' => 'Provides a link capability as used by some Fieldtype modules (like rich text editors).', 'version' => 112, 'permanent' => true, 'permission' => 'page-edit', 'icon' => 'link', ); } /** * URL type: Absolute path from root (no relative paths) * */ const urlTypeAbsolute = 0; /** * URL type: Relative path in same branch only * */ const urlTypeRelativeBranch = 1; /** * URL type: Relative path always * */ const urlTypeRelativeAll = 2; /** * @var Page|null * */ protected $page = null; /** * The "choose page" start label * * @var string * */ protected $startLabel = ''; /** * Language ID * * @var int * */ protected $langID = 0; /** * Get default configuration settings * * @return array * */ public static function getDefaultSettings() { return array( 'classOptions' => "", 'relOptions' => "nofollow", 'targetOptions' => "_blank", 'urlType' => self::urlTypeAbsolute, 'extLinkRel' => '', 'extLinkTarget' => '', 'extLinkClass' => '', 'noLinkTextEdit' => 0, ); } /** * Construct * */ public function __construct() { parent::__construct(); foreach(self::getDefaultSettings() as $key => $value) { parent::set($key, $value); } } /** * Setup for execute methods * */ public function setup() { $sanitizer = $this->wire()->sanitizer; $modules = $this->wire()->modules; $pages = $this->wire()->pages; $input = $this->wire()->input; /** @var ProcessPageList $pageList */ $pageList = $modules->get('ProcessPageList'); $pageList->renderReady(); $this->startLabel = $this->_('Choose page'); $id = (int) $input->get('id'); $this->langID = (int) $input->get('lang'); if($id) $this->page = $pages->get($id); if($this->page && $this->page->id && !$this->wire()->user->hasPermission("page-view", $this->page)) { throw new WireException("You don't have access to this page"); } if(!$this->page) $this->page = $pages->newNullPage(); $this->wire()->config->js('ProcessPageEditLink', array( 'selectStartLabel' => $this->startLabel, 'langID' => $this->langID, 'pageID' => $id, 'pageUrl' => $this->page->url, 'pageName' => $this->page->name, 'rootParentUrl' => $this->page->rootParent->url, 'slashUrls' => $this->page->template ? $this->page->template->slashUrls : 1, 'urlType' => $this->urlType, 'extLinkRel' => $sanitizer->names($this->extLinkRel), 'extLinkTarget' => $this->extLinkTarget, 'extLinkClass' => $sanitizer->names($this->extLinkClass), 'noLinkTextEdit' => (int) $this->noLinkTextEdit )); } /** * Set * * @param string $key * @param string|int|array $value * @return self * */ public function set($key, $value) { if($key === 'classOptions' || $key === 'relOptions' || $key === 'targetOptions') { $value = $this->sanitizeOptions($value); } else if($key === 'extLinkRel' || $key === 'extLinkClass') { $value = $this->wire()->sanitizer->htmlClasses($value); } else if($key === 'extLinkTarget') { $value = $this->wire()->sanitizer->htmlClass($value); } return parent::set($key, $value); } /** * Sanitize single option 'value', 'value=label', or 'value="label"' * * @param string $value * @return string * */ protected function sanitizeOption($value) { $sanitizer = $this->wire()->sanitizer; $value = trim($value); $plus = strpos($value, '+') === 0 ? '+' : ''; if($plus) $value = ltrim($value, '+'); if(strpos($value, '=') === false) return $plus . $sanitizer->htmlClasses($value); // value=label or value="label" list($value, $label) = explode('=', $value, 2); $value = trim($value); $label = trim($label); $value = $sanitizer->htmlClasses($value); if(!strlen($value)) return ''; $quote = strpos($label, '"') === 0 ? '"' : ''; $label = str_replace('"', '', $label); $label = $sanitizer->text($label); $value = strlen($label) ? "$plus$value=$quote$label$quote" : "$value"; return $value; } /** * Sanitize multiple newline separated options * * @param string $value * @return string * */ protected function sanitizeOptions($value) { $value = trim($value); if(!strlen($value)) return ''; if(strpos($value, "\n") === false) return $this->sanitizeOption($value); $lines = array(); foreach(explode("\n", $value) as $line) { $line = $this->sanitizeOption($line); if(strlen($line)) $lines[] = $line; } return implode("\n", $lines); } /** * Build the edit link form * * @param string Current href value $currentValue * @param string Current linked text $currentText * @since 3.0.217 * */ protected function ___buildForm($currentValue, $currentText) { $sanitizer = $this->wire()->sanitizer; $modules = $this->wire()->modules; $config = $this->wire()->config; $input = $this->wire()->input; /** @var InputfieldForm $form */ $form = $modules->get("InputfieldForm"); $form->attr('id', 'ProcessPageEditLinkForm'); $modules->get('JqueryWireTabs'); /** @var InputfieldWrapper $fieldset */ $fieldset = $this->wire(new InputfieldWrapper()); $fieldset->attr('title', $this->_('Link')); $fieldset->addClass('WireTab'); $form->add($fieldset); if($this->noLinkTextEdit) { // link text editing disabled } else if($currentText) { /** @var InputfieldText $field */ $field = $modules->get("InputfieldText"); $field->label = $this->_('Link text'); $field->icon = 'pencil-square'; $field->attr('id+name', 'link_text'); $field->val($currentText); $fieldset->add($field); } /** @var InputfieldPageAutocomplete $field */ $field = $modules->get("InputfieldPageAutocomplete"); $field->label = $this->_('Link to URL'); $field->attr('id+name', 'link_page_url'); $field->icon = 'external-link-square'; $field->description = $this->_('Enter a URL, email address, anchor, or enter word(s) to find a page.'); $field->labelFieldName = 'url'; if($modules->isInstalled('PagePaths') && !$this->wire('languages')) { $field->searchFields = 'path title'; } else { $field->searchFields = 'name title'; } if($this->langID) $field->lang_id = $this->langID; $field->maxSelectedItems = 1; $field->useList = false; $field->allowAnyValue = true; $field->disableChars = '/:.#'; $field->useAndWords = true; $field->findPagesSelector = "has_parent!=" . $config->adminRootPageID . ", " . "id!=" . $config->http404PageID; if($currentValue) $field->attr('value', $currentValue); $fieldset->add($field); if(is_array($input->get('anchors'))) { $field->columnWidth = 60; /** @var InputfieldSelect $field */ $field = $modules->get('InputfieldSelect'); $field->columnWidth = 40; $field->attr('id+name', 'link_page_anchor'); $field->label = $this->_('Select Anchor'); $field->description = $this->_('Anchors found in the text you are editing.'); $field->icon = 'flag'; foreach($input->get->array('anchors') as $anchor) { $anchor = '#' . $sanitizer->text($anchor); if(strlen($anchor)) $field->addOption($anchor); if($currentValue && $currentValue == $anchor) $field->attr('value', $currentValue); } $fieldset->add($field); } /** @var InputfieldInteger $field */ $field = $modules->get('InputfieldInteger'); $field->attr('id+name', 'link_page_id'); $field->label = $this->_("Select Page"); $field->set('startLabel', $this->startLabel); $field->collapsed = Inputfield::collapsedYes; $field->icon = 'sitemap'; $fieldset->add($field); if($this->page->numChildren) { /** @var InputfieldInteger $field */ $field = $modules->get('InputfieldInteger'); $field->attr('id+name', 'child_page_id'); $field->label = $this->_("Select Child Page"); $field->description = $this->_('This is the same as "Select Page" above, but may quicker to use if linking to children of the current page.'); $field->set('startLabel', $this->startLabel); $field->collapsed = Inputfield::collapsedYes; $field->icon = 'sitemap'; $fieldset->append($field); } $fieldset->append($this->getFilesField()); /** @var InputfieldWrapper $fieldset */ $fieldset = $this->wire(new InputfieldWrapper()); $fieldset->attr('title', $this->_('Attributes')); $fieldset->attr('id', 'link_attributes'); $fieldset->addClass('WireTab'); $form->append($fieldset); /** @var InputfieldText $field */ $field = $modules->get('InputfieldText'); $field->attr('id+name', 'link_title'); $field->label = $this->_('Title'); $field->description = $this->_('Additional text to describe link.'); if($input->get('title')) { $field->attr('value', $sanitizer->unentities($sanitizer->text($input->get('title')))); } $fieldset->add($field); if($this->targetOptions) { /** @var InputfieldSelect $field */ $field = $modules->get('InputfieldSelect'); $field->attr('id+name', 'link_target'); $field->label = $this->_('Target'); $field->description = $this->_('Where this link will open.'); $this->addSelectOptions($field, 'target', $this->targetOptions); if($this->relOptions) $field->columnWidth = 50; $fieldset->add($field); if($this->extLinkTarget) { $options = $field->getOptions(); if(!isset($options[$this->extLinkTarget])) $field->addOption($this->extLinkTarget); } } if($this->relOptions || $this->extLinkRel) { /** @var InputfieldSelect $field */ $field = $modules->get('InputfieldSelect'); $field->attr('id+name', 'link_rel'); $field->label = $this->_('Rel'); $field->description = $this->_('Relationship of link to document.'); if($this->targetOptions) $field->columnWidth = 50; $this->addSelectOptions($field, 'rel', $this->relOptions); $fieldset->add($field); if($this->extLinkRel) { $options = $field->getOptions(); if(!isset($options[$this->extLinkRel])) $field->addOption($this->extLinkRel); } } $classOptions = $this->getClassOptions(); if($classOptions) { /** @var InputfieldCheckboxes $field */ $field = $modules->get('InputfieldCheckboxes'); $field->attr('id+name', 'link_class'); $field->label = $this->_('Class'); $field->description = $this->_('Additional classes that can affect the look or behavior of the link.'); $field->optionColumns = 1; $this->addSelectOptions($field, 'class', $classOptions); if($this->extLinkClass) { $options = $field->getOptions(); if(!isset($options[$this->extLinkClass])) $field->addOption($this->extLinkClass); } $fieldset->add($field); } if($this->wire()->user->isSuperuser()) $fieldset->notes = sprintf( $this->_('You may customize available attributes shown above in the %s module settings.'), "[ProcessPageEditLink](" . $config->urls->admin . "module/edit?name=ProcessPageEditLink)" ); return $form; } /** * Primary execute * * @return string * */ public function ___execute() { $sanitizer = $this->wire()->sanitizer; $input = $this->wire()->input; $this->setup(); if($input->get('href')) { $currentValue = $sanitizer->url($input->get('href'), array( 'stripQuotes' => false, 'allowIDN' => true, )); } else { $currentValue = ''; } $currentText = $input->get('text'); $currentText = $currentText === null ? '' : $this->wire()->sanitizer->text($currentText); $form = $this->buildForm($currentValue, $currentText); return $form->render() . "

"; } /** * Get class options string * * This gets class options specified with module and those specified in input.get[class]. * * @return string Newline separated string of class options * @since 3.0.212 * */ protected function getClassOptions() { $sanitizer = $this->wire()->sanitizer; $inputClass = $this->wire()->input->get->text('class'); if(empty($inputClass)) return $this->classOptions; $inputClass = $sanitizer->htmlClasses($inputClass, true); if(!count($inputClass)) return $this->classOptions; sort($inputClass); $inputClasses = $inputClass; $inputClass = implode(' ', $inputClass); $classOptions = array(); if($this->classOptions) { foreach(explode("\n", $this->classOptions) as $line) { $value = ltrim(trim($line), '+'); if(strpos($value, '=')) { list($value, /*$label*/) = explode('=', $value, 2); } if(strpos($value, ' ')) { $value = $sanitizer->htmlClasses($value, true); sort($value); $value = implode(' ', $value); } $classOptions[$value] = $line; } } if(isset($classOptions[$inputClass])) { // class already appears as-is, i.e. "uk-text-muted" or "uk-text-muted uk-text-small", etc. } else { // add new classes from input foreach($inputClasses as $class) { if(!isset($classOptions[$class])) $classOptions[$class] = $class; } } return count($classOptions) ? implode("\n", $classOptions) : ''; } /** * @param InputfieldSelect $field * @param $attrName * @param $optionsText * */ protected function addSelectOptions(InputfieldSelect $field, $attrName, $optionsText) { $input = $this->wire()->input; $isExisting = $input->get('href') != ''; $existingValueStr = $this->wire()->sanitizer->text($input->get($attrName)); $existingValueArray = strlen($existingValueStr) ? explode(' ', $existingValueStr) : array(); $values = array(); if($field instanceof InputfieldRadios) { $field->addOption('', $this->_('None')); } foreach(explode("\n", $optionsText) as $value) { $value = trim($value); $isDefault = strpos($value, '+') !== false; if($isDefault) $value = trim($value, '+'); $attr = array(); $value = trim($value, '+ '); $label = ''; if(strpos($value, '=') !== false) { list($value, $label) = explode('=', $value, 2); $value = trim($value); $label = trim($label); } else { if($value == '_blank') $label = $this->_('open in new window'); if($value == 'nofollow') $label = $this->_('tell search engines not to follow'); } if(strpos($label, '"') === 0 || strpos($label, "'") === 0) { $label = trim($label, "\"'"); } else if($label) { $label = "$value ($label)"; } else { $label = $value; } if(($isDefault && !$isExisting) || (in_array($value, $existingValueArray) || $existingValueStr === $value)) { if($field instanceof InputfieldCheckboxes) { $attr['checked'] = 'checked'; } else { $attr['selected'] = 'selected'; } } $field->addOption($value, $label, $attr); $values[] = $value; } } /** * Return JSON containing files list for ajax use * * @return string * @throws WireException * */ public function ___executeFiles() { $this->setup(); if(!$this->page->id) throw new WireException("A page id must be specified"); $files = $this->getFiles(); return wireEncodeJSON($files); } /** * Get array of info about files attached to given Page * * @return array Associative array of "/url/to/file.pdf" => "Field label: basename" * */ protected function getFiles() { $files = array(); $page = $this->page; // As the link generator might be called in a repeater, we need to find the containing page $n = 0; while(wireInstanceOf($page, 'RepeaterPage') && ++$n < 10) { /** @var RepeaterPage $page */ $page = $page->getForPage(); } if($page && $page->id) { $files = $this->getFilesPage($page); } asort($files); return $files; } /** * Get array of info about files attached to given Page, including any repeater items * * Hookable in 3.0.222+ only * * @param Page $page * @param string $prefix Optional prefix to prepend to "Field label:" portion of label * @return array Associative array of "/url/to/file.pdf" => "Field label: basename" * */ protected function ___getFilesPage(Page $page, $prefix = '') { $files = array(); foreach($page->template->fieldgroup as $field) { /** @var Fieldtype $type */ $type = $field->type; if($type instanceof FieldtypeFile) { $value = $page->get($field->name); if($value) foreach($page->get($field->name) as $file) { $files[$file->url] = $prefix . $field->getLabel() . ': ' . $file->basename; } } else if(wireInstanceOf($type, 'FieldtypeRepeater')) { $value = $page->get($field->name); if($value) { if($value instanceof Page) $value = array($value); if(WireArray::iterable($value)) { foreach($value as $repeaterPage) { $files = array_merge($this->getFilesPage($repeaterPage, $field->getLabel() . ': '), $files); } } } } } return $files; } /** * @return InputfieldSelect * */ protected function getFilesField() { /** @var InputfieldSelect $field */ $field = $this->wire()->modules->get("InputfieldSelect"); $field->label = $this->_("Select File"); $field->attr('id+name', 'link_page_file'); $files = $this->getFiles(); $field->addOption(''); $field->addOptions($files); $field->collapsed = Inputfield::collapsedYes; if($this->page && $this->page->id) $field->notes = $this->_('Showing files on page:') . ' **' . $this->page->url . '**'; $field->description = $this->_('Select the file from this page that you want to link to.') . ' ' . $this->_("To select a file from another page, click 'Select Page' above and choose the page you want to select a file from."); // Instruction on how to select a file from another page $field->icon = 'file-text-o'; return $field; } /** * Module configuration * * @param InputfieldWrapper $inputfields * */ public function getModuleConfigInputfields(InputfieldWrapper $inputfields) { $modules = $this->wire()->modules; /** @var InputfieldFieldset $fieldset */ $fieldset = $modules->get('InputfieldFieldset'); $fieldset->label = $this->_('Attribute options'); $fieldset->description = $this->_('Enter one of attribute `value`, `value=label`, or `value="label"` per line (see notes for details).') . ' ' . $this->_('The user will be able to select these as options when adding links.') . ' ' . $this->_('To make an option selected by default (for new links), precede the value with a plus “+”.'); $fieldset->detail = $this->_('To include labels, specify `value=label` to show **“value (label)”** for each selectable option.') . ' ' . $this->_('Or specify `value="label"` (label in quotes) to show just **“label”** (hiding the value) for each selectable option.'); $fieldset->icon = 'sliders'; /** @var InputfieldTextarea $f */ $f = $modules->get('InputfieldTextarea'); $f->attr('name', 'classOptions'); $f->label = 'class'; $f->attr('value', $this->classOptions); $f->columnWidth = 34; $fieldset->add($f); /** @var InputfieldTextarea $f */ $f = $modules->get('InputfieldTextarea'); $f->attr('name', 'relOptions'); $f->label = 'rel'; $f->attr('value', $this->relOptions); $f->columnWidth = 33; $fieldset->add($f); /** @var InputfieldTextarea $f */ $f = $modules->get('InputfieldTextarea'); $f->attr('name', 'targetOptions'); $f->label = 'target'; $f->attr('value', $this->targetOptions); $f->columnWidth = 33; $fieldset->add($f); $inputfields->add($fieldset); /** @var InputfieldFieldset $fieldset */ $fieldset = $modules->get('InputfieldFieldset'); $fieldset->label = $this->_('External link attributes'); $fieldset->description = $this->_('Specify the default selected attributes that will be automatically populated when an external link is detected.'); $fieldset->description .= ' ' . $this->_('If used, the value must be one you have predefined above.'); $fieldset->icon = 'external-link'; $fieldset->collapsed = Inputfield::collapsedBlank; /** @var InputfieldText $f */ $f = $modules->get('InputfieldText'); $f->attr('name', 'extLinkClass'); $f->label = 'class'; $f->attr('value', $this->extLinkClass); $f->required = false; $f->columnWidth = 34; $fieldset->add($f); /** @var InputfieldText $f */ $f = $modules->get('InputfieldText'); $f->attr('name', 'extLinkRel'); $f->notes = $this->_('Example: Specifying **nofollow** would make external links default to be not followed by search engines.'); $f->label = 'rel'; $f->required = false; $f->attr('value', $this->extLinkRel); $f->columnWidth = 33; $fieldset->add($f); /** @var InputfieldName $f */ $f = $modules->get('InputfieldName'); $f->attr('name', 'extLinkTarget'); $f->label = 'target'; $f->notes = $this->_('Example: Specifying **_blank** would make external links default to open in a new window.'); $f->attr('value', $this->extLinkTarget); $f->required = false; $f->columnWidth = 33; $fieldset->add($f); $inputfields->add($fieldset); /** @var InputfieldRadios $f */ $f = $modules->get('InputfieldRadios'); $f->attr('name', 'urlType'); $f->label = $this->_('URL type for page links'); $f->addOption(self::urlTypeAbsolute, $this->_('Full/absolute path from root (default)')); $f->addOption(self::urlTypeRelativeBranch, $this->_('Relative URLs in the same branches only') . '*'); $f->addOption(self::urlTypeRelativeAll, $this->_('Relative URLs always') . '*'); $f->attr('value', $this->urlType ? $this->urlType : self::urlTypeAbsolute); $f->notes = $this->_('*Currently experimental'); $f->collapsed = Inputfield::collapsedYes; $inputfields->add($f); /** @var InputfieldCheckbox $f */ $f = $modules->get('InputfieldCheckbox'); $f->attr('name', 'noLinkTextEdit'); $f->label = $this->_('Disable link text edit feature?'); $f->description = $this->_('Disables the “Edit Link Text” feature, enabling you to support links that can contain existing markup.'); if($this->noLinkTextEdit) { $f->attr('checked', 'checked'); } else { $f->collapsed = Inputfield::collapsedYes; } $inputfields->add($f); } }