mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-29 11:02:28 +08:00
222 lines
6.4 KiB
JavaScript
222 lines
6.4 KiB
JavaScript
![]() |
import noteDetailService from '../services/note_detail.js';
|
||
|
import server from '../services/server.js';
|
||
|
import infoService from "../services/info.js";
|
||
|
|
||
|
const $dialog = $("#relations-dialog");
|
||
|
const $saveRelationsButton = $("#save-relations-button");
|
||
|
const $relationsBody = $('#relations-table tbody');
|
||
|
|
||
|
const relationsModel = new RelationsModel();
|
||
|
let relationNames = [];
|
||
|
|
||
|
function RelationsModel() {
|
||
|
const self = this;
|
||
|
|
||
|
this.relations = ko.observableArray();
|
||
|
|
||
|
this.updateRelationPositions = function() {
|
||
|
let position = 0;
|
||
|
|
||
|
// we need to update positions by searching in the DOM, because order of the
|
||
|
// relations in the viewmodel (self.relations()) stays the same
|
||
|
$relationsBody.find('input[name="position"]').each(function() {
|
||
|
const relation = self.getTargetRelation(this);
|
||
|
|
||
|
relation().position = position++;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
this.loadRelations = async function() {
|
||
|
const noteId = noteDetailService.getCurrentNoteId();
|
||
|
|
||
|
const relations = await server.get('notes/' + noteId + '/relations');
|
||
|
|
||
|
self.relations(relations.map(ko.observable));
|
||
|
|
||
|
addLastEmptyRow();
|
||
|
|
||
|
relationNames = await server.get('relations/names');
|
||
|
|
||
|
// relation might not be rendered immediatelly so could not focus
|
||
|
setTimeout(() => $(".relation-name:last").focus(), 100);
|
||
|
|
||
|
$relationsBody.sortable({
|
||
|
handle: '.handle',
|
||
|
containment: $relationsBody,
|
||
|
update: this.updateRelationPositions
|
||
|
});
|
||
|
};
|
||
|
|
||
|
this.deleteRelation = function(data, event) {
|
||
|
const relation = self.getTargetRelation(event.target);
|
||
|
const relationData = relation();
|
||
|
|
||
|
if (relationData) {
|
||
|
relationData.isDeleted = 1;
|
||
|
|
||
|
relation(relationData);
|
||
|
|
||
|
addLastEmptyRow();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function isValid() {
|
||
|
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
|
||
|
if (self.isEmptyName(i)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
this.save = async function() {
|
||
|
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||
|
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||
|
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||
|
$saveRelationsButton.focus();
|
||
|
|
||
|
if (!isValid()) {
|
||
|
alert("Please fix all validation errors and try saving again.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
self.updateRelationPositions();
|
||
|
|
||
|
const noteId = noteDetailService.getCurrentNoteId();
|
||
|
|
||
|
const relationsToSave = self.relations()
|
||
|
.map(relation => relation())
|
||
|
.filter(relation => relation.relationId !== "" || relation.name !== "");
|
||
|
|
||
|
const relations = await server.put('notes/' + noteId + '/relations', relationsToSave);
|
||
|
|
||
|
self.relations(relations.map(ko.observable));
|
||
|
|
||
|
addLastEmptyRow();
|
||
|
|
||
|
infoService.showMessage("Relations have been saved.");
|
||
|
|
||
|
noteDetailService.loadRelationList();
|
||
|
};
|
||
|
|
||
|
function addLastEmptyRow() {
|
||
|
const relations = self.relations().filter(attr => attr().isDeleted === 0);
|
||
|
const last = relations.length === 0 ? null : relations[relations.length - 1]();
|
||
|
|
||
|
if (!last || last.name.trim() !== "" || last.value !== "") {
|
||
|
self.relations.push(ko.observable({
|
||
|
relationId: '',
|
||
|
name: '',
|
||
|
value: '',
|
||
|
isDeleted: 0,
|
||
|
position: 0
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.relationChanged = function (data, event) {
|
||
|
addLastEmptyRow();
|
||
|
|
||
|
const relation = self.getTargetRelation(event.target);
|
||
|
|
||
|
relation.valueHasMutated();
|
||
|
};
|
||
|
|
||
|
this.isNotUnique = function(index) {
|
||
|
const cur = self.relations()[index]();
|
||
|
|
||
|
if (cur.name.trim() === "") {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
|
||
|
const relation = relations[i]();
|
||
|
|
||
|
if (index !== i && cur.name === relation.name) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
this.isEmptyName = function(index) {
|
||
|
const cur = self.relations()[index]();
|
||
|
|
||
|
return cur.name.trim() === "" && (cur.relationId !== "" || cur.value !== "");
|
||
|
};
|
||
|
|
||
|
this.getTargetRelation = function(target) {
|
||
|
const context = ko.contextFor(target);
|
||
|
const index = context.$index();
|
||
|
|
||
|
return self.relations()[index];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function showDialog() {
|
||
|
glob.activeDialog = $dialog;
|
||
|
|
||
|
await relationsModel.loadRelations();
|
||
|
|
||
|
$dialog.dialog({
|
||
|
modal: true,
|
||
|
width: 800,
|
||
|
height: 500
|
||
|
});
|
||
|
}
|
||
|
|
||
|
ko.applyBindings(relationsModel, document.getElementById('relations-dialog'));
|
||
|
|
||
|
$(document).on('focus', '.relation-name', function (e) {
|
||
|
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||
|
$(this).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: relationNames.map(relation => {
|
||
|
return {
|
||
|
relation: relation,
|
||
|
value: relation
|
||
|
}
|
||
|
}),
|
||
|
minLength: 0
|
||
|
});
|
||
|
}
|
||
|
|
||
|
$(this).autocomplete("search", $(this).val());
|
||
|
});
|
||
|
|
||
|
$(document).on('focus', '.relation-value', async function (e) {
|
||
|
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||
|
const relationName = $(this).parent().parent().find('.relation-name').val();
|
||
|
|
||
|
if (relationName.trim() === "") {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const relationValues = await server.get('relations/values/' + encodeURIComponent(relationName));
|
||
|
|
||
|
if (relationValues.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$(this).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: relationValues.map(relation => {
|
||
|
return {
|
||
|
label: relation,
|
||
|
value: relation
|
||
|
}
|
||
|
}),
|
||
|
minLength: 0
|
||
|
});
|
||
|
}
|
||
|
|
||
|
$(this).autocomplete("search", $(this).val());
|
||
|
});
|
||
|
|
||
|
export default {
|
||
|
showDialog
|
||
|
};
|