{include file='header' pageTitle='wcf.acp.menu.item.'|concat:$action}
<script data-relocate="true">
- //<![CDATA[
- $(function() {
- var $isInternalLink = $('input[name=isInternalLink]').filter('[value=1]');
- var $pageIDContainer = $('#pageIDContainer');
- var $externalURLContainer = $('#externalURLContainer');
+ require(['Dictionary', 'WoltLab/WCF/Acp/Ui/Menu/Item/Handler'], function(Dictionary, AcpUiMenuItemHandler) {
+ var handlers = new Dictionary();
+ {foreach from=$pageHandlers key=handlerPageID item=requireObjectID}
+ handlers.set({@$handlerPageID}, {if $requireObjectID}true{else}false{/if});
+ {/foreach}
- function handleIsInternalLink() {
- if ($isInternalLink.is(':checked')) {
- $pageIDContainer.show();
- $externalURLContainer.hide();
- }
- else {
- $pageIDContainer.hide();
- $externalURLContainer.show();
- }
- }
-
- $('input[name=isInternalLink]').change(handleIsInternalLink);
- handleIsInternalLink();
+ AcpUiMenuItemHandler.init(handlers);
});
- //]]>
</script>
<header class="contentHeader">
</dd>
</dl>
- <dl id="pageIDContainer"{if $errorField == 'pageID'} class="formError"{/if}>
+ <dl id="pageIDContainer"{if $errorField == 'pageID'} class="formError"{/if}{if !$isInternalLink} style="display: none;"{/if}>
<dt><label for="pageID">{lang}wcf.acp.page.parentPageID{/lang}</label></dt>
<dd>
<select name="pageID" id="pageID">
</dd>
</dl>
- <dl id="externalURLContainer"{if $errorField == 'externalURL'} class="formError"{/if}>
+ <dl id="pageObjectIDContainer"{if $errorField == 'pageObjectID'} class="formError"{/if}{if !$pageID || !$pageHandler[$pageID]|isset} style="display: none;"{/if}>
+ <dt><label for="pageObjectID">{lang}wcf.acp.page.pageObjectID{/lang}</label></dt>
+ <dd>
+ <div class="inputAddon">
+ <input type="text" id="pageObjectID" name="pageObjectID" value="{$pageObjectID}" class="short">
+ <a href="#" id="searchPageObjectID" class="inputSuffix button jsTooltip" title="TODO: Search"><span class="icon icon16 fa-search"></span></a>
+ </div>
+ {if $errorField == 'pageObjectID'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.menu.item.pageObjectID.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl id="externalURLContainer"{if $errorField == 'externalURL'} class="formError"{/if}{if $isInternalLink} style="display: none;"{/if}>
<dt><label for="externalURL">{lang}wcf.acp.menu.item.externalURL{/lang}</label></dt>
<dd>
<input type="text" name="externalURL" id="externalURL" value="{$externalURL}" class="long" />
--- /dev/null
+/**
+ * Provides the interface logic to add and edit menu items.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/Acp/Ui/Menu/Item/Handler
+ */
+define(['Dictionary', 'WoltLab/Wcf/Ui/Page/Search/Handler'], function(Dictionary, UiPageSearchHandler) {
+ "use strict";
+
+ var _activePageId = 0;
+ var _cache;
+ var _containerExternalLink;
+ var _containerInternalLink;
+ var _containerPageObjectId = null;
+ var _handlers;
+ var _pageId;
+ var _pageObjectId;
+
+ /**
+ * @exports WoltLab/WCF/Acp/Ui/Menu/Item/Handler
+ */
+ return {
+ /**
+ * Initializes the interface logic.
+ *
+ * @param {Dictionary} handlers list of handlers by page id supporting page object ids
+ */
+ init: function(handlers) {
+ _handlers = handlers;
+
+ _containerInternalLink = elById('pageIDContainer');
+ _containerExternalLink = elById('externalURLContainer');
+ _containerPageObjectId = elById('pageObjectIDContainer');
+
+ elBySelAll('input[name="isInternalLink"]', null, (function(input) {
+ input.addEventListener('change', this._toggleIsInternalLink.bind(this, input.value));
+
+ if (input.checked) {
+ this._toggleIsInternalLink(input.value);
+ }
+ }).bind(this));
+
+ if (_handlers.size) {
+ _pageId = elById('pageID');
+ _pageId.addEventListener('change', this._togglePageId.bind(this));
+
+ _pageObjectId = elById('pageObjectID');
+
+ _cache = new Dictionary();
+ _activePageId = ~~_pageId.value;
+ if (_activePageId && _handlers.has(_activePageId)) {
+ _cache.set(_activePageId, ~~_pageObjectId.value);
+ }
+
+ elById('searchPageObjectID').addEventListener(WCF_CLICK_EVENT, this._openSearch.bind(this));
+
+ // toggle page object id container on init
+ if (_handlers.has(~~_pageId.value)) {
+ elShow(_containerPageObjectId);
+ }
+ }
+ },
+
+ /**
+ * Toggles between the interface for internal and external links.
+ *
+ * @param {string} value selected option value
+ * @protected
+ */
+ _toggleIsInternalLink: function(value) {
+ window[(~~value ? 'elShow' : 'elHide')](_containerInternalLink);
+ window[(~~value ? 'elHide' : 'elShow')](_containerExternalLink);
+ },
+
+ /**
+ * Handles the changed page selection.
+ *
+ * @protected
+ */
+ _togglePageId: function() {
+ if (_handlers.has(_activePageId)) {
+ _cache.set(_activePageId, ~~_pageObjectId.value);
+ }
+
+ _activePageId = ~~_pageId.value;
+
+ // page w/o pageObjectID support, discard value
+ if (!_handlers.has(_activePageId)) {
+ _pageObjectId.value = '';
+
+ elHide(_containerPageObjectId);
+
+ return;
+ }
+
+ var newValue = ~~_cache.get(_activePageId);
+ _pageObjectId.value = (newValue) ? newValue : '';
+
+ elShow(_containerPageObjectId);
+ },
+
+ /**
+ * Opens the handler lookup dialog.
+ *
+ * @param {Event} event event object
+ * @protected
+ */
+ _openSearch: function(event) {
+ event.preventDefault();
+
+ UiPageSearchHandler.open(_activePageId, 'foo', function(objectId) {
+ _pageObjectId.value = objectId;
+ _cache.set(_activePageId, objectId);
+ });
+ }
+ };
+});
/**
* @exports WoltLab/WCF/StringUtil
*/
- var StringUtil = {
+ return {
/**
* Adds thousands separators to a given number.
*
* @see http://stackoverflow.com/a/6502556/782822
- * @param {*} number
+ * @param {?} number
* @return {String}
*/
addThousandsSeparator: function(number) {
/**
* Escapes special HTML-characters within a string
*
- * @param {*} string
+ * @param {?} string
* @return {String}
*/
escapeHTML: function (string) {
* Escapes a String to work with RegExp.
*
* @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25
- * @param {*} string
+ * @param {?} string
* @return {String}
*/
escapeRegExp: function(string) {
/**
* Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands separators.
*
- * @param {*} number
+ * @param {?} number
* @param {int} decimalPlaces The number of decimal places to leave after rounding.
* @return {String}
*/
/**
* Makes a string's first character lowercase.
*
- * @param {*} string
+ * @param {?} string
* @return {String}
*/
lcfirst: function(string) {
/**
* Makes a string's first character uppercase.
*
- * @param {*} string
+ * @param {?} string
* @return {String}
*/
ucfirst: function(string) {
/**
* Unescapes special HTML-characters within a string.
*
- * @param {*} string
+ * @param {?} string
* @return {String}
*/
unescapeHTML: function (string) {
return String(string).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
}
};
-
- return StringUtil;
});
--- /dev/null
+/**
+ * Provides access to the lookup function of page handlers, allowing the user to search and
+ * select page object ids.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/Ui/Page/Search/Handler
+ */
+define(['StringUtil', 'Dom/Util', 'Ui/Dialog', './Input'], function(StringUtil, DomUtil, UiDialog, UiPageSearchInput) {
+ "use strict";
+
+ var _callback = null;
+ var _searchInput = null;
+ var _searchInputHandler = null;
+ var _resultList = null;
+ var _resultListContainer = null;
+
+ /**
+ * @exports WoltLab/WCF/Ui/Page/Search/Handler
+ */
+ return {
+ /**
+ * Opens the lookup overlay for provided page id.
+ *
+ * @param {int} pageId page id
+ * @param {string} title dialog title
+ * @param {function} callback callback function provided with the user-selected object id
+ */
+ open: function (pageId, title, callback) {
+ _callback = callback;
+
+ UiDialog.open(this);
+ UiDialog.setTitle(this, title);
+
+ this._getSearchInputHandler().setPageId(pageId);
+ },
+
+ /**
+ * Builds the result list.
+ *
+ * @param {Object} data AJAX response data
+ * @protected
+ */
+ _buildList: function(data) {
+ this._resetList();
+
+ // no matches
+ if (!Array.isArray(data.returnValues) || data.returnValues.length === 0) {
+ var innerError = elCreate('small');
+ innerError.className = 'innerError';
+ innerError.textContent = 'TODO: no matches';
+ DomUtil.insertAfter(innerError, _searchInput);
+
+ return;
+ }
+
+ var image, item, listItem;
+ for (var i = 0, length = data.returnValues.length; i < length; i++) {
+ item = data.returnValues[i];
+ image = item.image;
+ if (/^fa-/.test(image)) {
+ image = '<span class="icon icon48 ' + image + '"></span>';
+ }
+
+ listItem = elCreate('li');
+ elData(listItem, 'object-id', item.objectID);
+
+ listItem.innerHTML = '<div class="box48">'
+ + image
+ + '<div>'
+ + '<div class="containerHeadline">'
+ + '<h3><a href="' + StringUtil.escapeHTML(item.link) + '">' + StringUtil.escapeHTML(item.title) + '</a></h3>'
+ + (item.description ? '<p>' + item.description + '</p>' : '')
+ + '</div>'
+ + '</div>'
+ + '</div>';
+
+ listItem.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+
+ _resultList.appendChild(listItem);
+ }
+
+ elShow(_resultListContainer);
+ },
+
+ /**
+ * Resets the list and removes any error elements.
+ *
+ * @protected
+ */
+ _resetList: function() {
+ var innerError = _searchInput.nextElementSibling;
+ if (innerError && innerError.classList.contains('innerError')) elRemove(innerError);
+
+ _resultList.innerHTML = '';
+
+ elHide(_resultListContainer);
+ },
+
+ /**
+ * Initializes the search input handler and returns the instance.
+ *
+ * @returns {UiPageSearchInput} search input handler
+ * @protected
+ */
+ _getSearchInputHandler: function() {
+ if (_searchInputHandler === null) {
+ var callback = this._buildList.bind(this);
+ _searchInputHandler = new UiPageSearchInput(elById('wcfUiPageSearchInput'), {
+ callbackSuccess: callback
+ });
+ }
+
+ return _searchInputHandler;
+ },
+
+ /**
+ * Handles clicks on the item unless the click occured directly on a link.
+ *
+ * @param {Event} event event object
+ * @protected
+ */
+ _click: function(event) {
+ if (event.target.nodeName === 'A') {
+ return;
+ }
+
+ event.stopPropagation();
+
+ _callback(elData(event.currentTarget, 'object-id'));
+ UiDialog.close(this);
+ },
+
+ _dialogSetup: function() {
+ return {
+ id: 'wcfUiPageSearchHandler',
+ options: {
+ onShow: function() {
+ if (_searchInput === null) {
+ _searchInput = elById('wcfUiPageSearchInput');
+ _resultList = elById('wcfUiPageSearchResultList');
+ _resultListContainer = elById('wcfUiPageSearchResultListContainer');
+ }
+
+ // clear search input
+ _searchInput.value = '';
+
+ // reset results
+ elHide(_resultListContainer);
+ _resultList.innerHTML = '';
+
+ _searchInput.focus();
+ },
+ title: ''
+ },
+ source: '<div class="section">'
+ + '<dl>'
+ + '<dt><label for="wcfUiPageSearchInput">TODO: Search terms</label></dt>'
+ + '<dd>'
+ + '<input type="text" id="wcfUiPageSearchInput" class="long">'
+ + '<small>TODO: Enter at least 3 characters to search</small>'
+ + '</dd>'
+ + '</dl>'
+ + '</div>'
+ + '<section id="wcfUiPageSearchResultListContainer" class="section sectionContainerList">'
+ + '<header class="sectionHeader">'
+ + '<h2 class="sectionTitle">TODO: results</h2>'
+ + '<small class="sectionDescription">TODO: click on a result to select it</small>'
+ + '</header>'
+ + '<ul id="wcfUiPageSearchResultList" class="containerList wcfUiPageSearchResultList"></ul>'
+ + '</section>'
+ };
+ }
+ };
+});
--- /dev/null
+/**
+ * Suggestions for page object ids with external response data processing.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/Ui/Page/Search/Input
+ * @extends module:WoltLab/WCF/Ui/Search/Input
+ */
+define(['Core', 'WoltLab/WCF/Ui/Search/Input'], function(Core, UiSearchInput) {
+ "use strict";
+
+ /**
+ * @param {Element} element input element
+ * @param {Object=} options search options and settings
+ * @constructor
+ */
+ function UiPageSearchInput(element, options) { this.init(element, options); }
+ Core.inherit(UiPageSearchInput, UiSearchInput, {
+ init: function(element, options) {
+ options = Core.extend({
+ ajax: {
+ className: 'wcf\\data\\page\\PageAction'
+ },
+ callbackSuccess: null
+ }, options);
+
+ if (typeof options.callbackSuccess !== 'function') {
+ throw new Error("Expected a valid callback function for 'callbackSuccess'.");
+ }
+
+ UiPageSearchInput._super.prototype.init.call(this, element, options);
+
+ this._pageId = 0;
+ },
+
+ /**
+ * Sets the target page id.
+ *
+ * @param {int} pageId target page id
+ */
+ setPageId: function(pageId) {
+ this._pageId = pageId;
+ },
+
+ _getParameters: function(value) {
+ var data = UiPageSearchInput._super.prototype._getParameters.call(this, value);
+
+ data.objectIDs = [this._pageId];
+
+ return data;
+ },
+
+ _ajaxSuccess: function(data) {
+ this._options.callbackSuccess(data);
+ }
+ });
+
+ return UiPageSearchInput;
+});
this._request.abortPrevious();
}
- this._request = Ajax.api(this, {
+ this._request = Ajax.api(this, this._getParameters(value));
+ },
+
+ /**
+ * Returns additional AJAX parameters.
+ *
+ * @param {string} value search string
+ * @return {Object} additional AJAX parameters
+ * @protected
+ */
+ _getParameters: function(value) {
+ return {
parameters: {
data: {
searchString: value
}
}
- });
+ };
},
/**
use wcf\system\exception\IllegalLinkException;
use wcf\system\exception\UserInputException;
use wcf\system\language\I18nHandler;
+use wcf\system\page\handler\ILookupPageHandler;
use wcf\system\WCF;
use wcf\util\StringUtil;
*/
public $isInternalLink = true;
+ /**
+ * list of page handlers by page id
+ * @var \wcf\system\page\handler\IMenuPageHandler[]
+ */
+ public $pageHandlers = [];
+
/**
* page id
* @var integer
*/
public $pageID = null;
+ /**
+ * page object id
+ * @var integer
+ */
+ public $pageObjectID = null;
+
/**
* menu item title
* @var string
*/
public $menuItems = null;
+ /**
+ * nested list of page nodes
+ * @var \RecursiveIteratorIterator
+ */
+ public $pageNodeList;
+
/**
* @inheritDoc
*/
I18nHandler::getInstance()->register('title');
I18nHandler::getInstance()->register('externalURL');
+
+ $this->pageNodeList = (new PageNodeTree())->getNodeList();
+
+ // fetch page handlers
+ foreach ($this->pageNodeList as $pageNode) {
+ $handler = $pageNode->getPage()->getHandler();
+ if ($handler !== null) {
+ if ($handler instanceof ILookupPageHandler) {
+ $this->pageHandlers[$pageNode->getPage()->pageID] = $pageNode->getPage()->requireObjectID;
+ }
+ }
+ }
}
/**
$this->isInternalLink = false;
if (isset($_POST['isInternalLink'])) $this->isInternalLink = (bool) $_POST['isInternalLink'];
if (!empty($_POST['pageID'])) $this->pageID = intval($_POST['pageID']);
+ if (!empty($_POST['pageObjectID'])) $this->pageObjectID = intval($_POST['pageObjectID']);
if (!empty($_POST['parentItemID'])) $this->parentItemID = intval($_POST['parentItemID']);
if (isset($_POST['showOrder'])) $this->showOrder = intval($_POST['showOrder']);
}
if (!$page->pageID) {
throw new UserInputException('pageID', 'invalid');
}
+
+ // validate page object id
+ if (isset($this->pageHandlers[$page->pageID])) {
+ if ($this->pageHandlers[$page->pageID] && !$this->pageObjectID) {
+ throw new UserInputException('pageObjectID');
+ }
+
+ /** @var ILookupPageHandler $handler */
+ $handler = $page->getHandler();
+ if ($this->pageObjectID && !$handler->isValid($this->pageObjectID)) {
+ throw new UserInputException('pageObjectID', 'invalid');
+ }
+ }
}
else {
- $this->pageID = null;
+ $this->pageID = $this->pageObjectID = null;
// validate external url
if (!I18nHandler::getInstance()->validateValue('externalURL')) {
'isDisabled' => ($this->isDisabled) ? 1 : 0,
'title' => $this->title,
'pageID' => $this->pageID,
+ 'pageObjectID' => ($this->pageObjectID ?: 0),
'externalURL' => $this->externalURL,
'menuID' => $this->menuID,
'parentItemID' => $this->parentItemID,
// reset variables
$this->isDisabled = $this->isInternalLink = false;
- $this->pageID = $this->parentItemID = null;
+ $this->pageID = $this->pageObjectID = $this->parentItemID = null;
$this->externalURL = $this->title = '';
$this->showOrder = 0;
'isDisabled' => $this->isDisabled,
'isInternalLink' => $this->isInternalLink,
'pageID' => $this->pageID,
+ 'pageObjectID' => $this->pageObjectID,
'title' => $this->title,
'externalURL' => $this->externalURL,
'parentItemID' => $this->parentItemID,
'showOrder' => $this->showOrder,
'menuItemNodeList' => $this->menuItems->getNodeList(),
- 'pageNodeList' => (new PageNodeTree())->getNodeList()
+ 'pageNodeList' => $this->pageNodeList,
+ 'pageHandlers' => $this->pageHandlers
]);
}
}
*/
protected static $databaseTableIndexName = 'pageID';
+ /**
+ * @var \wcf\system\page\handler\IMenuPageHandler
+ */
+ protected $pageHandler;
+
/**
* Returns true if the active user can delete this page.
*
}
}
+ /**
+ * Returns the associated page handler or null.
+ *
+ * @return \wcf\system\page\handler\IMenuPageHandler|null
+ */
+ public function getHandler() {
+ if ($this->handler) {
+ $this->pageHandler = new $this->handler();
+ }
+
+ return $this->pageHandler;
+ }
+
/**
* Returns the page's internal name.
*
<?php
namespace wcf\data\page;
use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\ISearchAction;
use wcf\data\IToggleAction;
use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\UserInputException;
+use wcf\system\page\handler\ILookupPageHandler;
use wcf\system\WCF;
/**
* @category Community Framework
* @since 2.2
*/
-class PageAction extends AbstractDatabaseObjectAction implements IToggleAction {
+class PageAction extends AbstractDatabaseObjectAction implements ISearchAction, IToggleAction {
/**
* @inheritDoc
*/
protected $className = PageEditor::class;
+ /**
+ * @var PageEditor
+ */
+ protected $pageEditor;
+
/**
* @inheritDoc
*/
- protected $permissionsCreate = array('admin.content.cms.canManagePage');
+ protected $permissionsCreate = ['admin.content.cms.canManagePage'];
/**
* @inheritDoc
*/
- protected $permissionsDelete = array('admin.content.cms.canManagePage');
+ protected $permissionsDelete = ['admin.content.cms.canManagePage'];
/**
* @inheritDoc
*/
- protected $permissionsUpdate = array('admin.content.cms.canManagePage');
+ protected $permissionsUpdate = ['admin.content.cms.canManagePage'];
/**
* @inheritDoc
*/
- protected $requireACP = array('create', 'delete', 'toggle', 'update');
+ protected $requireACP = ['create', 'delete', 'toggle', 'update'];
/**
* @inheritDoc
$statement = WCF::getDB()->prepareStatement($sql);
foreach ($this->parameters['content'] as $languageID => $content) {
- $statement->execute(array(
+ $statement->execute([
$page->pageID,
($languageID ?: null),
$content['title'],
$content['metaDescription'],
$content['metaKeywords'],
$content['customURL']
- ));
+ ]);
}
}
$insertStatement = WCF::getDB()->prepareStatement($sql);
foreach ($this->objects as $page) {
- $deleteStatement->execute(array($page->pageID));
+ $deleteStatement->execute([$page->pageID]);
foreach ($this->parameters['content'] as $languageID => $content) {
- $insertStatement->execute(array(
+ $insertStatement->execute([
$page->pageID,
($languageID ?: null),
$content['title'],
$content['metaDescription'],
$content['metaKeywords'],
$content['customURL']
- ));
+ ]);
}
}
}
-
- return $page;
}
/**
*/
public function toggle() {
foreach ($this->objects as $object) {
- $object->update(array('isDisabled' => ($object->isDisabled) ? 0 : 1));
+ $object->update(['isDisabled' => ($object->isDisabled) ? 0 : 1]);
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function validateGetSearchResultList() {
+ $this->pageEditor = $this->getSingleObject();
+ if ($this->pageEditor->getHandler() === null || !($this->pageEditor->getHandler() instanceof ILookupPageHandler)) {
+ throw new UserInputException('objectIDs');
+ }
+
+ $this->readString('searchString', false, 'data');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getSearchResultList() {
+ return $this->pageEditor->getHandler()->lookup($this->parameters['data']['searchString']);
+ }
}
*/
public function getLink($objectID);
+ /**
+ * Returns true if provided object id exists and is valid.
+ *
+ * @param integer $objectID page object id
+ * @return boolean true if object id is valid
+ */
+ public function isValid($objectID);
+
/**
* Performs a search for pages using a query string, returning an array containing
* an `objectID => title` relation.