Added support for page bbcode / page lookup
authorAlexander Ebert <ebert@woltlab.com>
Fri, 8 Jul 2016 11:29:54 +0000 (13:29 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 8 Jul 2016 11:29:54 +0000 (13:29 +0200)
com.woltlab.wcf/bbcode.xml
com.woltlab.wcf/templates/wysiwyg.tpl
wcfsetup/install/files/acp/templates/__pageAddContent.tpl
wcfsetup/install/files/acp/templates/wysiwyg.tpl
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabPage.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Page.js [new file with mode: 0644]
wcfsetup/install/files/lib/data/page/PageAction.class.php
wcfsetup/install/files/lib/data/page/content/PageContent.class.php
wcfsetup/install/files/lib/system/bbcode/WoltLabSuitePageBBCode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/message/embedded/object/PageMessageEmbeddedObjectHandler.class.php

index a24543e7c951fb735a97ce3a85ddca48b348ebfc..90931ec09fcb23507c5bdd2d59a5431a33b37417 100644 (file)
                                <attribute name="0">
                                        <validationpattern>^\d+$</validationpattern>
                                        <required>1</required>
-                                       <usetext>1</usetext>
                                </attribute>
                                <attribute name="1">
                                        <validationpattern>^(small|medium|large|original)$</validationpattern>
                                <attribute name="0">
                                        <validationpattern>^\d+(,\d+)*$</validationpattern>
                                        <required>1</required>
-                                       <usetext>1</usetext>
+                               </attribute>
+                       </attributes>
+               </bbcode>
+               
+               <bbcode name="wsp">
+                       <classname><![CDATA[wcf\system\bbcode\WoltLabSuitePageBBCode]]></classname>
+                       <attributes>
+                               <attribute name="0">
+                                       <validationpattern>^\d+$</validationpattern>
+                                       <required>1</required>
                                </attribute>
                        </attributes>
                </bbcode>
index 3183d0b94c6d4184d185d8f66dce073d194b590f..fd668b69d8bc06a12835554a69dc665bbe94d3a1 100644 (file)
                                config.plugins.push('WoltLabMedia');
                        {/if}
                        
-                       // load the button plugin last to ensure all buttons have been initialized
-                       // already and we can safely add all icons
-                       config.plugins.push('WoltLabButton');
-                       
                        {if $__redactorConfig|isset}{@$__redactorConfig}{/if}
                        {assign var=$__redactorConfig value=''}
                        
                        {event name='redactorConfig'}
                        
+                       // load the button plugin last to ensure all buttons have been initialized
+                       // already and we can safely add all icons
+                       config.plugins.push('WoltLabButton');
+                       
                        $(element).redactor(config);
                });
        });
index 8cd7aa98f1bd7caa15bca7339148ffce489fdaca..3f4ae3d8c8cc5fcf334259e8d607439fb5fdc94e 100644 (file)
@@ -1,5 +1,23 @@
 <textarea name="content[{@$languageID}]" id="content{@$languageID}">{if !$content[$languageID]|empty}{$content[$languageID]}{/if}</textarea>
 {if $pageType == 'text'}
+       {capture append='__redactorJavaScript'}, '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabPage.js?v={@LAST_UPDATE_TIME}'{/capture}
+       {capture append='__redactorConfig'}
+               Language.addObject({
+                       'wcf.page.search': '{lang}wcf.page.search{/lang}',
+                       'wcf.page.search.error.tooShort': '{lang}wcf.page.search.error.tooShort{/lang}',
+                       'wcf.page.search.error.noResults': '{lang}wcf.page.search.error.noResults{/lang}',
+                       'wcf.page.search.name': '{lang}wcf.page.search.name{/lang}',
+                       'wcf.page.search.results': '{lang}wcf.page.search.results{/lang}',
+                       'wcf.page.search.results.description': '{lang}wcf.page.search.results.description{/lang}'
+               });
+               
+               buttonOptions.woltlabPage = { icon: 'fa-file-text-o', title: '{lang}wcf.editor.button.page{/lang}' };
+               
+               buttons.push('woltlabPage');
+               
+               config.plugins.push('WoltLabPage');
+       {/capture}
+       
        {include file='wysiwyg' wysiwygSelector='content'|concat:$languageID}
 {elseif $pageType == 'html'}
        {include file='codemirror' codemirrorMode='htmlmixed' codemirrorSelector='#content'|concat:$languageID}
index 3183d0b94c6d4184d185d8f66dce073d194b590f..fd668b69d8bc06a12835554a69dc665bbe94d3a1 100644 (file)
                                config.plugins.push('WoltLabMedia');
                        {/if}
                        
-                       // load the button plugin last to ensure all buttons have been initialized
-                       // already and we can safely add all icons
-                       config.plugins.push('WoltLabButton');
-                       
                        {if $__redactorConfig|isset}{@$__redactorConfig}{/if}
                        {assign var=$__redactorConfig value=''}
                        
                        {event name='redactorConfig'}
                        
+                       // load the button plugin last to ensure all buttons have been initialized
+                       // already and we can safely add all icons
+                       config.plugins.push('WoltLabButton');
+                       
                        $(element).redactor(config);
                });
        });
diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabPage.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabPage.js
new file mode 100644 (file)
index 0000000..52bbf91
--- /dev/null
@@ -0,0 +1,13 @@
+$.Redactor.prototype.WoltLabPage = function() {
+       "use strict";
+       
+       return {
+               init: function() {
+                       var button = this.button.add('woltlabPage', '');
+                       
+                       require(['WoltLab/WCF/Ui/Redactor/Page'], (function (UiRedactorPage) {
+                               new UiRedactorPage(this, button[0]);
+                       }).bind(this));
+               }
+       };
+};
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search.js
new file mode 100644 (file)
index 0000000..917a227
--- /dev/null
@@ -0,0 +1,130 @@
+define(['Ajax', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function(Ajax, EventKey, Language, StringUtil, DomUtil, UiDialog) {
+       "use strict";
+       
+       var _callbackSelect, _resultContainer, _resultList, _searchInput = null;
+       
+       return {
+               open: function(callbackSelect) {
+                       _callbackSelect = callbackSelect;
+                       
+                       UiDialog.open(this);
+               },
+               
+               _search: function (event) {
+                       event.preventDefault();
+                       
+                       var inputContainer = _searchInput.parentNode;
+                       var innerError = inputContainer.nextSibling;
+                       if (innerError && innerError.nodeName === 'SMALL') elRemove(innerError);
+                       
+                       var value = _searchInput.value.trim();
+                       if (value.length < 3) {
+                               innerError = elCreate('small');
+                               innerError.className = 'innerError';
+                               innerError.textContent = Language.get('wcf.page.search.error.tooShort');
+                               DomUtil.insertAfter(innerError, inputContainer);
+                               return;
+                       }
+                       
+                       Ajax.api(this, {
+                               parameters: {
+                                       searchString: value
+                               }
+                       });
+               },
+               
+               _click: function (event) {
+                       event.preventDefault();
+                       
+                       _callbackSelect(elData(event.currentTarget, 'page-id'));
+                       
+                       UiDialog.close(this);
+               },
+               
+               _ajaxSuccess: function(data) {
+                       var html = '', page;
+                       //noinspection JSUnresolvedVariable
+                       for (var i = 0, length = data.returnValues.length; i < length; i++) {
+                               //noinspection JSUnresolvedVariable
+                               page = data.returnValues[i];
+                               
+                               html += '<li>'
+                                               + '<div class="containerHeadline pointer" data-page-id="' + page.pageID + '">'
+                                                       + '<h3>' + StringUtil.escapeHTML(page.name) + '</h3>'
+                                                       + '<small>' + StringUtil.escapeHTML(page.displayLink) + '</small>'
+                                               + '</div>'
+                                       + '</li>';
+                       }
+                       
+                       _resultList.innerHTML = html;
+                       
+                       window[html ? 'elShow' : 'elHide'](_resultContainer);
+                       
+                       if (html) {
+                               elBySelAll('.containerHeadline', _resultList, (function(item) {
+                                       item.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+                               }).bind(this));
+                       }
+                       else {
+                               var innerError = elCreate('small');
+                               innerError.className = 'innerError';
+                               innerError.textContent = Language.get('wcf.page.search.error.noResults');
+                               DomUtil.insertAfter(innerError, _searchInput.parentNode);
+                       }
+               },
+               
+               _ajaxSetup: function () {
+                       return {
+                               data: {
+                                       actionName: 'search',
+                                       className: 'wcf\\data\\page\\PageAction'
+                               }
+                       };
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'wcfUiPageSearch',
+                               options: {
+                                       onSetup: (function() {
+                                               var callbackSearch = this._search.bind(this);
+                                               
+                                               _searchInput = elById('wcfUiPageSearchInput');
+                                               _searchInput.addEventListener('keydown', function(event) {
+                                                       if (EventKey.Enter(event)) {
+                                                               callbackSearch(event);
+                                                       }
+                                               });
+                                               
+                                               _searchInput.nextElementSibling.addEventListener(WCF_CLICK_EVENT, callbackSearch);
+                                               
+                                               _resultContainer = elById('wcfUiPageSearchResultContainer');
+                                               _resultList = elById('wcfUiPageSearchResultList');
+                                       }).bind(this),
+                                       onShow: function() {
+                                               _searchInput.focus();
+                                       },
+                                       title: Language.get('wcf.page.search')
+                               },
+                               source: '<div class="section">'
+                                       + '<dl>'
+                                               + '<dt><label for="wcfUiPageSearchInput">' + Language.get('wcf.page.search.name') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<div class="inputAddon">'
+                                                               + '<input type="text" id="wcfUiPageSearchInput" class="long">'
+                                                               + '<a href="#" class="inputSuffix"><span class="icon icon16 fa-search"></span></a>'
+                                                       + '</div>'
+                                               + '</dd>'
+                                       + '</dl>'
+                               + '</div>'
+                               + '<section id="wcfUiPageSearchResultContainer" class="section" style="display: none;">'
+                                       + '<header class="sectionHeader">'
+                                               + '<h2 class="sectionTitle">' + Language.get('wcf.page.search.results') + '</h2>'
+                                               + '<p class="sectionDescription">' + Language.get('wcf.page.search.results.description') + '</p>'
+                                       + '</header>'
+                                       + '<ol id="wcfUiPageSearchResultList" class="containerList"></ol>'
+                               + '</section>'
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Page.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Page.js
new file mode 100644 (file)
index 0000000..b5b6c86
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Converts `<woltlab-metacode>` into the bbcode representation.
+ *
+ * @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/Redactor/Metacode
+ */
+define(['WoltLab/WCF/Ui/Page/Search'], function(UiPageSearch) {
+       "use strict";
+       
+       function UiRedactorPage(editor, button) { this.init(editor, button); }
+       UiRedactorPage.prototype = {
+               init: function (editor, button) {
+                       this._editor = editor;
+                       
+                       button.addEventListener(WCF_CLICK_EVENT, this._click.bind(this))
+               },
+               
+               _click: function (event) {
+                       event.preventDefault();
+                       
+                       UiPageSearch.open(this._insert.bind(this));
+               },
+               
+               _insert: function (pageID) {
+                       this._editor.buffer.set();
+                       
+                       this._editor.insert.text("[wsp='" + pageID + "'][/wsp]");
+               }
+       };
+       
+       return UiRedactorPage;
+});
index 689ed24754867f1837df8897261558a3d1819270..9e65fa65cfc22ec82cf1542efb23e5247c08ef66 100644 (file)
@@ -53,7 +53,7 @@ class PageAction extends AbstractDatabaseObjectAction implements ISearchAction,
        /**
         * @inheritDoc
         */
-       protected $requireACP = ['create', 'delete', 'toggle', 'update'];
+       protected $requireACP = ['create', 'delete', 'getSearchResultList', 'search', 'toggle', 'update'];
        
        /**
         * @inheritDoc
@@ -269,6 +269,43 @@ class PageAction extends AbstractDatabaseObjectAction implements ISearchAction,
                return $this->pageEditor->getHandler()->lookup($this->parameters['data']['searchString']);
        }
        
+       /**
+        * Validates parameters to search for a page by its internal name.
+        */
+       public function validateSearch() {
+               $this->readString('searchString');
+       }
+       
+       /**
+        * Searches for a page by its internal name.
+        * 
+        * @return      array   list of matching pages
+        */
+       public function search() {
+               $sql = "SELECT          pageID
+                       FROM            wcf".WCF_N."_page
+                       WHERE           name LIKE ?
+                                       AND requireObjectID = ?
+                       ORDER BY        name";
+               $statement = WCF::getDB()->prepareStatement($sql, 5);
+               $statement->execute([
+                       '%' . $this->parameters['searchString'] . '%',
+                       0
+               ]);
+               
+               $pages = [];
+               while ($pageID = $statement->fetchColumn()) {
+                       $page = PageCache::getInstance()->getPage($pageID);
+                       $pages[] = [
+                               'displayLink' => $page->getDisplayLink(),
+                               'name' => $page->name,
+                               'pageID' => $pageID
+                       ];
+               }
+               
+               return $pages;
+       }
+       
        /**
         * @inheritDoc
         */
index ef773d54dae2dfeaae5952efb446fcb32c190d67..e106ef238ab677162a2ab3224af4919fcf37ce68 100644 (file)
@@ -45,6 +45,7 @@ class PageContent extends DatabaseObject {
        public function getFormattedContent() {
                // assign embedded objects
                MessageEmbeddedObjectManager::getInstance()->setActiveMessage('com.woltlab.wcf.page.content', $this->pageContentID);
+               MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.page.content', [$this->pageContentID]);
                
                $processor = new HtmlOutputProcessor();
                $processor->process($this->content, 'com.woltlab.wcf.page.content', $this->pageContentID);
diff --git a/wcfsetup/install/files/lib/system/bbcode/WoltLabSuitePageBBCode.class.php b/wcfsetup/install/files/lib/system/bbcode/WoltLabSuitePageBBCode.class.php
new file mode 100644 (file)
index 0000000..71a5513
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\system\bbcode;
+use wcf\data\page\Page;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+use wcf\util\StringUtil;
+
+/**
+ * Parses the [wsp] bbcode tag.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Bbcode
+ * @since       3.0
+ */
+class WoltLabSuitePageBBCode extends AbstractBBCode {
+       /**
+        * @inheritDoc
+        */
+       public function getParsedTag(array $openingTag, $content, array $closingTag, BBCodeParser $parser) {
+               $pageID = (!empty($openingTag['attributes'][0])) ? intval($openingTag['attributes'][0]) : 0;
+               if (!$pageID) {
+                       return '';
+               }
+               
+               /** @var Page $page */
+               $page = MessageEmbeddedObjectManager::getInstance()->getObject('com.woltlab.wcf.page', $pageID);
+               if ($page !== null) {
+                       return StringUtil::getAnchorTag($page->getLink(), $page->getTitle());
+               }
+               
+               return '';
+       }
+}
index 83b33384a8a73331d7f88144cefabc92418c311a..38c99b9bd02dce1796d571692956a0765cc20df1 100644 (file)
@@ -2,6 +2,7 @@
 namespace wcf\system\message\embedded\object;
 use wcf\data\page\Page;
 use wcf\data\page\PageCache;
+use wcf\system\html\input\HtmlInputProcessor;
 
 /**
  * Parses embedded pages and outputs their link or title.
@@ -12,6 +13,20 @@ use wcf\data\page\PageCache;
  * @package    WoltLabSuite\Core\System\Message\Embedded\Object
  */
 class PageMessageEmbeddedObjectHandler extends AbstractSimpleMessageEmbeddedObjectHandler {
+       /**
+        * @inheritDoc
+        */
+       public function parse(HtmlInputProcessor $htmlInputProcessor, array $embeddedData) {
+               $pageIDs = [];
+               if (!empty($embeddedData['wsp'])) {
+                       for ($i = 0, $length = count($embeddedData['wsp']); $i < $length; $i++) {
+                               $pageIDs[] = intval($embeddedData['wsp'][$i][0]);
+                       }
+               }
+               
+               return array_unique($pageIDs);
+       }
+       
        /**
         * @inheritDoc
         */