mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
Merge branch 'master' into next50
This commit is contained in:
commit
ffdd917717
5734
libraries/codemirror/keymap/vim.js
vendored
Normal file
5734
libraries/codemirror/keymap/vim.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.49.1-beta",
|
||||
"version": "0.49.2-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
|
@ -1,7 +1,15 @@
|
||||
import mimeTypesService from "../../services/mime_types.js";
|
||||
import options from "../../services/options.js";
|
||||
import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = `
|
||||
<h4>Use vim keybindings in CodeNotes (no ex mode)</h4>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="vim-keymap-enabled">
|
||||
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label>
|
||||
</div>
|
||||
<h4>Available MIME types in the dropdown</h4>
|
||||
|
||||
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>`;
|
||||
@ -10,12 +18,18 @@ export default class CodeNotesOptions {
|
||||
constructor() {
|
||||
$("#options-code-notes").html(TPL);
|
||||
|
||||
this.$vimKeymapEnabled = $("#vim-keymap-enabled");
|
||||
this.$vimKeymapEnabled.on('change', () => {
|
||||
const opts = { 'vimKeymapEnabled': this.$vimKeymapEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
return false;
|
||||
});
|
||||
this.$mimeTypes = $("#options-mime-types");
|
||||
}
|
||||
|
||||
async optionsLoaded() {
|
||||
async optionsLoaded(options) {
|
||||
this.$mimeTypes.empty();
|
||||
|
||||
this.$vimKeymapEnabled.prop("checked", options['vimKeymapEnabled'] === 'true');
|
||||
let idCtr = 1;
|
||||
|
||||
for (const mimeType of await mimeTypesService.getMimeTypes()) {
|
||||
@ -45,4 +59,4 @@ export default class CodeNotesOptions {
|
||||
|
||||
mimeTypesService.loadMimeTypes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ const CODE_MIRROR = {
|
||||
"libraries/codemirror/addon/edit/matchtags.js",
|
||||
"libraries/codemirror/addon/search/match-highlighter.js",
|
||||
"libraries/codemirror/mode/meta.js",
|
||||
"libraries/codemirror/keymap/vim.js",
|
||||
"libraries/codemirror/addon/lint/lint.js",
|
||||
"libraries/codemirror/addon/lint/eslint.js"
|
||||
],
|
||||
|
@ -129,12 +129,7 @@ export default class TabManager extends Component {
|
||||
window.history.pushState(null, "", url);
|
||||
}
|
||||
|
||||
const titleFragments = [
|
||||
// it helps navigating in history if note title is included in the title
|
||||
activeNoteContext.note?.title,
|
||||
"Trilium Notes"
|
||||
].filter(Boolean);
|
||||
document.title = titleFragments.join(" - ");
|
||||
this.updateDocumentTitle(activeNoteContext);
|
||||
|
||||
this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event
|
||||
}
|
||||
@ -453,4 +448,22 @@ export default class TabManager extends Component {
|
||||
hoistedNoteChangedEvent() {
|
||||
this.tabsUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
updateDocumentTitle(activeNoteContext) {
|
||||
const titleFragments = [
|
||||
// it helps navigating in history if note title is included in the title
|
||||
activeNoteContext.note?.title,
|
||||
"Trilium Notes"
|
||||
].filter(Boolean);
|
||||
|
||||
document.title = titleFragments.join(" - ");
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
const activeContext = this.getActiveContext();
|
||||
|
||||
if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) {
|
||||
this.updateDocumentTitle(activeContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
src/public/app/share.js
Normal file
21
src/public/app/share.js
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Fetch note with given ID from backend
|
||||
*
|
||||
* @param noteId of the given note to be fetched. If falsy, fetches current note.
|
||||
*/
|
||||
async function fetchNote(noteId = null) {
|
||||
if (!noteId) {
|
||||
noteId = document.body.getAttribute("data-note-id");
|
||||
}
|
||||
|
||||
const resp = await fetch(`api/notes/${noteId}`);
|
||||
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggleMenuButton = document.getElementById('toggleMenuButton');
|
||||
const layout = document.getElementById('layout');
|
||||
|
||||
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
|
||||
}, false);
|
@ -226,6 +226,8 @@ const ATTR_HELP = {
|
||||
"renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered',
|
||||
"widget": "target of this relation will be executed and rendered as a widget in the sidebar",
|
||||
"shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.",
|
||||
"shareJs": "JavaScript note which will be injected into the share page. JS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
|
||||
"shareFavicon": "Favicon note to be set in the shared page. Typically you want to set it to share root and make it inheritable. Favicon note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -6,6 +6,7 @@ import ws from "../../services/ws.js";
|
||||
import appContext from "../../services/app_context.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import options from "../../services/options.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-code note-detail-printable">
|
||||
@ -94,6 +95,7 @@ export default class EditableCodeTypeWidget extends TypeWidget {
|
||||
viewportMargin: Infinity,
|
||||
indentUnit: 4,
|
||||
matchBrackets: true,
|
||||
keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
|
||||
matchTags: {bothTags: true},
|
||||
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
|
||||
lint: true,
|
||||
|
@ -117,6 +117,15 @@ iframe.pdf-view {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#noteClippedFrom {
|
||||
padding: 10px 0 10px 0;
|
||||
margin: 20px 0 20px 0;
|
||||
color: #666;
|
||||
border: 1px solid #ddd;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
#toggleMenuButton::after {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
|
@ -32,6 +32,7 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'similarNotesWidget',
|
||||
'editedNotesWidget',
|
||||
'calendarWidget',
|
||||
'vimKeymapEnabled',
|
||||
'codeNotesMimeTypes',
|
||||
'spellCheckEnabled',
|
||||
'spellCheckLanguageCode',
|
||||
|
@ -67,6 +67,8 @@ const BUILTIN_ATTRIBUTES = [
|
||||
{ type: 'relation', name: 'widget', isDangerous: true },
|
||||
{ type: 'relation', name: 'renderNote', isDangerous: true },
|
||||
{ type: 'relation', name: 'shareCss', isDangerous: false },
|
||||
{ type: 'relation', name: 'shareJs', isDangerous: false },
|
||||
{ type: 'relation', name: 'shareFavicon', isDangerous: false },
|
||||
];
|
||||
|
||||
/** @returns {Note[]} */
|
||||
|
@ -1 +1 @@
|
||||
module.exports = { buildDate:"2021-12-24T23:05:10+01:00", buildRevision: "0217b1c85de9a2824e7f07d07a357064c5803383" };
|
||||
module.exports = { buildDate:"2022-01-02T22:43:30+01:00", buildRevision: "feffd57f240438d107c1ed1c1772545611a97dee" };
|
||||
|
@ -3,6 +3,10 @@ const sanitizeHtml = require('sanitize-html');
|
||||
// intended mainly as protection against XSS via import
|
||||
// secondarily it (partly) protects against "CSS takeover"
|
||||
function sanitize(dirtyHtml) {
|
||||
if (!dirtyHtml) {
|
||||
return dirtyHtml;
|
||||
}
|
||||
|
||||
// avoid H1 per https://github.com/zadam/trilium/issues/1552
|
||||
// demote H1, and if that conflicts with existing H2, demote that, etc
|
||||
const transformTags = {};
|
||||
|
@ -51,7 +51,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
|
||||
throw new Error("Unrecognized OPML version " + opmlVersion);
|
||||
}
|
||||
|
||||
content = htmlSanitizer.sanitize(content);
|
||||
content = htmlSanitizer.sanitize(content || "");
|
||||
|
||||
const {note} = noteService.createNewNote({
|
||||
parentNoteId,
|
||||
|
@ -1,7 +1,16 @@
|
||||
const becca = require('../becca/becca');
|
||||
const sql = require("./sql.js");
|
||||
|
||||
function getOption(name) {
|
||||
const option = require('../becca/becca').getOption(name);
|
||||
let option;
|
||||
|
||||
if (becca.loaded) {
|
||||
option = becca.getOption(name);
|
||||
}
|
||||
else {
|
||||
// e.g. in initial sync becca is not loaded because DB is not initialized
|
||||
option = sql.getRow("SELECT * FROM options WHERE name = ?", name);
|
||||
}
|
||||
|
||||
if (!option) {
|
||||
throw new Error(`Option "${name}" doesn't exist`);
|
||||
@ -39,12 +48,12 @@ function getOptionBool(name) {
|
||||
}
|
||||
|
||||
function setOption(name, value) {
|
||||
const option = becca.getOption(name);
|
||||
|
||||
if (value === true || value === false) {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
const option = becca.getOption(name);
|
||||
|
||||
if (option) {
|
||||
option.value = value;
|
||||
|
||||
|
@ -53,6 +53,7 @@ const defaultOptions = [
|
||||
{ name: 'imageMaxWidthHeight', value: '2000', isSynced: true },
|
||||
{ name: 'imageJpegQuality', value: '75', isSynced: true },
|
||||
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
|
||||
{ name: 'vimKeymapEnabled', value: 'false', isSynced: false },
|
||||
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml"]', isSynced: true },
|
||||
{ name: 'leftPaneWidth', value: '25', isSynced: false },
|
||||
{ name: 'leftPaneVisible', value: 'true', isSynced: false },
|
||||
|
@ -371,7 +371,10 @@ function getLastSyncedPull() {
|
||||
|
||||
function setLastSyncedPull(entityChangeId) {
|
||||
const lastSyncedPullOption = becca.getOption('lastSyncedPull');
|
||||
lastSyncedPullOption.value = entityChangeId + '';
|
||||
|
||||
if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded
|
||||
lastSyncedPullOption.value = entityChangeId + '';
|
||||
}
|
||||
|
||||
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
|
||||
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPull']);
|
||||
@ -389,7 +392,10 @@ function setLastSyncedPush(entityChangeId) {
|
||||
ws.setLastSyncedPush(entityChangeId);
|
||||
|
||||
const lastSyncedPushOption = becca.getOption('lastSyncedPush');
|
||||
lastSyncedPushOption.value = entityChangeId + '';
|
||||
|
||||
if (lastSyncedPushOption) { // might be null in initial sync when becca is not loaded
|
||||
lastSyncedPushOption.value = entityChangeId + '';
|
||||
}
|
||||
|
||||
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
|
||||
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPush']);
|
||||
|
@ -16,7 +16,10 @@ let mainWindow;
|
||||
let setupWindow;
|
||||
|
||||
async function createExtraWindow(notePath, hoistedNoteId = 'root') {
|
||||
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
|
||||
|
||||
const {BrowserWindow} = require('electron');
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 800,
|
||||
@ -25,7 +28,7 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
|
||||
enableRemoteModule: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: optionService.getOptionBool('spellCheckEnabled')
|
||||
spellcheck: spellcheckEnabled
|
||||
},
|
||||
frame: optionService.getOptionBool('nativeTitleBarVisible'),
|
||||
icon: getIcon()
|
||||
@ -33,6 +36,8 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
win.loadURL('http://127.0.0.1:' + await port + '/?extra=1&extraHoistedNoteId=' + hoistedNoteId + '#' + notePath);
|
||||
|
||||
configureWebContents(win.webContents, spellcheckEnabled);
|
||||
}
|
||||
|
||||
ipcMain.on('create-extra-window', (event, arg) => {
|
||||
@ -74,8 +79,10 @@ async function createMainWindow() {
|
||||
mainWindow.loadURL('http://127.0.0.1:' + await port);
|
||||
mainWindow.on('closed', () => mainWindow = null);
|
||||
|
||||
const {webContents} = mainWindow;
|
||||
configureWebContents(mainWindow.webContents, spellcheckEnabled);
|
||||
}
|
||||
|
||||
function configureWebContents(webContents, spellcheckEnabled) {
|
||||
require("@electron/remote/main").enable(webContents);
|
||||
|
||||
webContents.on('new-window', (e, url) => {
|
||||
|
@ -46,19 +46,15 @@ function register(router) {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
|
||||
const image = shaca.getNote(req.params.noteId);
|
||||
router.get('/share/api/notes/:noteId', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
if (!image) {
|
||||
return res.status(404).send("Not found");
|
||||
}
|
||||
else if (image.type !== 'image') {
|
||||
return res.status(400).send("Requested note is not an image");
|
||||
if (!note) {
|
||||
return res.status(404).send(`Note ${noteId} not found`);
|
||||
}
|
||||
|
||||
res.set('Content-Type', image.mime);
|
||||
|
||||
res.send(image.getContent());
|
||||
res.json(note.getPojoWithAttributes());
|
||||
});
|
||||
|
||||
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
|
||||
@ -66,7 +62,7 @@ function register(router) {
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).send(`Not found`);
|
||||
return res.status(404).send(`Note ${noteId} not found`);
|
||||
}
|
||||
|
||||
const utils = require("../services/utils");
|
||||
@ -81,20 +77,30 @@ function register(router) {
|
||||
res.send(note.getContent());
|
||||
});
|
||||
|
||||
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
|
||||
const image = shaca.getNote(req.params.noteId);
|
||||
|
||||
if (!image) {
|
||||
return res.status(404).send(`Note ${noteId} not found`);
|
||||
}
|
||||
else if (image.type !== 'image') {
|
||||
return res.status(400).send("Requested note is not an image");
|
||||
}
|
||||
|
||||
res.set('Content-Type', image.mime);
|
||||
|
||||
res.send(image.getContent());
|
||||
});
|
||||
|
||||
// used for PDF viewing
|
||||
router.get('/share/api/notes/:noteId/view', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).send(`Not found`);
|
||||
return res.status(404).send(`Note ${noteId} not found`);
|
||||
}
|
||||
|
||||
const utils = require("../services/utils");
|
||||
|
||||
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
||||
|
||||
// res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
|
@ -89,6 +89,18 @@ class Attribute extends AbstractEntity {
|
||||
|
||||
return this.shaca.getNote(this.value);
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
attributeId: this.attributeId,
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
name: this.name,
|
||||
position: this.position,
|
||||
value: this.value,
|
||||
isInheritable: this.isInheritable
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Attribute;
|
||||
|
@ -410,6 +410,19 @@ class Note extends AbstractEntity {
|
||||
|
||||
return sharedAlias || this.noteId;
|
||||
}
|
||||
|
||||
getPojoWithAttributes() {
|
||||
return {
|
||||
noteId: this.noteId,
|
||||
title: this.title,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
utcDateModified: this.utcDateModified,
|
||||
attributes: this.getAttributes().map(attr => attr.getPojo()),
|
||||
parentNoteIds: this.parents.map(parentNote => parentNote.noteId),
|
||||
childNoteIds: this.children.map(child => child.noteId)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Note;
|
||||
|
@ -59,11 +59,7 @@ function load() {
|
||||
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND noteId IN (${noteIdStr})
|
||||
AND (
|
||||
(type = 'label' AND name IN ('archived', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss'))
|
||||
OR (type = 'relation' AND name IN ('imageLink', 'template', 'shareCss'))
|
||||
)`, []);
|
||||
AND noteId IN (${noteIdStr})`);
|
||||
|
||||
for (const row of rawAttributeRows) {
|
||||
new Attribute(row);
|
||||
|
@ -2,7 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<% if (note.hasRelation("shareFavicon")) { %>
|
||||
<link rel="shortcut icon" href="api/notes/<%= note.getRelation("shareFavicon").value %>/download">
|
||||
<% } else { %>
|
||||
<link rel="shortcut icon" href="../favicon.ico">
|
||||
<% } %>
|
||||
<script src="../app/share.js"></script>
|
||||
<% if (!note.hasLabel("shareOmitDefaultCss")) { %>
|
||||
<link href="../libraries/normalize.min.css" rel="stylesheet">
|
||||
<link href="../stylesheets/share.css" rel="stylesheet">
|
||||
@ -13,20 +18,28 @@
|
||||
<% for (const cssRelation of note.getRelations("shareCss")) { %>
|
||||
<link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
|
||||
<% } %>
|
||||
<% for (const jsRelation of note.getRelations("shareJs")) { %>
|
||||
<script type="module" src="api/notes/<%= jsRelation.value %>/download"></script>
|
||||
<% } %>
|
||||
<%- header %>
|
||||
<title><%= note.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<body data-note-id="<%= note.noteId %>">
|
||||
<div id="layout">
|
||||
<div id="main">
|
||||
<% if (note.parents[0].noteId !== 'share' && note.parents.length !== 0) { %>
|
||||
<nav id="parentLink">
|
||||
parent: <a href="<%= note.parents[0].noteId %>" class="type-<%= note.parents[0].type %>"><%= note.parents[0].title %></a>
|
||||
parent: <a href="<%= note.parents[0].noteId %>"
|
||||
class="type-<%= note.parents[0].type %>"><%= note.parents[0].title %></a>
|
||||
</nav>
|
||||
<% } %>
|
||||
|
||||
<h1 id="title"><%= note.title %></h1>
|
||||
|
||||
<% if (note.hasLabel("pageUrl")) { %>
|
||||
<div id="noteClippedFrom">This note was originally clipped from <a href="<%= note.getLabelValue("pageUrl") %>"><%= note.getLabelValue("pageUrl") %></a></div>
|
||||
<% } %>
|
||||
|
||||
<% if (note.type === 'book') { %>
|
||||
<% } else if (isEmpty) { %>
|
||||
<p>This note has no content.</p>
|
||||
@ -39,18 +52,19 @@
|
||||
<% if (note.hasChildren()) { %>
|
||||
<nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>">
|
||||
<% if (!isEmpty) { %>
|
||||
<hr>
|
||||
<div id="noteClippedFrom">
|
||||
<span>Child notes: </span>
|
||||
<ul>
|
||||
<% for (const childNote of note.getChildNotes()) { %>
|
||||
<li>
|
||||
<a href="<%= childNote.shareId %>"
|
||||
class="type-<%= childNote.type %>"><%= childNote.title %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<ul>
|
||||
<% for (const childNote of note.getChildNotes()) { %>
|
||||
<li>
|
||||
<a href="<%= childNote.shareId %>"
|
||||
class="type-<%= childNote.type %>"><%= childNote.title %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
@ -63,14 +77,5 @@
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const toggleMenuButton = document.getElementById('toggleMenuButton');
|
||||
const layout = document.getElementById('layout');
|
||||
|
||||
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user