From 6931cfc86f6430853caab7aa6b364d8b8809f913 Mon Sep 17 00:00:00 2001 From: Thomas Hackl Date: Mon, 11 Nov 2024 07:33:55 +0000 Subject: Resolve "ContentBar 2.0" Closes #4244 Merge request studip/studip!3128 --- app/controllers/course/wiki.php | 60 +++--- app/controllers/oer/market.php | 9 +- app/views/course/wiki/edit.php | 4 +- app/views/course/wiki/page.php | 3 +- app/views/course/wiki/version.php | 8 +- app/views/oer/market/details.php | 7 +- lib/classes/Debug/VueCollector.php | 17 ++ lib/classes/Icon.php | 7 +- lib/classes/VueApp.php | 23 ++- lib/models/WikiPage.php | 26 --- lib/modules/CoreWiki.php | 53 ++++-- resources/assets/javascripts/lib/actionmenu.js | 6 +- resources/assets/stylesheets/scss/contentbar.scss | 4 +- .../scss/courseware/layouts/ribbon.scss | 118 ++++++++---- .../stylesheets/scss/courseware/layouts/tabs.scss | 2 +- resources/assets/stylesheets/scss/responsive.scss | 112 ++++------- .../assets/stylesheets/scss/table_of_contents.scss | 73 +++++--- resources/studip.d.ts | 21 +++ resources/vue/components/ContentBar.vue | 163 ++++++++++++++++ resources/vue/components/ContentBarBreadcrumbs.vue | 78 ++++++++ .../vue/components/ContentBarTableOfContents.vue | 63 +++++++ resources/vue/components/ContentBarTocItemList.vue | 33 ++++ resources/vue/components/WikiEditor.vue | 22 ++- .../structural-element/CoursewareRibbon.vue | 207 +++++++-------------- .../structural-element/CoursewareRibbonToolbar.vue | 50 ++--- .../structural-element/CoursewareSearchResults.vue | 16 +- .../CoursewareStructuralElement.vue | 128 +++++++------ .../structural-element/CoursewareWelcomeScreen.vue | 6 +- .../PublicCoursewareStructuralElement.vue | 17 +- .../structural-element-components.js | 6 +- .../tasks/CoursewareDashboardStudents.vue | 18 +- .../courseware/tasks/PagesTaskGroupsShow.vue | 18 +- .../courseware/toolbar/CoursewareToolbar.vue | 2 +- .../components/responsive/ResponsiveContentBar.vue | 63 +++---- resources/vue/components/table-of-contents.ts | 19 ++ resources/vue/mixins/courseware/export.js | 5 +- resources/vue/store/StudipStore.js | 21 ++- .../store/courseware/courseware-public.module.js | 13 -- .../vue/store/courseware/courseware.module.js | 8 - templates/vue-app.php | 7 +- 40 files changed, 949 insertions(+), 567 deletions(-) create mode 100644 resources/vue/components/ContentBar.vue create mode 100644 resources/vue/components/ContentBarBreadcrumbs.vue create mode 100644 resources/vue/components/ContentBarTableOfContents.vue create mode 100644 resources/vue/components/ContentBarTocItemList.vue create mode 100644 resources/vue/components/table-of-contents.ts diff --git a/app/controllers/course/wiki.php b/app/controllers/course/wiki.php index 8e72f8a..926f641 100644 --- a/app/controllers/course/wiki.php +++ b/app/controllers/course/wiki.php @@ -30,7 +30,7 @@ class Course_WikiController extends AuthenticatedController } Navigation::activateItem('/course/wiki/start'); - $this->page = new WikiPage($page_id); + $this->page = WikiPage::find($page_id) ?: new WikiPage(); $this->validateWikiPage($this->page, $this->range); $sidebar = Sidebar::Get(); @@ -97,6 +97,13 @@ class Course_WikiController extends AuthenticatedController $sidebar->addWidget($actions); } + $contentbarProps = [ + 'icon' => 'wiki', + 'isContentBar' => true + ]; + + $toc = CoreWiki::getTOC($this->page); + if (!$this->page->isNew()) { //then the wiki is not empty $search = new SearchWidget($this->searchURL()); @@ -116,12 +123,18 @@ class Course_WikiController extends AuthenticatedController Icon::create('file-pdf') ); $sidebar->addWidget($exports); + + $contentbarProps['toc'] = $toc; } - $startPage = WikiPage::find($this->range->getConfiguration()->WIKI_STARTPAGE_ID); - $this->contentbar = ContentBar::get() - ->setTOC(CoreWiki::getTOC($this->page)) - ->setIcon(Icon::create('wiki')); + // Content bar + $this->contentBarVueApp = \Studip\VueApp::create('ContentBar') + ->withProps($contentbarProps) + ->withComponent('ContentBarBreadcrumbs') + ->withSlot('breadcrumb-list', + sprintf("", json_encode($toc)) + ); + if (!$this->page->isNew()) { $author = _('unbekannt'); if ($this->page->user) { @@ -132,8 +145,8 @@ class Course_WikiController extends AuthenticatedController ); } - $this->contentbar->setInfoHTML(sprintf( - _('Version %1$s, geändert von %2$s
am %3$s'), + $this->contentBarVueApp = $this->contentBarVueApp->withSlot('info-text', sprintf( + _('Version %1$s, geändert von %2$s am %3$s'), $this->page->versionnumber, $author, date('d.m.Y H:i:s', $this->page['chdate']) @@ -167,13 +180,7 @@ class Course_WikiController extends AuthenticatedController ); } } - $action_menu->addLink( - '#', - _('Als Vollbild anzeigen'), - Icon::create('screen-full'), - ['class' => 'fullscreen-trigger hidden-medium-down'] - ); - $this->contentbar->setActionMenu($action_menu); + $this->contentBarVueApp = $this->contentBarVueApp->withSlot('menu', $action_menu->render()); } } @@ -478,10 +485,6 @@ class Course_WikiController extends AuthenticatedController ORDER BY Nachname, Vorname", [$page->id] ); - $this->contentbar = ContentBar::get() - ->setTOC(CoreWiki::getTOC($page)) - ->setIcon(Icon::create('wiki')) - ->setInfoHTML(_('Zuletzt gespeichert') .': '. ''); } public function apply_editing_action(WikiPage $page) @@ -715,15 +718,24 @@ class Course_WikiController extends AuthenticatedController Navigation::activateItem('/course/wiki/start'); Sidebar::Get()->addWidget($this->getViewsWidget($version->page, 'history')); - $startPage = WikiPage::find($this->range->getConfiguration()->WIKI_STARTPAGE_ID); - $this->contentbar = ContentBar::get() - ->setTOC(CoreWiki::getTOC($version->page)) - ->setIcon(Icon::create('wiki')) - ->setInfoHTML(sprintf( + + $toc = CoreWiki::getTOC($version->page); + $this->contentBarVueApp = \Studip\VueApp::create('ContentBar') + ->withProps([ + 'icon' => 'wiki', + 'isContentBar' => true, + 'toc' => $toc, + ]) + ->withSlot('info-text', sprintf( _('Version %1$s vom %2$s'), $version->versionnumber, date('d.m.Y H:i:s', $version['mkdate']) - )); + )) + ->withComponent('ContentBarBreadcrumbs') + ->withSlot('breadcrumb-list', sprintf( + "", + json_encode($toc)) + ); } public function blame_action(WikiPage $page) diff --git a/app/controllers/oer/market.php b/app/controllers/oer/market.php index 72133d7..69030c2 100644 --- a/app/controllers/oer/market.php +++ b/app/controllers/oer/market.php @@ -328,10 +328,11 @@ class Oer_MarketController extends StudipController } } - $this->contentbar = ContentBar::get() - ->setTOC(new TOCItem($this->material['name'])) - ->setInfoHTML(htmlReady($infotext)) - ->setIcon(Icon::create('oer-campus')); + $this->contentBarVueApp = \Studip\VueApp::create('ContentBar')->withProps([ + 'title' => $this->material['name'], + 'icon' => 'oer-campus', + 'isContentBar' => true, + ])->withSlot('info-text', htmlReady($infotext)); } public function embed_action($material_id) diff --git a/app/views/course/wiki/edit.php b/app/views/course/wiki/edit.php index 983a02c..40e0111 100644 --- a/app/views/course/wiki/edit.php +++ b/app/views/course/wiki/edit.php @@ -3,12 +3,9 @@ * @var WikiPage $page * @var Course_WikiController $controller * @var WikiOnlineEditingUser $me_online - * @var ContentBar $contentbar */ ?> - - withProps([ 'cancel-url' => $controller->leave_editingURL($page), @@ -18,5 +15,6 @@ 'page-id' => (int) $page->id, 'save-url' => $controller->saveURL($page), 'users' => $page->getOnlineUsers(), + 'toc' => CoreWiki::getTOC($page), ]) ?> diff --git a/app/views/course/wiki/page.php b/app/views/course/wiki/page.php index e3c67cf..21ae031 100644 --- a/app/views/course/wiki/page.php +++ b/app/views/course/wiki/page.php @@ -4,10 +4,11 @@ * @var string $edit_perms * @var Context $range * @var Course_WikiController $controller + * @var \Studip\VueApp $contentBarVueApp */ -echo $contentbar; ?> +render() ?> isEditable()) : ?>
diff --git a/app/views/course/wiki/version.php b/app/views/course/wiki/version.php index 26094ae..177d7d0 100644 --- a/app/views/course/wiki/version.php +++ b/app/views/course/wiki/version.php @@ -1,4 +1,10 @@ - + + +render() ?>
diff --git a/app/views/oer/market/details.php b/app/views/oer/market/details.php index 9e1c60b..c05bbdf 100644 --- a/app/views/oer/market/details.php +++ b/app/views/oer/market/details.php @@ -1,4 +1,9 @@ - + +render() ?> getDownloadUrl() ?> diff --git a/lib/classes/Debug/VueCollector.php b/lib/classes/Debug/VueCollector.php index a2d90f3..0dd479b 100644 --- a/lib/classes/Debug/VueCollector.php +++ b/lib/classes/Debug/VueCollector.php @@ -45,6 +45,23 @@ final class VueCollector extends DataCollector implements Renderable } } + $slots = $this->app->getSlots(); + if (count($slots) > 0) { + ksort($slots); + + $data['== SLOTS =='] = count($slots) . ' items'; + foreach ($slots as $key => $value) { + $data[$key] = $this->dumpVar($value); + } + } + + $components = $this->app->getComponents(); + ksort($components); + $data['== COMPONENTS =='] = count($components) . ' items'; + foreach ($components as $value) { + $data[$value] = ''; + } + return $data; } diff --git a/lib/classes/Icon.php b/lib/classes/Icon.php index 6c586a0..fd2a25b 100644 --- a/lib/classes/Icon.php +++ b/lib/classes/Icon.php @@ -9,7 +9,7 @@ * @license GPL2 or any later version * @since 3.2 */ -class Icon +class Icon implements JsonSerializable { const SVG = 1; const CSS_BACKGROUND = 4; @@ -196,6 +196,11 @@ class Icon return $this->asImg(); } + public function jsonSerialize(): mixed + { + return get_object_vars($this); + } + /** * Renders the icon inside an img html tag. * diff --git a/lib/classes/VueApp.php b/lib/classes/VueApp.php index 76c9a52..c26b011 100644 --- a/lib/classes/VueApp.php +++ b/lib/classes/VueApp.php @@ -43,6 +43,7 @@ final class VueApp implements Stringable private array $slots = []; private array $stores = []; private array $storeData = []; + private array $components = []; /** * Private constructor since we want to enforce the use of VueApp::create(). @@ -50,6 +51,7 @@ final class VueApp implements Stringable private function __construct( private readonly string $base_component ) { + $this->components[] = $base_component; } /** @@ -153,12 +155,30 @@ final class VueApp implements Stringable } /** + * Registers a component for use e.g. in slots. + */ + public function withComponent(string $component): VueApp + { + $clone = clone $this; + $clone->components[] = $component; + return $clone; + } + + /** + * Returns all components + */ + public function getComponents(): array + { + return $this->components; + } + + /** * Returns the template to render the vue app */ public function getTemplate(): Template { $data = [ - 'components' => [$this->base_component], + 'components' => $this->components, ]; if (count($this->stores) > 0) { @@ -174,6 +194,7 @@ final class VueApp implements Stringable $template->attributes = ['data-vue-app' => json_encode($data)]; $template->props = $this->getPreparedProps(); $template->storeData = $this->storeData; + $template->slots = $this->getSlots(); return $template; } diff --git a/lib/models/WikiPage.php b/lib/models/WikiPage.php index 817dfae..bb41d1b 100644 --- a/lib/models/WikiPage.php +++ b/lib/models/WikiPage.php @@ -202,32 +202,6 @@ class WikiPage extends SimpleORMap implements PrivacyObject /** - * Returns the start page of a wiki for a given course. The start page has - * the keyword 'WikiWikiWeb'. - * - * @param string $range_id Course id - * @return WikiPage - */ - public static function getStartPage($range_id): WikiPage - { - $page_id = CourseConfig::get($range_id)->WIKI_STARTPAGE_ID; - - if ($page_id) { - return self::find($page_id); - } - - $page = new WikiPage(); - $page->setValue('content', _('Dieses Wiki ist noch leer.')); - if ($page->isEditable()) { - $page->setValue( - 'content', - $page->getValue('content') . ' ' . _("Bearbeiten Sie es!\nNeue Seiten oder Links werden einfach durch Eingeben von [nop][[Wikinamen]][/nop] in doppelten eckigen Klammern angelegt.") - ); - } - return $page; - } - - /** * Export available data of a given user into a storage object * (an instance of the StoredUserData class) for that user. * diff --git a/lib/modules/CoreWiki.php b/lib/modules/CoreWiki.php index a447451..b544af3 100644 --- a/lib/modules/CoreWiki.php +++ b/lib/modules/CoreWiki.php @@ -178,28 +178,45 @@ class CoreWiki extends CorePlugin implements StudipModule /** - * Generates a page hierarchy for table of contents/breadcrumbs. - * @return TOCItem + * Generates a TOCItem tree containing all pages in the currently opened wiki + * for use in table of contents/breadcrumbs. + * To prevent cyclic data references, the TOCItems in the tree do not contain + * references to their parent pages. + * This allows the resultant TOCItem to be serialized via json_decode for + * use in Vue. + * + * @param $activePage WikiPage The page that the user has currently navigated to. + * @return TOCItem A TOCItem for the root of the wiki and all of its descendants. */ - public static function getTOC($page, $first = true): TOCItem + public static function getTOC(WikiPage $activePage): TOCItem { - $root = new TOCItem( - ($page && ($page->isNew() || $page->name === 'WikiWikiWeb')) - ? _('Wiki-Startseite') - : $page->name - ); - $root->setURL(URLHelper::getURL('dispatch.php/course/wiki/page/'.$page->id)); - if ($page->name == 'WikiWikiWeb' || $page->id == CourseConfig::get($page->range_id)->WIKI_STARTPAGE_ID) { - $root->setIcon(Icon::create('wiki')); - } - $root->setActive($first); + $rootId = CourseConfig::get(Context::getId())->WIKI_STARTPAGE_ID; + $rootPage = WikiPage::find($rootId) ?: $activePage; - if ($page->parent) { - $parent = self::getTOC($page->parent, false); - $root->setParent($parent); - } + $rootToc = self::getTOCRecursive($rootPage, $activePage->page_id); + $rootToc->setTitle(_('Wiki-Startseite')); + $rootToc->setIcon(Icon::create('wiki')); + return $rootToc; + } - return $root; + /** + * Using a recursive depth-first traversal of the wiki page hierarchy, + * create a TOCItem tree starting at the given $page. + * + * @param WikiPage $page The currently visited page in the traversal. + * @param int|null $active_page_id The id of the page that the user has navigated to. + * @return TOCItem A TOCItem for the given $page and all of its descendants + */ + private static function getTOCRecursive(WikiPage $page, int|null $active_page_id): TOCItem + { + $toc = new TOCItem($page->isNew() ? _('Wiki-Startseite') : $page->name); + $toc->setURL($page->isNew() ? URLHelper::getURL('dispatch.php/course/wiki/page') : URLHelper::getURL('dispatch.php/course/wiki/page/' . $page->id)); + $toc->setActive($page->page_id === $active_page_id); + foreach ($page->children as $child) { + $childToc = self::getTOCRecursive($child, $active_page_id); + $toc->children[] = $childToc; + } + return $toc; } } diff --git a/resources/assets/javascripts/lib/actionmenu.js b/resources/assets/javascripts/lib/actionmenu.js index a81f78a..da85e33 100644 --- a/resources/assets/javascripts/lib/actionmenu.js +++ b/resources/assets/javascripts/lib/actionmenu.js @@ -103,7 +103,11 @@ class ActionMenu // Reposition the menu? if (position) { - let parents = getScrollableParents(this.element, menu_width, menu_height); + let parents = getScrollableParents(this.element, menu_width, menu_height) + // Prevent us from appending the actionmenu outside of the . + // (If it's appended outside of , some CSS rules will not + // be applied, and the Z-ordering will be incorrect.) + .filter(parent => parent !== document.documentElement); if (parents.length > 0) { const form = this.element.closest('form'); if (form) { diff --git a/resources/assets/stylesheets/scss/contentbar.scss b/resources/assets/stylesheets/scss/contentbar.scss index 3b0aff3..338737b 100644 --- a/resources/assets/stylesheets/scss/contentbar.scss +++ b/resources/assets/stylesheets/scss/contentbar.scss @@ -2,7 +2,6 @@ background-color: var(--dark-gray-color-10); display: flex; flex-wrap: nowrap; - height: auto; align-items: center; justify-content: flex-start; margin-bottom: 15px; @@ -85,6 +84,9 @@ margin: 0 7px; @-moz-document url-prefix() { + &.contentbar-toc-wrapper { + margin-top: -4px; + } &.contentbar-action-menu-wrapper { margin-top: 2px; } diff --git a/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss b/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss index 457cd4d..28ecfbe 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss @@ -14,17 +14,13 @@ $consum_ribbon_width: calc(100% - 58px); } .cw-ribbon-wrapper-consume { - position: fixed; - padding-top: 15px; + padding-top: 7px; padding-bottom: 15px; background-color: var(--white); - width: $consum_ribbon_width; - height: 46px; z-index: 42; } .cw-ribbon-consume-bottom { - position: fixed; top: 75px; height: 15px; left: 0; @@ -56,38 +52,46 @@ $consum_ribbon_width: calc(100% - 58px); } .cw-ribbon { + position: relative; display: flex; flex-wrap: wrap; - height: auto; - min-height: 30px; + align-items: center; + height: 60px; + min-height: 60px; margin-bottom: 15px; - padding: 1em; + padding: 0 2em; justify-content: space-between; background-color: var(--dark-gray-color-10); &.cw-ribbon-sticky { position: fixed; top: 50px; - width: calc(100% - 346px); + width: calc(100% - 375px); z-index: 40; } &.cw-ribbon-consume { width: $consum_ribbon_width; - position: fixed; margin-bottom: 0; } .cw-ribbon-wrapper-left { display: flex; - max-width: calc(100% - 106px); + gap: 1em; + min-width: 0; + width: 100%; + flex: 1; .cw-ribbon-nav { display: flex; - min-width: 75px; + align-items: center; + + .contentbar-icon { + margin: 0 15px 0 10px; - &.single-icon { - min-width: 45px; + img { + vertical-align: middle; + } } } @@ -138,11 +142,18 @@ $consum_ribbon_width: calc(100% - 58px); } } } + + .cw-ribbon-info-text { + font-size: small; + line-height: 1em; + } } } .cw-ribbon-wrapper-right { display: flex; + gap: 0.5em; + align-items: center; button { border: none; @@ -174,7 +185,7 @@ $consum_ribbon_width: calc(100% - 58px); &.cw-ribbon-button-next { @include background-icon(arr_1right, clickable, 24); - margin: 0 1em 0 0; + margin: 0 0.5em 0 0; } &.cw-ribbon-button-prev-disabled { @@ -185,15 +196,13 @@ $consum_ribbon_width: calc(100% - 58px); &.cw-ribbon-button-next-disabled { @include background-icon(arr_1right, inactive, 24); - margin: 0 1em 0 0; + margin: 0 0.5em 0 0; cursor: default; } } } .cw-ribbon-action-menu { - vertical-align: text-top; - margin: 2px 0 0 2px; &.is-open { z-index: 32; } @@ -204,26 +213,25 @@ $consum_ribbon_width: calc(100% - 58px); border: solid thin var(--content-color-40); box-shadow: 2px 2px var(--dark-gray-color-30); position: absolute; - right: -570px; - top: 15px; + right: 0; + top: 0; height: 100%; max-width: calc(100% - 28px); display: flex; flex-flow: row; - transition: right 0.8s; z-index: 42; - &.unfold { - right: 0px; - margin-right: 15px; - } - &.cw-ribbon-tools-consume { - position: fixed; + right: 15px; + } - &.unfold { - right: 15px; - } + &.cw-ribbon-slide-enter-active, + &.cw-ribbon-slide-leave-active { + transition: transform var(--transition-duration-superslow); + } + &.cw-ribbon-slide-enter, + &.cw-ribbon-slide-leave-to { + transform: translateX(calc(100% + 30px)); } &.cw-ribbon-tools-sticky { @@ -274,6 +282,7 @@ $consum_ribbon_width: calc(100% - 58px); > .cw-tabs-nav { border: none; + height: 57px; width: calc(100% - 48px); > button { @@ -281,7 +290,7 @@ $consum_ribbon_width: calc(100% - 58px); max-width: unset; flex-grow: 0.5; &::after { - margin-top: 16px; + margin-top: 20px; } } } @@ -348,7 +357,7 @@ $consum_ribbon_width: calc(100% - 58px); .cw-structural-element-consumemode { .cw-ribbon-tools { - top: 25px; + top: 18px; } } @@ -370,3 +379,48 @@ $consum_ribbon_width: calc(100% - 58px); height: 75px; } } + +// Rules extracted from PHP contentbar for use in ContentBar.vue +.contentbar-button-wrapper { + height: 24px; + margin: 0 7px; + + @-moz-document url-prefix() { + &.contentbar-toc-wrapper { + margin-top: -4px; + } + &.contentbar-action-menu-wrapper { + margin-top: 2px; + } + } + + .contentbar-button, + .cw-ribbon-button { + background-color: transparent; + background-position: center; + background-repeat: no-repeat; + background-size: 24px; + border: none; + cursor: pointer; + display: inline-block; + height: 24px; + width: 24px; + + &.contentbar-button-menu, + &.cw-ribbon-button-menu { + @include background-icon(table-of-contents, clickable, 24); + } + + &.contentbar-button-zoom::before { + left: -5px; + position: relative; + top: -2px; + } + + @-moz-document url-prefix() { + &.contentbar-button-zoom::before { + top: -3px; + } + } + } +} diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tabs.scss b/resources/assets/stylesheets/scss/courseware/layouts/tabs.scss index cf28364..023b31d 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/tabs.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/tabs.scss @@ -24,7 +24,7 @@ &::after { display: block; margin-top: 4px; - margin-bottom: -5px; + margin-bottom: -12px; margin-left: -14px; width: calc(100% + 28px); content: ''; diff --git a/resources/assets/stylesheets/scss/responsive.scss b/resources/assets/stylesheets/scss/responsive.scss index c2ca5c3..0a84d3e 100644 --- a/resources/assets/stylesheets/scss/responsive.scss +++ b/resources/assets/stylesheets/scss/responsive.scss @@ -329,7 +329,7 @@ $sidebarOut: -330px; &.responsive-show { animation: slide-in var(--transition-duration) forwards; position: sticky; - top: 100px; + top: 110px; visibility: visible; } @@ -383,10 +383,6 @@ $sidebarOut: -330px; } #responsive-contentbar { - justify-content: stretch; - margin-bottom: 15px; - padding-bottom: 0.5em; - .contentbar-nav, .cw-ribbon-nav { .contentbar-button { @@ -407,71 +403,20 @@ $sidebarOut: -330px; } } - } - - .contentbar-wrapper-left { - flex: 1; - max-width: calc(100% - 70px); - min-width: 0; - width:100%; - - & > .contentbar-icon { - margin-right: 15px; - } - - .contentbar-breadcrumb { - font-size: $font-size-large; - - > img { - margin-left: 15px; - width: 24px; - } - - > span { - display: inline; - flex-shrink: 10000; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } - - > .contentbar-wrapper-right { - flex: 0; - left: 5px; - position: relative; - - .contentbar-button, - nav { - position: relative; + img { + vertical-align: middle; } } - &.cw-ribbon { - .cw-ribbon-tools { - max-width: calc(100% - 2px); - top: 0; - margin-right: 0; - } + .cw-ribbon-tools { + right: 16px; } - &.cw-ribbon-sticky { - position: unset; - width: calc(100vw - 30px); - } } #toc { - max-width: 100vw; - position: absolute; - right: -8px; - top: -21px; - } - - #toc_header { - height: 47px; + margin-right: -2px; + margin-top: -7px; } #main-footer { @@ -509,7 +454,30 @@ $sidebarOut: -330px; #responsive-contentbar { &.cw-ribbon-sticky { position: unset; - width: calc(100vw - 30px); + width: unset; + } + + .cw-ribbon-breadcrumb { + max-width: 100%; + min-width: unset; + } + + .cw-ribbon-info-text, + .contentbar-icon { + display: none; + } + + &:not(.cw-ribbon) { + .contentbar-wrapper-left { + flex: 1; + max-width: 100%; + text-overflow: ellipsis; + } + } + + .cw-ribbon-tools { + max-width: 100vw; + right: 0; } } @@ -521,10 +489,9 @@ $sidebarOut: -330px; height: calc(100% - 100px); overflow-y: auto; position: fixed; - top: 75px; transform: translateX($sidebarOut); -webkit-transform: translateX($sidebarOut); - top: 80px; + top: 110px; z-index: 100; &.responsive-show { @@ -536,6 +503,12 @@ $sidebarOut: -330px; } } + #toc { + margin-right: -16px; + max-width: 100vw; + width: 100vw; + } + #system-notifications { top: 0; position: fixed; @@ -619,13 +592,13 @@ $sidebarOut: -330px; .contentbar-nav, .cw-ribbon-nav { - margin-left: -8px; + margin-left: -6px; } } #content-wrapper { flex: 1; - margin-top: 75px; + margin-top: 55px; min-height: calc(100vh - 150px); } } @@ -686,11 +659,8 @@ $sidebarOut: -330px; } #toc { - position: absolute; - right: -29px; - top: -25px; + margin-right: 12px; } - } html:not(.responsive-display):not(.fullscreen-mode) { diff --git a/resources/assets/stylesheets/scss/table_of_contents.scss b/resources/assets/stylesheets/scss/table_of_contents.scss index b361a90..e323c79 100644 --- a/resources/assets/stylesheets/scss/table_of_contents.scss +++ b/resources/assets/stylesheets/scss/table_of_contents.scss @@ -15,45 +15,57 @@ ul.numberedchapters { display: none; } -#cb-toc:checked + .check-box + #cb-toc-close + article.toc_overview, button#toc-button:hover article.toc_overview { - visibility: visible; - width: 540px; - overflow: hidden; -} - -#cb-toc-close:checked article.toc_overview { - visibility: hidden; - width: 0; -} - -.toc_overview { - visibility: hidden; - width: 0%; +#toc { + margin: 11px; + text-align: left; z-index: 100; position: absolute; - right: -22px; - top: -25px; + right: -10px; + top: -11px; background-color: var(--white); border: 1px solid var(--content-color-40); margin-bottom: 10px; box-shadow: 2px 2px var(--dark-gray-color-30); - + width: min(100%, 540px); > section { max-width: 100%; overflow-y: scroll; height: 580px; margin-top: 7px; + padding: 5px 15px; } -} -#toc { - margin: 10px; - text-align: left; + .toc-hide-button { + position: absolute; + border: none; + height: 36px; + width: 24px; + min-width: 24px; + margin-right: 1em; + padding: 0 4px; + right: 0; + top: 12px; + cursor: pointer; + @include background-icon(decline, clickable, 24); + background-repeat: no-repeat; + background-size: 24px; + background-position: center right; + background-color: var(--white); + } + + &.cw-ribbon-slide-enter-active, + &.cw-ribbon-slide-leave-active { + transition: transform var(--transition-duration-superslow); + } + &.cw-ribbon-slide-enter, + &.cw-ribbon-slide-leave-to { + transform: translateX(calc(100% + 30px)); + } } #toc_header { - height: 58px; + height: 57px; overflow: hidden; background-color: var(--white); color: var(--black); @@ -73,14 +85,10 @@ ul.numberedchapters { #toc_h1 { color: var(--black); font-weight: 500; - margin-left: 10px; + margin-left: 25px; margin-bottom: unset; } -.toc_transform { - transition: all var(--transition-duration) ease; -} - #main_content { opacity: 1; @@ -121,6 +129,13 @@ section > .toc { img, svg { vertical-align: bottom; } + + a, + span { + img { + margin-right: 10px; + } + } } li#chap1 { @@ -196,10 +211,6 @@ section > .toc { width:375px; } - #toc { - max-width: 94%; - } - ul.breadcrumb { list-style: none; font-size: 18px; diff --git a/resources/studip.d.ts b/resources/studip.d.ts index 5ec8fef..ec5c065 100644 --- a/resources/studip.d.ts +++ b/resources/studip.d.ts @@ -22,3 +22,24 @@ export interface InstalledLanguage { picture: string; selected: boolean; } + +enum iconTypes { + 'accept', + 'attention', + 'clickable', + 'inactive', + 'info', + 'info_alt', + 'navigation', + 'new', + 'sort', + 'status-green', + 'status-red', + 'status-yellow' +} + +export interface Icon { + role: iconTypes; + shape: string; + attributes: Record; +} diff --git a/resources/vue/components/ContentBar.vue b/resources/vue/components/ContentBar.vue new file mode 100644 index 0000000..539c582 --- /dev/null +++ b/resources/vue/components/ContentBar.vue @@ -0,0 +1,163 @@ + + + diff --git a/resources/vue/components/ContentBarBreadcrumbs.vue b/resources/vue/components/ContentBarBreadcrumbs.vue new file mode 100644 index 0000000..35606b4 --- /dev/null +++ b/resources/vue/components/ContentBarBreadcrumbs.vue @@ -0,0 +1,78 @@ + + + diff --git a/resources/vue/components/ContentBarTableOfContents.vue b/resources/vue/components/ContentBarTableOfContents.vue new file mode 100644 index 0000000..525c4d6 --- /dev/null +++ b/resources/vue/components/ContentBarTableOfContents.vue @@ -0,0 +1,63 @@ + + + diff --git a/resources/vue/components/ContentBarTocItemList.vue b/resources/vue/components/ContentBarTocItemList.vue new file mode 100644 index 0000000..15101c4 --- /dev/null +++ b/resources/vue/components/ContentBarTocItemList.vue @@ -0,0 +1,33 @@ + + + diff --git a/resources/vue/components/WikiEditor.vue b/resources/vue/components/WikiEditor.vue index 069b2ff..3b15bb0 100644 --- a/resources/vue/components/WikiEditor.vue +++ b/resources/vue/components/WikiEditor.vue @@ -1,5 +1,14 @@ diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue index 2a92579..fe99808 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue @@ -2,7 +2,7 @@
@@ -49,6 +49,7 @@ import CoursewareToolsContents from './CoursewareToolsContents.vue'; import CoursewareToolsUnits from './CoursewareToolsUnits.vue'; import { FocusTrap } from 'focus-trap-vue'; import { mapActions, mapGetters } from 'vuex'; +import { store } from "../../../../assets/javascripts/chunks/vue"; export default { name: 'courseware-ribbon-toolbar', @@ -60,16 +61,6 @@ export default { FocusTrap, }, props: { - toolsActive: Boolean, - canEdit: Boolean, - disableSettings: { - type: Boolean, - default: false, - }, - disableAdder: { - type: Boolean, - default: false, - }, stickyRibbon: { type: Boolean, default: false, @@ -84,9 +75,11 @@ export default { }; }, computed: { + consumeMode() { + return store.state.studip.consumeMode; + }, ...mapGetters({ userIsTeacher: 'userIsTeacher', - consumeMode: 'consumeMode', containerAdder: 'containerAdder', adderStorage: 'blockAdder', viewMode: 'viewMode', @@ -108,28 +101,25 @@ export default { coursewareContainerAdder: 'coursewareContainerAdder', }), scrollToCurrent() { - setTimeout(() => { - let contents = this.$refs.contents.$el; - let current = contents.querySelector('.cw-tree-item-link-current'); - if (current) { - contents.scroll({ top: current.offsetTop - 4, behavior: 'smooth' }); - } - }, 360); + let contents = this.$refs.contents.$el; + let current = contents.querySelector('.cw-tree-item-link-current'); + if (current) { + contents.scroll({ top: current.offsetTop - 4, behavior: 'smooth' }); + } }, - }, - mounted () { - this.scrollToCurrent(); - }, - watch: { - toolsActive(newValue) { + activate() { const focusElement = this.$refs.tabs.getTabButtonByAlias(this.selectedToolbarItem); - if (newValue && focusElement) { - setTimeout(() => { - this.initialFocusElement = focusElement; - this.trap = true; - }, 300); + if (focusElement) { + this.initialFocusElement = focusElement; + this.trap = true; } }, }, + mounted() { + this.$nextTick(() => { + this.activate(); + this.$nextTick(() => this.scrollToCurrent()); + }); + }, }; diff --git a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue index 6efbca2..5d7547d 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue @@ -1,14 +1,10 @@