Skip to main content

Neue Seite

<style>
#bs-tree-menu {
    position: fixed;
    left: 0;
    top: 70px;
    bottom: 0;
    width: 320px;
    overflow-y: auto;
    background: #fff;
    border-right: 1px solid #ddd;
    z-index: 9999;
    padding: 12px;
    font-size: 14px;
}

#bs-tree-menu ul {
    list-style: none;
    margin: 0;
    padding-left: 18px;
}

#bs-tree-menu > ul {
    padding-left: 0;
}

#bs-tree-menu li {
    margin: 2px 0;
}

#bs-tree-menu .tree-row {
    display: flex;
    align-items: center;
    gap: 5px;
    min-height: 28px;
}

#bs-tree-menu button {
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    font-size: 15px;
}

#bs-tree-menu a {
    color: #333;
    text-decoration: none;
}

#bs-tree-menu a:hover {
    text-decoration: underline;
}

#bs-tree-menu .children {
    display: none;
}

#bs-tree-menu .open > .children {
    display: block;
}

#bs-tree-menu .page {
    padding-left: 27px;
}

#bs-tree-menu .error {
    color: #b00020;
    padding: 10px;
}
</style>

<div id="bs-tree-menu">
    <strong>CONTENT</strong>
    <div id="bs-tree-status">Loading…</div>
    <ul id="bs-tree"></ul>
</div>

<script>
(async function () {
    const root = document.getElementById('bs-tree');
    const status = document.getElementById('bs-tree-status');

    const api = async (url) => {
        const response = await fetch(url, {
            credentials: 'same-origin',
            headers: {
                'Accept': 'application/json'
            }
        });

        if (!response.ok) {
            throw new Error(url + ' → HTTP ' + response.status);
        }

        return response.json();
    };

    const all = async (endpoint) => {
        let page = 1;
        let result = [];

        while (true) {
            const data = await api(
                '/api/' + endpoint + '?count=500&page=' + page
            );

            const rows = data.data || [];
            result = result.concat(rows);

            if (rows.length < 500) {
                break;
            }

            page++;
        }

        return result;
    };

    const esc = (value) => {
        const div = document.createElement('div');
        div.textContent = value || '';
        return div.innerHTML;
    };

    const node = (name, url, children, expandable) => {
        const li = document.createElement('li');

        const row = document.createElement('div');
        row.className = 'tree-row';

        if (expandable) {
            const button = document.createElement('button');
            button.textContent = '+';

            button.addEventListener('click', () => {
                li.classList.toggle('open');
                button.textContent = li.classList.contains('open') ? '−' : '+';
            });

            row.appendChild(button);
        } else {
            const spacer = document.createElement('span');
            spacer.style.width = '22px';
            row.appendChild(spacer);
        }

        const link = document.createElement('a');
        link.href = url;
        link.innerHTML = esc(name);

        row.appendChild(link);
        li.appendChild(row);

        if (children) {
            children.className = 'children';
            li.appendChild(children);
        }

        return li;
    };

    const list = () => {
        const ul = document.createElement('ul');
        return ul;
    };

    try {
        status.remove();

        const shelves = await all('shelves');
        const books = await all('books');
        const chapters = await all('chapters');
        const pages = await all('pages');

        const booksByShelf = {};
        const chaptersByBook = {};
        const pagesByBook = {};

        books.forEach(book => {
            const shelfId = book.shelf_id;

            if (!booksByShelf[shelfId]) {
                booksByShelf[shelfId] = [];
            }

            booksByShelf[shelfId].push(book);
        });

        chapters.forEach(chapter => {
            if (!chaptersByBook[chapter.book_id]) {
                chaptersByBook[chapter.book_id] = [];
            }

            chaptersByBook[chapter.book_id].push(chapter);
        });

        pages.forEach(page => {
            if (!pagesByBook[page.book_id]) {
                pagesByBook[page.book_id] = [];
            }

            pagesByBook[page.book_id].push(page);
        });

        shelves.forEach(shelf => {
            const shelfChildren = list();

            const shelfBooks = booksByShelf[shelf.id] || [];

            shelfBooks.sort((a, b) =>
                (a.priority || 0) - (b.priority || 0)
            );

            shelfBooks.forEach(book => {
                const bookChildren = list();

                const bookChapters =
                    chaptersByBook[book.id] || [];

                bookChapters.sort((a, b) =>
                    (a.priority || 0) - (b.priority || 0)
                );

                bookChapters.forEach(chapter => {
                    const chapterChildren = list();

                    const chapterPages =
                        pages.filter(page =>
                            page.chapter_id === chapter.id
                        );

                    chapterPages.sort((a, b) =>
                        (a.priority || 0) - (b.priority || 0)
                    );

                    chapterPages.forEach(page => {
                        chapterChildren.appendChild(
                            node(
                                page.name,
                                '/books/' +
                                book.slug +
                                '/chapter/' +
                                chapter.slug +
                                '/page/' +
                                page.slug,
                                null,
                                false
                            )
                        );
                    });

                    bookChildren.appendChild(
                        node(
                            chapter.name,
                            '/books/' +
                            book.slug +
                            '/chapter/' +
                            chapter.slug,
                            chapterChildren,
                            chapterPages.length > 0
                        )
                    );
                });

                const bookPages =
                    pagesByBook[book.id] || [];

                bookPages
                    .filter(page => !page.chapter_id)
                    .sort((a, b) =>
                        (a.priority || 0) - (b.priority || 0)
                    )
                    .forEach(page => {
                        bookChildren.appendChild(
                            node(
                                page.name,
                                '/books/' +
                                book.slug +
                                '/page/' +
                                page.slug,
                                null,
                                false
                            )
                        );
                    });

                const expandable =
                    bookChildren.children.length > 0;

                shelfChildren.appendChild(
                    node(
                        book.name,
                        '/books/' + book.slug,
                        bookChildren,
                        expandable
                    )
                );
            });

            root.appendChild(
                node(
                    shelf.name,
                    '/shelves/' + shelf.slug,
                    shelfChildren,
                    shelfBooks.length > 0
                )
            );
        });

    } catch (error) {
        console.error(error);

        const errorBox = document.createElement('div');
        errorBox.className = 'error';
        errorBox.textContent =
            'BookStack structure could not be loaded: ' +
            error.message;

        root.appendChild(errorBox);
    }
})();
</script>
CONTENT
Loading…