diff --git a/app/controllers/course/wiki.php b/app/controllers/course/wiki.php index 4de56a7ce2ad83634c22a02b95d43137cddd6c3b..adf364312e1876476fd51c8df109dc0d96a8a186 100644 --- a/app/controllers/course/wiki.php +++ b/app/controllers/course/wiki.php @@ -446,16 +446,10 @@ class Course_WikiController extends AuthenticatedController } Navigation::activateItem('/course/wiki/start'); $user = User::findCurrent(); - WikiOnlineEditingUser::deleteBySQL( - "`page_id` = :page_id AND `chdate` < UNIX_TIMESTAMP() - :threshold", - [ - 'page_id' => $page->id, - 'threshold' => WikiOnlineEditingUser::$threshold - ] - ); + WikiOnlineEditingUser::purge($page); $pageData = [ 'page_id' => $page->id, - 'user_id' => $user ? $user->id : null, + 'user_id' => $user?->id, ]; $online_user = WikiOnlineEditingUser::findOneBySQL( '`page_id` = :page_id AND `user_id` = :user_id', @@ -466,11 +460,12 @@ class Course_WikiController extends AuthenticatedController } $editingUsers = WikiOnlineEditingUser::countBySQL( "`page_id` = ? AND `editing` = 1 AND `user_id` != ?", - [$page->id, $user ? $user->id : null] + [$page->id, $user?->id] ); - $online_user->editing = $editingUsers === 0 ? 1 : 0; + $online_user->editing = $editingUsers === 0; $online_user->chdate = time(); $online_user->store(); + $this->me_online = $online_user; $this->online_users = WikiOnlineEditingUser::findBySQL( "JOIN `auth_user_md5` USING (`user_id`) @@ -478,11 +473,10 @@ class Course_WikiController extends AuthenticatedController ORDER BY Nachname, Vorname", [$page->id] ); - $startPage = WikiPage::find($this->range->getConfiguration()->WIKI_STARTPAGE_ID); $this->contentbar = ContentBar::get() ->setTOC(CoreWiki::getTOC($page)) ->setIcon(Icon::create('wiki')) - ->setInfo(_('Zuletzt gespeichert') .': '. '<studip-date-time :timestamp="Math.floor(lastSaveDate / 1000)" :relative="true"></studip-date-time>'); + ->setInfo(_('Zuletzt gespeichert') .': '. '<span class="wiki-last-edited-' . $this->page->id . '"></span>'); } public function apply_editing_action(WikiPage $page) @@ -513,12 +507,37 @@ class Course_WikiController extends AuthenticatedController } $online_user->store(); $output = [ - 'me_online' => $online_user->toArray(), + 'me_online' => ['editing' => (bool) $online_user->editing], 'users' => $page->getOnlineUsers() ]; $this->render_json($output); } + public function cancel_apply_editing_action(WikiPage $page) + { + if (!$page->isEditable() || !Request::isPost()) { + throw new AccessDeniedException(); + } + $user = User::findCurrent(); + $pageData = [ + 'page_id' => $page->id, + 'user_id' => $user->id + ]; + $online_user = WikiOnlineEditingUser::findOneBySQL( + '`page_id` = :page_id AND `user_id` = :user_id', + $pageData + ); + if (!$online_user) { + $online_user = WikiOnlineEditingUser::build($pageData); + } + $online_user->editing_request = false; + $online_user->store(); + + $this->render_json([ + 'users' => $page->getOnlineUsers() + ]); + } + public function leave_editing_action(WikiPage $page) { if (!$page->isEditable()) { @@ -554,6 +573,7 @@ class Course_WikiController extends AuthenticatedController $this->render_json([ 'error' => 'not_in_edit_mode' ]); + return; } $online_user_them = WikiOnlineEditingUser::findOneBySQL( '`page_id` = :page_id AND `user_id` = :user_id', @@ -566,11 +586,11 @@ class Course_WikiController extends AuthenticatedController return; } - $online_user_me->editing = 0; + $online_user_me->editing = false; $online_user_me->store(); - $online_user_them->editing_request = 1; //that will be set to 0 by the user themself - $online_user_them->editing = 1; + $online_user_them->editing_request = true; //that will be set to 0 by the user themself + $online_user_them->editing = true; $online_user_them->store(); $this->render_json([ diff --git a/app/controllers/jsupdater.php b/app/controllers/jsupdater.php index 9d09a833ff033ad5a7a38dacaba3a6bb67ae4b7a..8860f8de58f2154613a82bde173fce83ffb743f6 100644 --- a/app/controllers/jsupdater.php +++ b/app/controllers/jsupdater.php @@ -117,7 +117,6 @@ class JsupdaterController extends AuthenticatedController 'messages' => $this->getMessagesUpdates($pageInfo['messages'] ?? null), 'personalnotifications' => $this->getPersonalNotificationUpdates(), 'questionnaire' => $this->getQuestionnaireUpdates($pageInfo['questionnaire'] ?? null), - 'wiki_page_content' => $this->getWikiPageContents($pageInfo['wiki_page_content'] ?? null), 'wiki_editor_status' => $this->getWikiEditorStatus($pageInfo['wiki_editor_status'] ?? null), ]; @@ -258,105 +257,79 @@ class JsupdaterController extends AuthenticatedController return $data; } - private function getWikiPageContents($pageInfo): array + private function getWikiEditorStatus($pageInfo): array { $data = []; if (!empty($pageInfo)) { - foreach ($pageInfo as $page_id) { - $page = WikiPage::find($page_id); - if ($page && $page->isReadable() && ($page->chdate >= Request::int('server_timestamp'))) { - $data['contents'][$page_id] = wikiReady($page->content, true, $page->range_id, $page->id); - } - } - } - return $data; - } + $id = $pageInfo['id']; - private function getWikiEditorStatus($pageInfo): array - { - $data = []; - if (!empty($pageInfo['page_ids'])) { $user = User::findCurrent(); - foreach ((array) $pageInfo['page_ids'] as $page_id) { - WikiOnlineEditingUser::deleteBySQL( - "`page_id` = :page_id AND `chdate` < UNIX_TIMESTAMP() - :threshold", - [ - 'page_id' => $page_id, - 'threshold' => WikiOnlineEditingUser::$threshold - ] - ); - $page = WikiPage::find($page_id); - if ($page) { - if ($page->isEditable()) { - if ( - $pageInfo['focussed'] == $page_id - && !empty($pageInfo['page_content']) - ) { - $page->content = \Studip\Markup::markAsHtml( - $pageInfo['page_content'] - ); - if ($page->isDirty()) { - $page['user_id'] = User::findCurrent()->id; - $page->store(); - } - } - $onlineData = [ - 'user_id' => $user->id, - 'page_id' => $page_id - ]; - $online = WikiOnlineEditingUser::findOneBySQL( - "`user_id` = :user_id AND `page_id` = :page_id", - $onlineData - ); - if (!$online) { - $online = WikiOnlineEditingUser::build($onlineData); + $page = WikiPage::find($id); + if ($page) { + WikiOnlineEditingUser::purge($page); + + if ($page->isEditable()) { + $onlineData = [ + 'user_id' => $user->id, + 'page_id' => $page->id + ]; + $online = WikiOnlineEditingUser::findOneBySQL( + '`user_id` = :user_id AND `page_id` = :page_id', + $onlineData + ); + if (!$online) { + $online = WikiOnlineEditingUser::build($onlineData); + } elseif ($online->editing && isset($pageInfo['content'])) { + $page->content = \Studip\Markup::markAsHtml($pageInfo['content']); + if ($page->isDirty()) { + $page->user_id = $user->id; + $page->store(); } + } else { $editingUsers = WikiOnlineEditingUser::countBySQL( - "`page_id` = ? AND `editing` = 1 AND `user_id` != ?", + '`page_id` = ? AND `editing` = 1 AND `user_id` != ?', [$page->id, $user->id] ); if ($editingUsers > 0) { - $online->editing = 0; - } else if ($online->editing && $online->editing_request) { + $online->editing = false; + } elseif ($online->editing && $online->editing_request) { // this is the mode that this user requested the editing mode and was granted to get it: - $online->editing_request = 0; - } else if ($online->editing_request) { - $other_requests = WikiOnlineEditingUser::countBySql("`page_id` = ? AND `editing_request` = 1 AND `user_id` != ?", [ + $online->editing_request = false; + } elseif ($online->editing_request) { + $other_requests = WikiOnlineEditingUser::countBySql('`page_id` = ? AND `editing_request` = 1 AND `user_id` != ?', [ $page->id, $user->id, ]); if ($other_requests === 0) { - $online->editing_request = 0; - $online->editing = 1; - } - } else { - if ($pageInfo['focussed'] == $page_id) { - $online->editing = 1; - } else { - $other_users = WikiOnlineEditingUser::countBySql("`page_id` = ? AND `user_id` != ?", [ - $page->id, - $user->id, - ]); - if ($other_users === 0) { - // if I'm the only user I don't need to lose the edit mode - $online->editing = 1; - } else { - $online->editing = 0; - } + $online->editing_request = false; + $online->editing = true; } + } elseif (!$pageInfo['online']) { + $other_users = WikiOnlineEditingUser::countBySql('`page_id` = ? AND `user_id` != ?', [ + $page->id, + $user->id, + ]); + // if I'm the only user I don't need to lose the edit mode + $online->editing = $other_users === 0; } - $online->chdate = time(); - $online->store(); - $data['contents'][$page_id] = wikiReady($page->content, true, $page->range_id, $page_id); - $data['wysiwyg_contents'][$page_id] = $page->content; - $data['pages'][$page_id]['editing'] = $online->editing; } - else { - $data['pages'][$page_id]['editing'] = 0; - } - $data['pages'][$page_id]['chdate'] = $page->chdate; - $data['users'][$page_id] = $page->getOnlineUsers(); + + $online->chdate = time(); + $online->store(); + + $data['editing'] = (bool) $online->editing; } + + if ( + $page->isReadable() + && $page->chdate >= Request::int('server_timestamp') + ) { + $data['content'] = wikiReady($page->content, true, $page->range_id, $page->id); + $data['wysiwyg'] = $page->content; + $data['chdate'] = date('c', $page->chdate); + } + + $data['users'] = $page->getOnlineUsers(); } } return $data; diff --git a/app/views/course/wiki/edit.php b/app/views/course/wiki/edit.php index df126a48a2f176607c831f4a3c83955c46cdd8b0..438ed56bdebafd820a415fa8541e35312413b091 100644 --- a/app/views/course/wiki/edit.php +++ b/app/views/course/wiki/edit.php @@ -3,61 +3,20 @@ * @var WikiPage $page * @var Course_WikiController $controller * @var WikiOnlineEditingUser $me_online + * @var ContentBar $contentbar */ ?> -<div class="wiki-editor-container" - data-page_id="<?= htmlReady($page->id) ?>" - data-editing="<?= htmlReady($me_online->editing) ?>" - data-content="<?= htmlReady(wikiReady($page->content, true, $page->range_id, $page->id)) ?>" - data-chdate="<?= htmlReady($page->chdate) ?>" - data-users="<?= htmlReady(json_encode($page->getOnlineUsers())) ?>"> - - <?= $contentbar ?> - - <form action="<?= $controller->save($page) ?>" method="post" class="default" v-show="editing"> - <?= CSRFProtection::tokenTag() ?> - <textarea class="wiki-editor size-l" - ref="wiki_editor" - data-editor="extraPlugins=WikiLink" - name="content"><?= wysiwygReady($page->content) ?></textarea> - - <div></div> - <label> - <input type="checkbox" v-model="autosave"> - <?= _('Automatisches Speichern aktivieren.') ?> - </label> - <div> - <?= _('Zuletzt gespeichert') .': ' ?> - <studip-date-time :timestamp="Math.floor(lastSaveDate / 1000)" :relative="true"></studip-date-time> - </div> - - <div data-dialog-button=""> - <button class="button" :title="isChanged ? '<?= _('Den aktuellen Stand speichern.') ?>' : '<?= _('Der aktuelle Stand wurde bereits gespeichert.') ?>'"> - <?= _('Speichern') ?> - </button> - <?= \Studip\LinkButton::create(_('Verlassen'), $controller->leave_editing($page))?> - <button v-for="user in requestingUsers" - :key="user.user_id" - @click.prevent="delegateEditMode(user.user_id)" - class="button"> - {{ $gettextInterpolate($gettext('Schreibmodus an %{name} übergeben'), { name: user.fullname }) }} - </button> - </div> - </form> - - <div v-if="!editing" class=""> - <div v-html="content"></div> - <div data-dialog-button=""> - <button class="button" - title="<?= _('Beantragen Sie, dass Sie den Text jetzt bearbeiten wollen.') ?>" - @click.prevent="applyEditing"> - <?= _('Bearbeiten beantragen') ?> - </button> - <?= \Studip\LinkButton::create(_('Verlassen'), $controller->leave_editing($page))?> - </div> - </div> - - <wiki-editor-online-users :users="users"></wiki-editor-online-users> - -</div> +<?= $contentbar ?> + +<?= Studip\VueApp::create('WikiEditor') + ->withProps([ + 'cancel-url' => $controller->leave_editingURL($page), + 'chdate' => date('c', $page->chdate), + 'page-content' => wikiReady($page->content, true, $page->range_id, $page->id), + 'editing' => (bool) $me_online->editing, + 'page-id' => (int) $page->id, + 'save-url' => $controller->saveURL($page), + 'users' => $page->getOnlineUsers(), + ]) +?> diff --git a/lib/classes/VueApp.php b/lib/classes/VueApp.php index 3ebae34de1b097c717b36dbb86d637142d819052..76c9a52ae3d171199db2679bd3c820d469fa65ca 100644 --- a/lib/classes/VueApp.php +++ b/lib/classes/VueApp.php @@ -40,6 +40,7 @@ final class VueApp implements Stringable private array $plugins = []; private array $props = []; + private array $slots = []; private array $stores = []; private array $storeData = []; @@ -86,8 +87,9 @@ final class VueApp implements Stringable */ public function withSlot(string $name, string|Template $content): VueApp { - $this->slots[$name] = $content instanceof Template ? $content->render() : $content; - return $this; + $clone = clone $this; + $clone->slots[$name] = $content instanceof Template ? $content->render() : $content; + return $clone; } /** @@ -139,7 +141,6 @@ final class VueApp implements Stringable { $clone = clone $this; $clone->plugins[$plugin] = $filename ?? $plugin; - return $clone; } diff --git a/lib/models/WikiOnlineEditingUser.php b/lib/models/WikiOnlineEditingUser.php index 5cc0df14c6bfbff72ccc47884771f688741ca98d..f28ae0ed04479cabc99ab2f2af7dd4e235ee9a69 100644 --- a/lib/models/WikiOnlineEditingUser.php +++ b/lib/models/WikiOnlineEditingUser.php @@ -1,5 +1,4 @@ <?php - /** * WikiOnlineEditingUser.php * @@ -13,14 +12,20 @@ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP * - * @property string page_id database column - * @property string user_id database column - * @property string id alias column for user_id - * @property string last_lifesign computed column read/write + * @property int $id pk + * @property string $user_id database column + * @property int $page_id database column + * @property bool $editing database column + * @property bool $editing_request database column + * @property int $chdate database column + * @property int $mkdate database column + * + * @property WikiPage $page + * @property User $user */ class WikiOnlineEditingUser extends SimpleORMap { - public static $threshold = 60 * 1; + public static int $threshold = 1 * 60; protected static function configure($config = []) { @@ -35,4 +40,15 @@ class WikiOnlineEditingUser extends SimpleORMap ]; parent::configure($config); } + + public static function purge(WikiPage $page): void + { + WikiOnlineEditingUser::deleteBySQL( + '`page_id` = :page_id AND `chdate` < UNIX_TIMESTAMP() - :threshold', + [ + 'page_id' => $page->id, + 'threshold' => self::$threshold + ] + ); + } } diff --git a/lib/models/WikiPage.php b/lib/models/WikiPage.php index b5f0101338cfbc3e2e3d4631ee228940a8580e2d..52ca456b5255b751e4094a4d34c11c2ecb4a6beb 100644 --- a/lib/models/WikiPage.php +++ b/lib/models/WikiPage.php @@ -11,7 +11,8 @@ * @author mlunzena * @copyright (c) Authors * - * @property array $id alias for pk + * @property int $id alias for pk + * @property int $page_id database column * @property string $course_id database column * @property string|null $user_id database column * @property string $name database column @@ -289,14 +290,9 @@ class WikiPage extends SimpleORMap implements PrivacyObject */ public function getOnlineUsers(): array { - $users = []; - WikiOnlineEditingUser::deleteBySQL( - "`page_id` = :page_id AND `chdate` < UNIX_TIMESTAMP() - :threshold", - [ - 'page_id' => $this->id, - 'threshold' => WikiOnlineEditingUser::$threshold - ] - ); + WikiOnlineEditingUser::purge($this); + $this->resetRelation('onlineeditingusers'); + return $this->onlineeditingusers->map(function (WikiOnlineEditingUser $editing_user) { return [ 'user_id' => $editing_user->user_id, diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js index 551dacaa0735208df52f1eb767d8d82d8ef5dbff..513c796cb4322d456ceae43e85cbd0a19accb55a 100644 --- a/resources/assets/javascripts/bootstrap/vue.js +++ b/resources/assets/javascripts/bootstrap/vue.js @@ -38,58 +38,65 @@ STUDIP.ready(() => { }; }); - STUDIP.Vue.load().then(async ({createApp, store, Vue}) => { + STUDIP.Vue.load().then(({createApp, store, Vue}) => { + const promises = [Promise.resolve()]; + for (const [index, name] of Object.entries(config.stores)) { - import(`../../../vue/store/${name}.js`).then(storeConfig => { - store.registerModule(index, storeConfig.default); + promises.push( + import(`../../../vue/store/${name}.js`).then(storeConfig => { + store.registerModule(index, storeConfig.default); - const dataElement = document.getElementById(`vue-store-data-${index}`); - if (dataElement) { - const data = JSON.parse(dataElement.innerText); - Object.keys(data).forEach(command => { - store.commit(`${index}/${command}`, data[command]); - }); + const dataElement = document.getElementById(`vue-store-data-${index}`); + if (dataElement) { + const data = JSON.parse(dataElement.innerText); + Object.keys(data).forEach(command => { + store.commit(`${index}/${command}`, data[command]); + }); - dataElement.remove(); - } - }); + dataElement.remove(); + } + }) + ); } for (const [plugin, filename] of Object.entries(config.plugins)) { - import(`../../../vue/plugins/${filename}.js`) - .then((temp) => Vue.use(temp[plugin], { store })); + promises.push( + import(`../../../vue/plugins/${filename}.js`) + .then((temp) => Vue.use(temp[plugin], { store })) + ); } + Promise.all(promises).then(() => { + createApp({ + components, + store, - createApp({ - components, - store, - - beforeCreate() { - STUDIP.Vue.emit('VueAppWillCreate', this); - }, - created() { - STUDIP.Vue.emit('VueAppDidCreate', this); - }, - beforeMount() { - STUDIP.Vue.emit('VueAppWillMount', this); - }, - mounted() { - STUDIP.Vue.emit('VueAppDidMount', this); - }, - beforeUpdate() { - STUDIP.Vue.emit('VueAppWillUpdate', this); - }, - updated() { - STUDIP.Vue.emit('VueAppDidUpdate', this); - }, - beforeDestroy() { - STUDIP.Vue.emit('VueAppWillDestroy', this); - }, - destroyed() { - STUDIP.Vue.emit('VueAppDidDestroy', this); - }, - }).$mount(node); + beforeCreate() { + STUDIP.Vue.emit('VueAppWillCreate', this); + }, + created() { + STUDIP.Vue.emit('VueAppDidCreate', this); + }, + beforeMount() { + STUDIP.Vue.emit('VueAppWillMount', this); + }, + mounted() { + STUDIP.Vue.emit('VueAppDidMount', this); + }, + beforeUpdate() { + STUDIP.Vue.emit('VueAppWillUpdate', this); + }, + updated() { + STUDIP.Vue.emit('VueAppDidUpdate', this); + }, + beforeDestroy() { + STUDIP.Vue.emit('VueAppWillDestroy', this); + }, + destroyed() { + STUDIP.Vue.emit('VueAppDidDestroy', this); + }, + }).$mount(node); + }); }); node.dataset.vueAppCreated = 'true'; diff --git a/resources/assets/javascripts/bootstrap/wiki.js b/resources/assets/javascripts/bootstrap/wiki.js deleted file mode 100644 index b28c12760d4f75b2ca8cef0a4bd18b4b89c68c0c..0000000000000000000000000000000000000000 --- a/resources/assets/javascripts/bootstrap/wiki.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * This file contains all wiki related javascript. - * - * For now this is the "submit and edit" functionality via ajax. - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @copyright Stud.IP Core Group - * @license GPL2 or any later version - * @since Stud.IP 3.3 - */ - - - -STUDIP.domReady(() => { - STUDIP.JSUpdater.register('wiki_page_content', STUDIP.Wiki.updatePageContent, function () { - //update the wiki page for readers: - return Array.from(document.getElementsByClassName('wiki_page_content')).map(node => { - return node.dataset.page_id; - }); - }); - - if (document.querySelector('.wiki-editor-container') !== null) { - STUDIP.Wiki.initEditor(); - } - - STUDIP.JSUpdater.register('wiki_editor_status', STUDIP.Wiki.updateEditorStatus, function () { - let info = { - page_ids: [], - focussed: null - }; - for (let page_id in STUDIP.Wiki.Editors) { - info.page_ids.push(page_id); - let editor = STUDIP.Wiki.Editors[page_id].editor; - if (STUDIP.Wiki.Editors[page_id].isChanged && STUDIP.Wiki.Editors[page_id].autosave) { - //if either the textarea or the wysiwyg has focus: - info.page_content = editor.getData(); - STUDIP.Wiki.Editors[page_id].isChanged = false; - STUDIP.Wiki.Editors[page_id].lastSaveDate = new Date(); - } - if (editor.editing.view.document.isFocused) { - STUDIP.Wiki.Editors[page_id].lastFocussedDate = new Date(); - } - if (new Date() - STUDIP.Wiki.Editors[page_id].lastFocussedDate < 1000 * 60) { //time after inactivity - info.focussed = page_id; - } else { - if (STUDIP.Wiki.Editors[page_id].users.length !== 1) { - //then I will likely lose my edit mode so others can obtain it - STUDIP.Wiki.Editors[page_id].editing = false; - } - } - } - return info; - }); -}); diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 7a382b8cd6ee64be4d3899dc6ce48129b03b14e9..102a558fde117174efd202ca3f9ac771081c792d 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -44,7 +44,6 @@ import "./bootstrap/tour.js" import "./bootstrap/questionnaire.js" import "./bootstrap/qr_code.js" import "./bootstrap/startpage.js" -import "./bootstrap/wiki.js" import "./bootstrap/course_wizard.js" import "./bootstrap/big_image_handler.js" import "./bootstrap/opengraph.js" diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index 3a8402e4acaf65964edb944f532c7b41d6354b9d..b9c1b5bb0aea30234b837b9589df2f8856b89e53 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -81,7 +81,6 @@ import * as Gettext from './lib/gettext'; import UserFilter from './lib/user_filter.js'; import wysiwyg from './lib/wysiwyg.js'; import ScrollToTop from './lib/scroll_to_top.js'; -import Wiki from './lib/wiki.js'; const configURLHelper = _.get(window, 'STUDIP.URLHelper', {}); const URLHelper = createURLHelper(configURLHelper); @@ -173,5 +172,4 @@ window.STUDIP = _.assign(window.STUDIP || {}, { dialogReady, ScrollToTop, Vue, - Wiki }); diff --git a/resources/assets/javascripts/lib/jsupdater.js b/resources/assets/javascripts/lib/jsupdater.js index 5069af0be099d3feda102da851289a7d11693f30..29abed86abd9a83f1de161432db97c3600fa6ad0 100644 --- a/resources/assets/javascripts/lib/jsupdater.js +++ b/resources/assets/javascripts/lib/jsupdater.js @@ -84,7 +84,7 @@ function collectData() { for (const [index, handler] of Object.entries(registeredHandlers)) { if (handler.data) { const thisData = $.isFunction(handler.data) ? handler.data() : handler.data; - if (thisData !== null && !$.isEmptyObject(thisData)) { + if (thisData !== null && !(typeof thisData === 'object' && $.isEmptyObject(thisData))) { data[index] = thisData; } } diff --git a/resources/assets/javascripts/lib/wiki.js b/resources/assets/javascripts/lib/wiki.js deleted file mode 100644 index 962011865927c32ba876b0a38503e9abf2c1e93f..0000000000000000000000000000000000000000 --- a/resources/assets/javascripts/lib/wiki.js +++ /dev/null @@ -1,112 +0,0 @@ -const Wiki = { - updatePageContent(pageContents) { - if (!pageContents) { - return; - } - for (let page_id in pageContents.contents) { - $('.wiki_page_content_' + page_id).html(pageContents.contents[page_id]); - } - }, - updateEditorStatus(editorStatus) { - if (!editorStatus) { - return; - } - for (let page_id in STUDIP.Wiki.Editors) { - STUDIP.Wiki.Editors[page_id].users = editorStatus.users[page_id]; - if (!STUDIP.Wiki.Editors[page_id].editing) { - STUDIP.Wiki.Editors[page_id].content = editorStatus.contents[page_id]; - STUDIP.Wiki.Editors[page_id].editor.setData(editorStatus.wysiwyg_contents[page_id]); - } - if ( - !STUDIP.Wiki.Editors[page_id].editing - && editorStatus.pages[page_id].editing > 0 - ) { - STUDIP.Wiki.Editors[page_id].editing = true; - STUDIP.Wiki.Editors[page_id].focusEditor(); - } else { - STUDIP.Wiki.Editors[page_id].editing = editorStatus.pages[page_id].editing > 0; - } - STUDIP.Wiki.Editors[page_id].lastSaveDate = new Date(editorStatus.pages[page_id].chdate * 1000); - } - - }, - Editors: {}, - initEditor() { - - let wiki_edit_container = document.querySelectorAll( '.wiki-editor-container'); - for (let edit_container of wiki_edit_container) { - let page_id = edit_container.dataset.page_id; - - Promise.all([ - STUDIP.Vue.load(), - import('../../../vue/components/WikiEditorOnlineUsers.vue').then((config) => config.default), - ]).then(([{ createApp }, WikiEditorOnlineUsers]) => { - return createApp({ - el: edit_container, - data() { - return { - page_id: page_id, - editing: edit_container.dataset.editing > 0, - content: edit_container.dataset.content, - users: JSON.parse(edit_container.dataset.users), - editor: null, - isChanged: false, - lastSaveDate: new Date(edit_container.dataset.chdate * 1000), - lastChangeDate: 0, - lastFocussedDate: 0, - autosave: true - }; - }, - methods: { - applyEditing() { - const url = STUDIP.URLHelper.getURL('dispatch.php/course/wiki/apply_editing/' + this.page_id) - $.post(url).done(output => { - if (output.me_online.editing > 0) { - this.editing = true; - this.focusEditor(); - } - this.users = output.users; - }); - }, - delegateEditMode(user_id) { - const url = STUDIP.URLHelper.getURL('dispatch.php/course/wiki/delegate_edit_mode/' + this.page_id + '/' + user_id); - $.post(url).done(() => this.editing = false); - }, - focusEditor() { - this.$nextTick(() => { - this.editor.editing.view.focus(); - }); - } - }, - mounted() { - let textarea = this.$refs['wiki_editor']; - let promise = STUDIP.wysiwyg.replace(textarea); - promise.then((editor) => { - if (this.editing) { - editor.editing.view.focus(); - } - editor.model.document.on('change:data',() => { - this.isChanged = true; - this.lastChangeDate = new Date(); - }); - this.editor = editor; - }); - }, - computed: { - requestingUsers() { - return this.users - .filter(u => u.editing_request) - .sort((a, b) => a.fullname.localeCompare(b.fullname)); - } - }, - components: { WikiEditorOnlineUsers } - }); - }).then((app) => { - STUDIP.Wiki.Editors[page_id] = app; - }); - } - - } -}; - -export default Wiki; diff --git a/resources/vue/components/WikiEditor.vue b/resources/vue/components/WikiEditor.vue new file mode 100644 index 0000000000000000000000000000000000000000..d1641811e8e9a657874067e3c90dce438939c7c3 --- /dev/null +++ b/resources/vue/components/WikiEditor.vue @@ -0,0 +1,232 @@ +<template> + <div> + <form :action="saveUrl" method="post" class="default" v-show="isEditing"> + <input type="hidden" :name="csrf.name" :value="csrf.value"> + + <textarea class="wiki-editor size-l" + ref="wiki_editor" + data-editor="extraPlugins=WikiLink" + name="content" + v-model="content" + ></textarea> + + <div></div> + <label> + <input type="checkbox" v-model="autosave"> + {{ $gettext('Automatisches Speichern aktivieren.') }} + </label> + <div> + {{ $gettext('Zuletzt gespeichert') }}: + <studip-date-time :timestamp="Math.floor(lastSaveDate / 1000)" + :relative="true" + ></studip-date-time> + </div> + + <div data-dialog-button=""> + <button class="button" :title="isChanged ? $gettext('Den aktuellen Stand speichern.') : $gettext('Der aktuelle Stand wurde bereits gespeichert.')"> + {{ $gettext('Speichern') }} + </button> + <a :href="cancelUrl" class="button"> + {{ $gettext('Verlassen') }} + </a> + <button v-for="user in requestingUsers" + :key="user.user_id" + @click.prevent="delegateEditMode(user.user_id)" + class="button" + > + {{ $gettextInterpolate($gettext('Schreibmodus an %{name} übergeben'), { name: user.fullname }, true) }} + </button> + </div> + </form> + + <div v-if="!isEditing"> + <div v-html="content"></div> + <div data-dialog-button> + <button class="button" + v-if="!editingWasRequested" + :title="$gettext('Beantragen Sie, dass Sie den Text jetzt bearbeiten wollen.')" + @click.prevent="applyEditing()" + > + {{ $gettext('Bearbeiten beantragen') }} + </button> + <button class="cancel button" + v-else + :title="$gettext('Klicken Sie, um die Anfrage zum Bearbeiten abzubrechen')" + @click.prevent="cancelApplyEditing()" + > + {{ $gettext('Bearbeiten beantragt') }} + </button> + <a :href="cancelUrl" class="button"> + {{ $gettext('Verlassen') }} + </a> + </div> + </div> + + <wiki-editor-online-users :users="onlineUsers"></wiki-editor-online-users> + + <mounting-portal :mount-to="`.wiki-last-edited-${pageId}`"> + <studip-date-time :timestamp="Math.floor(lastSaveDate / 1000)" + :relative="true" + ></studip-date-time> + </mounting-portal> + </div> +</template> +<script> +import WikiEditorOnlineUsers from "./WikiEditorOnlineUsers.vue"; +import StudipDateTime from "./StudipDateTime.vue"; +import JSUpdater from "@/assets/javascripts/lib/jsupdater"; + +export default { + name: 'wiki-editor', + components: {StudipDateTime, WikiEditorOnlineUsers }, + props: { + cancelUrl: { + type: String, + required: true, + }, + chdate: { + type: String, + required: true, + }, + editing: { + type: Boolean, + default: true + }, + offlineThreshold: { + type: Number, + default: 60 * 1000 + }, + pageContent: { + type: String, + default: '' + }, + pageId: { + type: Number, + required: true + }, + saveUrl: { + type: String, + required: true + }, + users: { + type: Array, + default: () => [] + } + }, + data() { + return { + autosave: true, + content: this.pageContent, + editor: null, + isChanged: false, + isEditing: this.editing, + lastFocussedDate: null, + lastSaveDate: new Date(this.chdate), + onlineUsers: this.users, + }; + }, + computed: { + csrf() { + return STUDIP.CSRF_TOKEN; + }, + editingWasRequested() { + return this.onlineUsers + .filter(u => u.user_id === STUDIP.USER_ID) + .some(u => u.editing_request); + }, + isOnlineAndEditing() { + return this.isEditing + && new Date() - this.lastFocussedDate < this.offlineThreshold; + }, + requestingUsers() { + return this.onlineUsers + .filter(u => u.editing_request) + .sort((a, b) => a.fullname.localeCompare(b.fullname)); + } + }, + methods: { + applyEditing() { + const url = STUDIP.URLHelper.getURL(`dispatch.php/course/wiki/apply_editing/${this.pageId}`) + $.post(url).done(output => { + if (output.me_online.editing > 0) { + this.isEditing = true; + this.focusEditor(); + } + this.onlineUsers = output.users; + }); + }, + cancelApplyEditing() { + const url = STUDIP.URLHelper.getURL(`dispatch.php/course/wiki/cancel_apply_editing/${this.pageId}`) + $.post(url).done(output => { + this.onlineUsers = output.users; + }); + }, + delegateEditMode(user_id) { + const url = STUDIP.URLHelper.getURL(`dispatch.php/course/wiki/delegate_edit_mode/${this.pageId}/${user_id}`); + $.post(url).done(() => { + this.isEditing = false; + }); + }, + focusEditor() { + this.$nextTick(() => { + this.editor.editing.view.focus(); + }); + }, + getUpdaterData() { + if (this.editor.editing.view.document.isFocused) { + this.lastFocussedDate = new Date(); + } + + const data = { + id: this.pageId, + online: this.isOnlineAndEditing + }; + + if (this.isChanged) { + data.content = this.editor.getData(); + this.isChanged = false; + } + + return data; + } + }, + mounted() { + const textarea = this.$refs['wiki_editor']; + + STUDIP.wysiwyg.replace(textarea).then((editor) => { + editor.model.document.on('change:data', () => { + if (this.autosave) { + this.isChanged = true; + } + }); + + if (this.isEditing) { + this.focusEditor(); + } + + this.editor = editor; + }); + + JSUpdater.register( + 'wiki_editor_status', + (content) => { + this.onlineUsers = content.users; + this.isEditing = content.editing; + + if ('chdate' in content) { + this.lastSaveDate = new Date(content.chdate); + } + + if ('content' in content) { + this.content = content.content; + } + + if (!this.isEditing && 'wysiwyg' in content) { + this.editor.setData(content.wysiwyg); + } + }, + () => this.getUpdaterData() + ) + } +} +</script> diff --git a/resources/vue/components/WikiEditorOnlineUsers.vue b/resources/vue/components/WikiEditorOnlineUsers.vue index b4a1b6099f55d42e17ace52047d33fad7c6d1036..71c7c1c9929662ec9a54fe38407f6c55ca97aee7 100644 --- a/resources/vue/components/WikiEditorOnlineUsers.vue +++ b/resources/vue/components/WikiEditorOnlineUsers.vue @@ -4,7 +4,7 @@ <template #content> <ol class="clean"> <li v-for="user in users" :key="user.user_id"> - <img class="avatar-small" :src="user.avatar"> + <img class="avatar-small" :src="user.avatar" alt=""> {{ user.fullname }} <span v-if="user.editing" :title="$gettext('Diese Person hat den Bearbeitungsmodus.')"> @@ -19,10 +19,13 @@ </SidebarWidget> </MountingPortal> </template> - <script> +import SidebarWidget from "./SidebarWidget.vue"; +import StudipIcon from "./StudipIcon.vue"; + export default { name: 'WikiEditorOnlineUsers', + components: {StudipIcon, SidebarWidget}, props: { users: Array },