<nav class="contentHeaderNavigation">
<ul>
- {if $action == 'edit' && !$page->requireObjectID}
- <li><a href="{$page->getLink()}" class="button"><span class="icon icon16 fa-search"></span> <span>{lang}wcf.acp.page.button.viewPage{/lang}</span></a></li>
+ {if $action == 'edit'}
+ {if !$page->requireObjectID}
+ <li><a href="{$page->getLink()}" class="button"><span class="icon icon16 fa-search"></span> <span>{lang}wcf.acp.page.button.viewPage{/lang}</span></a></li>
+ {/if}
+
+ <li><a href="{link controller='PageBoxOrder' id=$page->pageID}{/link}" class="button"><span class="icon icon16 fa-sort-amount-asc"></span> <span>{lang}wcf.acp.page.button.boxOrder{/lang}</span></a></li>
{/if}
<li><a href="{link controller='PageList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.cms.page.list{/lang}</span></a></li>
<div id="boxes" class="tabMenuContent">
<div class="section">
+ <p class="info">{lang}wcf.acp.page.boxOrder.page{@$action|ucfirst}{/lang}</p>
+
<dl{if $errorField == 'boxIDs'} class="formError"{/if}>
<dt>{lang}wcf.acp.page.boxes{/lang}</dt>
<dd>
--- /dev/null
+{include file='header' pageTitle='wcf.acp.page.boxOrder'}
+
+<style>
+ #pbo [data-placeholder] {
+ background-color: rgb(224, 224, 224);
+ padding: 10px;
+ }
+</style>
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.page.boxOrder{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='LabelAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.label.add{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+<div class="section" id="pbo">
+ <div data-placeholder="hero"></div>
+ <div class="pbo-quad" data-placeholder="headerBoxes"></div>
+ <div data-placeholder="top"></div>
+
+ <div class="pbo-main">
+ <div data-placeholder="sidebarLeft"></div>
+ <div>
+ <div data-placeholder="contentTop"></div>
+ <div class="pbo-content">{lang}wcf.acp.page.boxOrder.position.content{/lang}</div>
+ <div data-placeholder="contentBottom"></div>
+ </div>
+ <div data-placeholder="sidebarRight"></div>
+ </div>
+
+ <div data-placeholder="bottom"></div>
+ <div class="pbo-quad" data-placeholder="footerBoxes"></div>
+ <div data-placeholder="footer"></div>
+</div>
+
+{include file='footer'}
--- /dev/null
+define(['Ajax', 'Language', 'Ui/Confirmation', 'Ui/Notification'], function (Ajax, Language, UiConfirmation, UiNotification) {
+ "use strict";
+
+ var _pageId = 0;
+ var _pbo = elById('pbo');
+
+ return {
+ init: function (pageId, boxes) {
+ _pageId = pageId;
+
+ boxes.forEach(function(boxData, position) {
+ var container = elCreate('ul');
+ boxData.forEach(function(box) {
+ var item = elCreate('li');
+ elData(item, 'box-id', box.boxID);
+ item.innerHTML = box.name;
+
+ container.appendChild(item);
+ });
+
+ if (boxData.length > 1) {
+ window.jQuery(container).sortable({
+ opacity: .6,
+ placeholder: 'sortablePlaceholder'
+ });
+ }
+
+ elBySel('[data-placeholder="' + position + '"]', _pbo).appendChild(container);
+ });
+
+ elBySel('button[data-type="submit"]').addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
+
+ var buttonDiscard = elBySel('.jsButtonCustomShowOrder');
+ if (buttonDiscard) buttonDiscard.addEventListener(WCF_CLICK_EVENT, this._discard.bind(this));
+ },
+
+ _save: function (event) {
+ event.preventDefault();
+
+ var data = {};
+
+ // collect data
+ elBySelAll('[data-placeholder]', _pbo, function (position) {
+ var boxes = [];
+ elBySelAll('li', position, function (li) {
+ var id = ~~elData(li, 'box-id');
+ if (id) boxes.push(id);
+ });
+
+ data[elData(position, 'placeholder')] = boxes;
+ });
+
+ Ajax.api(this, {
+ parameters: {
+ position: data
+ }
+ });
+ },
+
+ _discard: function (event) {
+ event.preventDefault();
+
+ UiConfirmation.show({
+ confirm: (function () {
+ Ajax.api(this, {
+ actionName: 'resetPosition'
+ });
+ }).bind(this),
+ message: Language.get('wcf.acp.page.boxOrder.discard.confirmMessage')
+ })
+ },
+
+ _ajaxSuccess: function (data) {
+ switch (data.actionName) {
+ case 'updatePosition':
+ UiNotification.show();
+ break;
+
+ case 'resetPosition':
+ UiNotification.show(undefined, function () {
+ window.location.reload();
+ });
+ break;
+ }
+ },
+
+ _ajaxSetup: function () {
+ return {
+ data: {
+ actionName: 'updatePosition',
+ className: 'wcf\\data\\page\\PageAction',
+ interfaceName: 'wcf\\data\\ISortableAction',
+ objectIDs: [_pageId]
+ }
+ };
+ }
+ };
+});
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\acp\page;
+use wcf\data\box\Box;
+use wcf\data\page\Page;
+use wcf\data\page\PageCache;
+use wcf\page\AbstractPage;
+use wcf\system\box\BoxHandler;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the list of boxes for selected page and offers sorting capabilities.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Page
+ * @since 3.0
+ */
+class PageBoxOrderPage extends AbstractPage {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.cms.page.list';
+
+ /**
+ * list of boxes by position
+ * @var Box[][]
+ */
+ public $boxes;
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.content.cms.canManagePage'];
+
+ /**
+ * page object
+ * @var Page
+ */
+ public $page;
+
+ /**
+ * page id
+ * @var integer
+ */
+ public $pageID = 0;
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (!empty($_REQUEST['id'])) $this->pageID = intval($_REQUEST['id']);
+
+ $this->page = PageCache::getInstance()->getPage($this->pageID);
+ if (!$this->page->pageID) {
+ throw new IllegalLinkException();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ $this->boxes = BoxHandler::loadBoxes($this->pageID, false);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'boxes' => $this->boxes,
+ 'hasCustomShowOrder' => BoxHandler::hasCustomShowOrder($this->pageID),
+ 'page' => $this->page,
+ 'pageID' => $this->pageID
+ ]);
+ }
+}
*/
protected $linkPage;
+ /**
+ * virtual show order of this box
+ * @var integer
+ */
+ public $virtualShowOrder = -1;
+
/**
* @inheritDoc
*/
return SimpleAclResolver::getInstance()->canAccess('com.woltlab.wcf.box', $this->boxID);
}
+ /**
+ * Sets the virtual show order of this box.
+ *
+ * @param integer $virtualShowOrder
+ */
+ public function setVirtualShowOrder($virtualShowOrder) {
+ $this->virtualShowOrder = $virtualShowOrder;
+ }
+
/**
* Returns the box with the given identifier.
*
<?php
namespace wcf\data\page;
+use wcf\data\box\Box;
+use wcf\data\ISortableAction;
use wcf\data\page\content\PageContent;
use wcf\data\page\content\PageContentEditor;
use wcf\data\AbstractDatabaseObjectAction;
use wcf\data\ISearchAction;
use wcf\data\IToggleAction;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
use wcf\system\exception\PermissionDeniedException;
use wcf\system\exception\UserInputException;
use wcf\system\html\simple\HtmlSimpleParser;
* @method PageEditor[] getObjects()
* @method PageEditor getSingleObject()
*/
-class PageAction extends AbstractDatabaseObjectAction implements ISearchAction, IToggleAction {
+class PageAction extends AbstractDatabaseObjectAction implements ISearchAction, ISortableAction, IToggleAction {
/**
* @inheritDoc
*/
/**
* @inheritDoc
*/
- protected $requireACP = ['create', 'delete', 'getSearchResultList', 'search', 'toggle', 'update'];
+ protected $requireACP = ['create', 'delete', 'getSearchResultList', 'resetPosition', 'search', 'toggle', 'update', 'updatePosition'];
/**
* @inheritDoc
SearchIndexManager::getInstance()->delete('com.woltlab.wcf.page', $pageContentIDs);
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function validateUpdatePosition() {
+ WCF::getSession()->checkPermissions(['admin.content.cms.canManagePage']);
+
+ $this->pageEditor = $this->getSingleObject();
+
+ if (empty($this->parameters['position']) || !is_array($this->parameters['position'])) {
+ throw new UserInputException('position');
+ }
+
+ $seenBoxIDs = [];
+ foreach ($this->parameters['position'] as $position => $boxIDs) {
+ // validate each position for both existence and the supplied box ids
+ if (!in_array($position, Box::$availablePositions) || !is_array($boxIDs)) {
+ throw new UserInputException('position');
+ }
+
+ foreach ($boxIDs as $boxID) {
+ // check for duplicate box ids
+ if (in_array($boxID, $seenBoxIDs)) {
+ throw new UserInputException('position');
+ }
+
+ $seenBoxIDs[] = $boxID;
+ }
+ }
+
+ // validates box ids
+ $conditions = new PreparedStatementConditionBuilder();
+ $conditions->add("boxID IN (?)", [$seenBoxIDs]);
+
+ $sql = "SELECT boxID
+ FROM wcf".WCF_N."_box
+ ".$conditions;
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditions->getParameters());
+ $validBoxIDs = [];
+ while ($boxID = $statement->fetchColumn()) {
+ $validBoxIDs[] = $boxID;
+ }
+
+ foreach ($seenBoxIDs as $boxID) {
+ if (!in_array($boxID, $validBoxIDs)) {
+ throw new UserInputException('position');
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function updatePosition() {
+ $pageID = $this->pageEditor->getDecoratedObject()->pageID;
+
+ $sql = "DELETE FROM wcf".WCF_N."_page_box_order
+ WHERE pageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$pageID]);
+
+ $sql = "INSERT INTO wcf".WCF_N."_page_box_order
+ (pageID, boxID, showOrder)
+ VALUES (?, ?, ?)";
+ $statement = WCF::getDB()->prepareStatement($sql);
+
+ WCF::getDB()->beginTransaction();
+ foreach ($this->parameters['position'] as $boxIDs) {
+ for ($i = 0, $length = count($boxIDs); $i < $length; $i++) {
+ $statement->execute([
+ $pageID,
+ $boxIDs[$i],
+ $i
+ ]);
+ }
+ }
+ WCF::getDB()->commitTransaction();
+ }
+
+ /**
+ * Validates parameters to reset the custom box positions for provided page.
+ */
+ public function validateResetPosition() {
+ WCF::getSession()->checkPermissions(['admin.content.cms.canManagePage']);
+
+ $this->pageEditor = $this->getSingleObject();
+ }
+
+ /**
+ * Resets the custom box positions for provided page.
+ */
+ public function resetPosition() {
+ $sql = "DELETE FROM wcf".WCF_N."_page_box_order
+ WHERE pageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->pageEditor->getDecoratedObject()->pageID]);
+ }
}
}
}
- // load box layout for active page
- $boxList = new BoxList();
- $boxList->enableContentLoading();
- if ($pageID) $boxList->getConditionBuilder()->add('(box.visibleEverywhere = ? AND boxID NOT IN (SELECT boxID FROM wcf'.WCF_N.'_box_to_page WHERE pageID = ? AND visible = ?)) OR boxID IN (SELECT boxID FROM wcf'.WCF_N.'_box_to_page WHERE pageID = ? AND visible = ?)', [1, $pageID, 0, $pageID, 1]);
- else $boxList->getConditionBuilder()->add('box.visibleEverywhere = ?', [1]);
- $boxList->sqlOrderBy = 'showOrder';
- $boxList->readObjects();
-
- $this->boxes = $boxList->getObjects();
- foreach ($boxList as $box) {
- if ($box->isAccessible()) {
- if (!isset($this->boxesByPosition[$box->position])) $this->boxesByPosition[$box->position] = [];
- $this->boxesByPosition[$box->position][] = $box;
-
+ $this->boxesByPosition = self::loadBoxes($pageID, true);
+ foreach ($this->boxesByPosition as $boxes) {
+ foreach ($boxes as $box) {
+ $this->boxes[$box->boxID] = $box;
$this->boxesByIdentifier[$box->identifier] = $box;
}
}
public static function disablePageLayout() {
self::$disablePageLayout = true;
}
+
+ /**
+ * Returns the list of boxes sorted by their global and page-local show order.
+ *
+ * @param integer $pageID page id
+ * @param boolean $forDisplay enables content loading and removes inaccessible boxes from view
+ * @return Box[][]
+ */
+ public static function loadBoxes($pageID, $forDisplay) {
+ // load box layout for active page
+ $boxList = new BoxList();
+ if ($pageID) $boxList->getConditionBuilder()->add('(box.visibleEverywhere = ? AND boxID NOT IN (SELECT boxID FROM wcf'.WCF_N.'_box_to_page WHERE pageID = ? AND visible = ?)) OR boxID IN (SELECT boxID FROM wcf'.WCF_N.'_box_to_page WHERE pageID = ? AND visible = ?)', [1, $pageID, 0, $pageID, 1]);
+ else $boxList->getConditionBuilder()->add('box.visibleEverywhere = ?', [1]);
+
+ if ($forDisplay) $boxList->enableContentLoading();
+
+ $boxList->readObjects();
+
+ $showOrders = [];
+ if ($pageID) {
+ $sql = "SELECT boxID, showOrder
+ FROM wcf" . WCF_N . "_page_box_order
+ WHERE pageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$pageID]);
+ while ($row = $statement->fetchArray()) {
+ $showOrders[$row['boxID']] = $row['showOrder'];
+ }
+ }
+
+ $boxes = [];
+ foreach ($boxList as $box) {
+ if (!$forDisplay || $box->isAccessible()) {
+ $virtualShowOrder = (isset($showOrders[$box->boxID])) ? $showOrders[$box->boxID] : ($box->showOrder + 1000);
+ $box->setVirtualShowOrder($virtualShowOrder);
+
+ if (!isset($boxes[$box->position])) $boxes[$box->position] = [];
+ $boxes[$box->position][] = $box;
+ }
+ }
+
+ foreach ($boxes as &$positionBoxes) {
+ usort($positionBoxes, function($a, $b) {
+ if ($a->virtualShowOrder == $b->virtualShowOrder) {
+ return 0;
+ }
+
+ return ($a->virtualShowOrder < $b->virtualShowOrder) ? -1 : 1;
+ });
+ }
+ unset($positionBoxes);
+
+ return $boxes;
+ }
+
+ /**
+ * Returns true if provided page id uses a custom box show order.
+ *
+ * @param integer $pageID page id
+ * @return boolean true if there is a custom show order for boxes
+ */
+ public static function hasCustomShowOrder($pageID) {
+ $sql = "SELECT COUNT(*) AS count
+ FROM wcf".WCF_N."_page_box_order
+ WHERE pageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$pageID]);
+
+ return $statement->fetchColumn() > 0;
+ }
}
<item name="wcf.acp.page.add"><![CDATA[Seite hinzufügen]]></item>
<item name="wcf.acp.page.application"><![CDATA[App]]></item>
<item name="wcf.acp.page.boxes"><![CDATA[Ausgewählte Boxen auf dieser Seite anzeigen]]></item>
+ <item name="wcf.acp.page.boxOrder"><![CDATA[Boxen sortieren]]></item>
+ <item name="wcf.acp.page.boxOrder.discard"><![CDATA[Sortierung verwerfen]]></item>
+ <item name="wcf.acp.page.boxOrder.discard.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} individuelle Sortierung der Boxen für diese Seite wirklich löschen?]]></item>
+ <item name="wcf.acp.page.boxOrder.pageAdd"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Du kannst{else}Sie können{/if} die individuelle Sortierung der Boxen für diese Seite nach dem Speichern festlegen.]]></item>
+ <item name="wcf.acp.page.boxOrder.pageEdit"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Du kannst{else}Sie können{/if} die Sortierung der angezeigten Boxen <a href="{link controller='PageBoxOrder' id=$page->pageID}{/link}">individuell festlegen</a>, bitte {if LANGUAGE_USE_INFORMAL_VARIANT}speichere{else}speichern Sie{/if} zuvor {if LANGUAGE_USE_INFORMAL_VARIANT}deine{else}Ihre{/if} Änderungen.]]></item>
+ <item name="wcf.acp.page.boxOrder.position.content"><![CDATA[(Inhalt)]]></item>
+ <item name="wcf.acp.page.button.boxOrder"><![CDATA[Boxen sortieren]]></item>
<item name="wcf.acp.page.button.viewPage"><![CDATA[Vorschau anzeigen]]></item>
<item name="wcf.acp.page.content"><![CDATA[Inhalt]]></item>
<item name="wcf.acp.page.contents"><![CDATA[Inhalte]]></item>
<item name="wcf.acp.page.add"><![CDATA[Add Page]]></item>
<item name="wcf.acp.page.application"><![CDATA[App]]></item>
<item name="wcf.acp.page.boxes"><![CDATA[Display the Selected Boxes on This Page]]></item>
+ <item name="wcf.acp.page.boxOrder"><![CDATA[Sort Boxes]]></item>
+ <item name="wcf.acp.page.boxOrder.discard"><![CDATA[Discard Sorting]]></item>
+ <item name="wcf.acp.page.boxOrder.discard.confirmMessage"><![CDATA[Do you really want to discard the invidiual box sorting for this page?]]></item>
+ <item name="wcf.acp.page.boxOrder.pageAdd"><![CDATA[You can set an individual sort order for boxes after saving.]]></item>
+ <item name="wcf.acp.page.boxOrder.pageEdit"><![CDATA[You can set an individual <a href="{link controller='PageBoxOrder' id=$page->pageID}{/link}">sort order for boxes</a>, please save your changes before continuing.]]></item>
+ <item name="wcf.acp.page.boxOrder.position.content"><![CDATA[(Content)]]></item>
+ <item name="wcf.acp.page.button.boxOrder"><![CDATA[Sort Boxes]]></item>
<item name="wcf.acp.page.button.viewPage"><![CDATA[Show Preview]]></item>
<item name="wcf.acp.page.content"><![CDATA[Content]]></item>
<item name="wcf.acp.page.contents"><![CDATA[Contents]]></item>
options TEXT NULL
);
+DROP TABLE IF EXISTS wcf1_page_box_order;
+CREATE TABLE wcf1_page_box_order (
+ pageID INT(10) NOT NULL,
+ boxID INT(10) NOT NULL,
+ showOrder INT(10) NOT NULL DEFAULT 0,
+ UNIQUE KEY pageToBox (pageID, boxID)
+);
+
DROP TABLE IF EXISTS wcf1_page_content;
CREATE TABLE wcf1_page_content (
pageContentID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
ALTER TABLE wcf1_page ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
ALTER TABLE wcf1_page ADD FOREIGN KEY (applicationPackageID) REFERENCES wcf1_package (packageID) ON DELETE SET NULL;
+ALTER TABLE wcf1_page_box_order ADD FOREIGN KEY (pageID) REFERENCES wcf1_page (pageID) ON DELETE CASCADE;
+ALTER TABLE wcf1_page_box_order ADD FOREIGN KEY (boxID) REFERENCES wcf1_box (boxID) ON DELETE CASCADE;
+
ALTER TABLE wcf1_page_content ADD FOREIGN KEY (pageID) REFERENCES wcf1_page (pageID) ON DELETE CASCADE;
ALTER TABLE wcf1_page_content ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE CASCADE;