import treeService from './tree.js'; import treeUtils from './tree_utils.js'; import noteTypeService from './note_type.js'; import protectedSessionService from './protected_session.js'; import protectedSessionHolder from './protected_session_holder.js'; import utils from './utils.js'; import server from './server.js'; import messagingService from "./messaging.js"; import infoService from "./info.js"; import linkService from "./link.js"; import treeCache from "./tree_cache.js"; import NoteFull from "../entities/note_full.js"; import noteDetailCode from './note_detail_code.js'; import noteDetailText from './note_detail_text.js'; import noteDetailFile from './note_detail_file.js'; import noteDetailSearch from './note_detail_search.js'; import noteDetailRender from './note_detail_render.js'; import noteDetailRelationMap from './note_detail_relation_map.js'; import bundleService from "./bundle.js"; import noteAutocompleteService from "./note_autocomplete.js"; const $noteTitle = $("#note-title"); const $noteDetailComponents = $(".note-detail-component"); const $protectButton = $("#protect-button"); const $unprotectButton = $("#unprotect-button"); const $noteDetailWrapper = $("#note-detail-wrapper"); const $noteIdDisplay = $("#note-id-display"); const $attributeList = $("#attribute-list"); const $attributeListInner = $("#attribute-list-inner"); const $childrenOverview = $("#children-overview"); const $scriptArea = $("#note-detail-script-area"); const $promotedAttributesContainer = $("#note-detail-promoted-attributes"); let currentNote = null; let noteChangeDisabled = false; let isNoteChanged = false; let attributePromise; const components = { 'code': noteDetailCode, 'text': noteDetailText, 'file': noteDetailFile, 'search': noteDetailSearch, 'render': noteDetailRender, 'relation-map': noteDetailRelationMap }; function getComponent(type) { if (!type) { type = getCurrentNote().type; } if (components[type]) { return components[type]; } else { infoService.throwError("Unrecognized type: " + type); } } function getCurrentNote() { return currentNote; } function getCurrentNoteId() { return currentNote ? currentNote.noteId : null; } function getCurrentNoteType() { const currentNote = getCurrentNote(); return currentNote ? currentNote.type : null; } function noteChanged() { if (noteChangeDisabled) { return; } isNoteChanged = true; } async function reload() { // no saving here await loadNoteDetail(getCurrentNoteId()); } async function switchToNote(noteId) { if (getCurrentNoteId() !== noteId) { await saveNoteIfChanged(); await loadNoteDetail(noteId); } } function getCurrentNoteContent() { return getComponent().getContent(); } function onNoteChange(func) { return getComponent().onNoteChange(func); } async function saveNote() { const note = getCurrentNote(); note.title = $noteTitle.val(); note.content = getCurrentNoteContent(note); // it's important to set the flag back to false immediatelly after retrieving title and content // otherwise we might overwrite another change (especially async code) isNoteChanged = false; treeService.setNoteTitle(note.noteId, note.title); await server.put('notes/' + note.noteId, note.dto); if (note.isProtected) { protectedSessionHolder.touchProtectedSession(); } infoService.showMessage("Saved!"); } async function saveNoteIfChanged() { if (!isNoteChanged) { return; } await saveNote(); } function setNoteBackgroundIfProtected(note) { $noteDetailWrapper.toggleClass("protected", note.isProtected); $protectButton.toggleClass("active", note.isProtected); $unprotectButton.toggleClass("active", !note.isProtected); $unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable()); } let isNewNoteCreated = false; function newNoteCreated() { isNewNoteCreated = true; } async function handleProtectedSession() { const newSessionCreated = await protectedSessionService.ensureProtectedSession(currentNote.isProtected, false); if (currentNote.isProtected) { protectedSessionHolder.touchProtectedSession(); } // this might be important if we focused on protected note when not in protected note and we got a dialog // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. protectedSessionService.ensureDialogIsClosed(); return newSessionCreated; } async function loadNoteDetail(noteId) { const loadedNote = await loadNote(noteId); // we will try to render the new note only if it's still the active one in the tree // this is useful when user quickly switches notes (by e.g. holding down arrow) so that we don't // try to render all those loaded notes one after each other. This only guarantees that correct note // will be displayed independent of timing const currentTreeNode = treeService.getCurrentNode(); if (currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) { return; } // only now that we're in sync with tree active node we will switch currentNote currentNote = loadedNote; // needs to happend after loading the note itself because it references current noteId refreshAttributes(); if (isNewNoteCreated) { isNewNoteCreated = false; $noteTitle.focus().select(); } $noteIdDisplay.html(noteId); setNoteBackgroundIfProtected(currentNote); $noteDetailWrapper.show(); noteChangeDisabled = true; try { $noteTitle.val(currentNote.title); noteTypeService.setNoteType(currentNote.type); noteTypeService.setNoteMime(currentNote.mime); for (const componentType in components) { if (componentType !== currentNote.type) { components[componentType].cleanup(); } } $noteDetailComponents.hide(); const newSessionCreated = await handleProtectedSession(); if (newSessionCreated) { // in such case we're reloading note anyway so no need to continue here. return; } await getComponent(currentNote.type).show(); } finally { noteChangeDisabled = false; } treeService.setBranchBackgroundBasedOnProtectedStatus(noteId); // after loading new note make sure editor is scrolled to the top $noteDetailWrapper.scrollTop(0); $scriptArea.empty(); await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView'); await showAttributes(); await showChildrenOverview(); } async function showChildrenOverview() { const note = getCurrentNote(); const attributes = await attributePromise; const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview') || note.type === 'relation-map'; if (hideChildrenOverview) { $childrenOverview.hide(); return; } $childrenOverview.empty(); const notePath = treeService.getCurrentNotePath(); for (const childBranch of await note.getChildBranches()) { const link = $('', { href: 'javascript:', text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId) }).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId); const childEl = $('
').html(link); $childrenOverview.append(childEl); } $childrenOverview.show(); } async function refreshAttributes() { attributePromise = server.get('notes/' + getCurrentNoteId() + '/attributes'); await showAttributes(); } async function getAttributes() { return await attributePromise; } async function showAttributes() { $promotedAttributesContainer.empty(); $attributeList.hide(); const noteId = getCurrentNoteId(); const attributes = await attributePromise; const promoted = attributes.filter(attr => (attr.type === 'label-definition' || attr.type === 'relation-definition') && !attr.name.startsWith("child:") && attr.value.isPromoted); let idx = 1; async function createRow(definitionAttr, valueAttr) { const definition = definitionAttr.value; const inputId = "promoted-input-" + idx; const $tr = $(""); const $labelCell = $("").append(valueAttr.name); const $input = $("") .prop("id", inputId) .prop("tabindex", definitionAttr.position) .prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one .prop("attribute-type", valueAttr.type) .prop("attribute-name", valueAttr.name) .prop("value", valueAttr.value) .addClass("form-control") .addClass("promoted-attribute-input") .change(promotedAttributeChanged); idx++; const $inputCell = $("").append($("
").addClass("input-group").append($input)); const $actionCell = $(""); const $multiplicityCell = $(""); $tr .append($labelCell) .append($inputCell) .append($actionCell) .append($multiplicityCell); if (valueAttr.type === 'label') { if (definition.labelType === 'text') { $input.prop("type", "text"); // no need to await for this, can be done asynchronously server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => { if (attributeValues.length === 0) { return; } $input.autocomplete({ // shouldn't be required and autocomplete should just accept array of strings, but that fails // because we have overriden filter() function in autocomplete.js source: attributeValues.map(attribute => { return { attribute: attribute, value: attribute } }), minLength: 0 }); $input.focus(() => $input.autocomplete("search", "")); }); } else if (definition.labelType === 'number') { $input.prop("type", "number"); } else if (definition.labelType === 'boolean') { $input.prop("type", "checkbox"); if (valueAttr.value === "true") { $input.prop("checked", "checked"); } } else if (definition.labelType === 'date') { $input.prop("type", "text"); $input.datepicker({ changeMonth: true, changeYear: true, yearRange: "c-200:c+10", dateFormat: "yy-mm-dd" }); const $todayButton = $("