mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	attributes dialog converted to algolia autocomplete, closes #203
This commit is contained in:
		| @@ -61,7 +61,8 @@ function AttributesModel() { | |||||||
|  |  | ||||||
|         for (const attr of ownedAttributes) { |         for (const attr of ownedAttributes) { | ||||||
|             attr.labelValue = attr.type === 'label' ? attr.value : ''; |             attr.labelValue = attr.type === 'label' ? attr.value : ''; | ||||||
|             attr.relationValue = attr.type === 'relation' ? (await treeUtils.getNoteTitle(attr.value) + " (" + attr.value + ")") : ''; |             attr.relationValue = attr.type === 'relation' ? (await treeUtils.getNoteTitle(attr.value)) : ''; | ||||||
|  |             attr.selectedPath = attr.type === 'relation' ? attr.value : ''; | ||||||
|             attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : { |             attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : { | ||||||
|                 labelType: "text", |                 labelType: "text", | ||||||
|                 multiplicityType: "singlevalue", |                 multiplicityType: "singlevalue", | ||||||
| @@ -94,12 +95,6 @@ function AttributesModel() { | |||||||
|  |  | ||||||
|         // attribute might not be rendered immediatelly so could not focus |         // attribute might not be rendered immediatelly so could not focus | ||||||
|         setTimeout(() => $(".attribute-type-select:last").focus(), 100); |         setTimeout(() => $(".attribute-type-select:last").focus(), 100); | ||||||
|  |  | ||||||
|         $ownedAttributesBody.sortable({ |  | ||||||
|             handle: '.handle', |  | ||||||
|             containment: $ownedAttributesBody, |  | ||||||
|             update: this.updateAttributePositions |  | ||||||
|         }); |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.deleteAttribute = function(data, event) { |     this.deleteAttribute = function(data, event) { | ||||||
| @@ -149,7 +144,7 @@ function AttributesModel() { | |||||||
|                 attr.value = attr.labelValue; |                 attr.value = attr.labelValue; | ||||||
|             } |             } | ||||||
|             else if (attr.type === 'relation') { |             else if (attr.type === 'relation') { | ||||||
|                 attr.value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(attr.relationValue)) || ""; |                 attr.value = treeUtils.getNoteIdFromNotePath(attr.selectedPath); | ||||||
|             } |             } | ||||||
|             else if (attr.type === 'label-definition') { |             else if (attr.type === 'label-definition') { | ||||||
|                 attr.value = attr.labelDefinition; |                 attr.value = attr.labelDefinition; | ||||||
| @@ -236,64 +231,69 @@ async function showDialog() { | |||||||
| } | } | ||||||
|  |  | ||||||
| $dialog.on('focus', '.attribute-name', function (e) { | $dialog.on('focus', '.attribute-name', function (e) { | ||||||
|     if (!$(this).hasClass("ui-autocomplete-input")) { |     if (!$(this).hasClass("aa-input")) { | ||||||
|         $(this).autocomplete({ |         $(this).autocomplete({ | ||||||
|             source: async (request, response) => { |             appendTo: document.querySelector('body'), | ||||||
|  |             hint: false, | ||||||
|  |             autoselect: true, | ||||||
|  |             openOnFocus: true, | ||||||
|  |             minLength: 0 | ||||||
|  |         }, [{ | ||||||
|  |             displayKey: 'name', | ||||||
|  |             source: async (term, cb) => { | ||||||
|                 const attribute = attributesModel.getTargetAttribute(this); |                 const attribute = attributesModel.getTargetAttribute(this); | ||||||
|                 const type = (attribute().type === 'relation' || attribute().type === 'relation-definition') ? 'relation' : 'label'; |                 const type = (attribute().type === 'relation' || attribute().type === 'relation-definition') ? 'relation' : 'label'; | ||||||
|                 const names = await server.get('attributes/names/?type=' + type + '&query=' + encodeURIComponent(request.term)); |                 const names = await server.get('attributes/names/?type=' + type + '&query=' + encodeURIComponent(term)); | ||||||
|                 const result = names.map(name => { |                 const result = names.map(name => { | ||||||
|                     return { |                     return {name}; | ||||||
|                         label: name, |  | ||||||
|                         value: name |  | ||||||
|                     } |  | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|                 if (result.length > 0) { |                 if (result.length === 0) { | ||||||
|                     response(result); |                     result.push({name: "No results"}) | ||||||
|                 } |                 } | ||||||
|                 else { |  | ||||||
|                     response([{ |                 cb(result); | ||||||
|                         label: "No results", |             } | ||||||
|                         value: "No results" |             }]); | ||||||
|                     }]); |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             minLength: 0 |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $(this).autocomplete("search", $(this).val()); |     $(this).autocomplete("open"); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| $dialog.on('focus', '.label-value', async function (e) { | $dialog.on('focus', '.label-value', async function (e) { | ||||||
|     if (!$(this).hasClass("ui-autocomplete-input")) { |     if (!$(this).hasClass("aa-input")) { | ||||||
|         const attributeName = $(this).parent().parent().find('.attribute-name').val(); |         const attributeName = $(this).parent().parent().find('.attribute-name').val(); | ||||||
|  |  | ||||||
|         if (attributeName.trim() === "") { |         if (attributeName.trim() === "") { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName)); |         const attributeValues = (await server.get('attributes/values/' + encodeURIComponent(attributeName))) | ||||||
|  |             .map(attribute => { return { value: attribute }; }); | ||||||
|  |  | ||||||
|         if (attributeValues.length === 0) { |         if (attributeValues.length === 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         $(this).autocomplete({ |         $(this).autocomplete({ | ||||||
|             // shouldn't be required and autocomplete should just accept array of strings, but that fails |             appendTo: document.querySelector('body'), | ||||||
|             // because we have overriden filter() function in autocomplete.js |             hint: false, | ||||||
|             source: attributeValues.map(attribute => { |             autoselect: true, | ||||||
|                 return { |             openOnFocus: true, | ||||||
|                     attribute: attribute, |  | ||||||
|                     value: attribute |  | ||||||
|                 } |  | ||||||
|             }), |  | ||||||
|             minLength: 0 |             minLength: 0 | ||||||
|         }); |         }, [{ | ||||||
|  |             displayKey: 'value', | ||||||
|  |             source: function (term, cb) { | ||||||
|  |                 term = term.toLowerCase(); | ||||||
|  |  | ||||||
|  |                 const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term)); | ||||||
|  |  | ||||||
|  |                 cb(filtered); | ||||||
|  |             } | ||||||
|  |         }]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $(this).autocomplete("search", $(this).val()); |     $(this).autocomplete("open"); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   | |||||||
| @@ -63,6 +63,10 @@ function initNoteAutocomplete($el) { | |||||||
| ko.bindingHandlers.noteAutocomplete = { | ko.bindingHandlers.noteAutocomplete = { | ||||||
|     init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { |     init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { | ||||||
|         initNoteAutocomplete($(element)); |         initNoteAutocomplete($(element)); | ||||||
|  |  | ||||||
|  |         $(element).on('autocomplete:selected', function(event, suggestion, dataset) { | ||||||
|  |             bindingContext.$data.selectedPath = suggestion.path; | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -326,19 +326,24 @@ async function showAttributes() { | |||||||
|                         return; |                         return; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     $input.autocomplete({ |                     attributeValues = attributeValues.map(attribute => { return { value: attribute }; }); | ||||||
|                         // 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", "")); |                     $input.autocomplete({ | ||||||
|  |                         appendTo: document.querySelector('body'), | ||||||
|  |                         hint: false, | ||||||
|  |                         autoselect: true, | ||||||
|  |                         openOnFocus: true, | ||||||
|  |                         minLength: 0 | ||||||
|  |                     }, [{ | ||||||
|  |                         displayKey: 'value', | ||||||
|  |                         source: function (term, cb) { | ||||||
|  |                             term = term.toLowerCase(); | ||||||
|  |  | ||||||
|  |                             const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term)); | ||||||
|  |  | ||||||
|  |                             cb(filtered); | ||||||
|  |                         } | ||||||
|  |                     }]); | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|             else if (definition.labelType === 'number') { |             else if (definition.labelType === 'number') { | ||||||
| @@ -376,16 +381,22 @@ async function showAttributes() { | |||||||
|         } |         } | ||||||
|         else if (valueAttr.type === 'relation') { |         else if (valueAttr.type === 'relation') { | ||||||
|             if (valueAttr.value) { |             if (valueAttr.value) { | ||||||
|                 $input.val((await treeUtils.getNoteTitle(valueAttr.value) + " (" + valueAttr.value + ")")); |                 $input.val(await treeUtils.getNoteTitle(valueAttr.value)); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // no need to wait for this |             // no need to wait for this | ||||||
|             noteAutocompleteService.initNoteAutocomplete($input); |             noteAutocompleteService.initNoteAutocomplete($input); | ||||||
|  |  | ||||||
|  |             $input.on('autocomplete:selected', function(event, suggestion, dataset) { | ||||||
|  |                 promotedAttributeChanged(event); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             $input.prop("data-selected-path", valueAttr.value); | ||||||
|  |  | ||||||
|             // ideally we'd use link instead of button which would allow tooltip preview, but |             // ideally we'd use link instead of button which would allow tooltip preview, but | ||||||
|             // we can't guarantee updating the link in the a element |             // we can't guarantee updating the link in the a element | ||||||
|             const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => { |             const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => { | ||||||
|                 const notePath = linkService.getNotePathFromLabel($input.val()); |                 const notePath = $input.prop("data-selected-path"); | ||||||
|  |  | ||||||
|                 treeService.activateNote(notePath); |                 treeService.activateNote(notePath); | ||||||
|             }); |             }); | ||||||
| @@ -473,9 +484,14 @@ async function showAttributes() { | |||||||
|                     $attributeListInner.append(utils.formatLabel(attribute) + " "); |                     $attributeListInner.append(utils.formatLabel(attribute) + " "); | ||||||
|                 } |                 } | ||||||
|                 else if (attribute.type === 'relation') { |                 else if (attribute.type === 'relation') { | ||||||
|                     $attributeListInner.append('@' + attribute.name + "="); |                     if (attribute.value) { | ||||||
|                     $attributeListInner.append(await linkService.createNoteLink(attribute.value)); |                         $attributeListInner.append('@' + attribute.name + "="); | ||||||
|                     $attributeListInner.append(" "); |                         $attributeListInner.append(await linkService.createNoteLink(attribute.value)); | ||||||
|  |                         $attributeListInner.append(" "); | ||||||
|  |                     } | ||||||
|  |                     else { | ||||||
|  |                         messagingService.logError(`Relation ${attribute.attributeId} has empty target`); | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|                 else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') { |                 else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') { | ||||||
|                     $attributeListInner.append(attribute.name + " definition "); |                     $attributeListInner.append(attribute.name + " definition "); | ||||||
| @@ -501,8 +517,10 @@ async function promotedAttributeChanged(event) { | |||||||
|         value = $attr.is(':checked') ? "true" : "false"; |         value = $attr.is(':checked') ? "true" : "false"; | ||||||
|     } |     } | ||||||
|     else if ($attr.prop("attribute-type") === "relation") { |     else if ($attr.prop("attribute-type") === "relation") { | ||||||
|         if ($attr.val()) { |         const selectedPath = $attr.prop("data-selected-path"); | ||||||
|             value = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel($attr.val())); |  | ||||||
|  |         if (selectedPath) { | ||||||
|  |             value = treeUtils.getNoteIdFromNotePath(selectedPath); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|   | |||||||
| @@ -523,7 +523,7 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | |||||||
|  |  | ||||||
| /* if modal height overflows, then only modal body scrolls */ | /* if modal height overflows, then only modal body scrolls */ | ||||||
| .modal-body { | .modal-body { | ||||||
|     max-height: calc(100vh - 120px); |     max-height: calc(100vh - 200px); | ||||||
|     overflow-y: auto; |     overflow-y: auto; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,13 +7,12 @@ | |||||||
|           <span aria-hidden="true">×</span> |           <span aria-hidden="true">×</span> | ||||||
|         </button> |         </button> | ||||||
|       </div> |       </div> | ||||||
|       <div class="modal-body"> |       <form data-bind="submit: save"> | ||||||
|         <form data-bind="submit: save"> |         <div class="modal-body"> | ||||||
|           <div style="height: 97%; overflow: auto"> |           <div style="height: 97%; overflow: auto"> | ||||||
|             <table id="owned-attributes-table" class="table"> |             <table id="owned-attributes-table" class="table"> | ||||||
|               <thead> |               <thead> | ||||||
|               <tr> |               <tr> | ||||||
|                 <th></th> |  | ||||||
|                 <th>Type</th> |                 <th>Type</th> | ||||||
|                 <th>Name</th> |                 <th>Name</th> | ||||||
|                 <th>Value</th> |                 <th>Value</th> | ||||||
| @@ -22,10 +21,6 @@ | |||||||
|               </thead> |               </thead> | ||||||
|               <tbody data-bind="foreach: ownedAttributes"> |               <tbody data-bind="foreach: ownedAttributes"> | ||||||
|               <tr data-bind="if: !isDeleted"> |               <tr data-bind="if: !isDeleted"> | ||||||
|                 <td class="handle"> |  | ||||||
|                   <span class="glyphicon glyphicon-resize-vertical"></span> |  | ||||||
|                   <input type="hidden" name="position" data-bind="value: position"/> |  | ||||||
|                 </td> |  | ||||||
|                 <td> |                 <td> | ||||||
|                   <select class="form-control attribute-type-select" style="width: auto;" |                   <select class="form-control attribute-type-select" style="width: auto;" | ||||||
|                       data-bind="options: $parent.availableTypes, optionsText: 'text', optionsValue: 'value', value: type, event: { change: $parent.typeChanged }"></select> |                       data-bind="options: $parent.availableTypes, optionsText: 'text', optionsValue: 'value', value: type, event: { change: $parent.typeChanged }"></select> | ||||||
| @@ -122,16 +117,16 @@ | |||||||
|               </table> |               </table> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </form> |         </div> | ||||||
|       </div> |         <div class="modal-footer" style="display: flex; justify-content: space-between;"> | ||||||
|       <div class="modal-footer" style="display: flex; justify-content: space-between;"> |           <button class="btn btn-primary btn-large" style="width: 200px;" id="save-attributes-button" type="submit"> | ||||||
|         <button class="btn btn-primary btn-large" style="width: 200px;" id="save-attributes-button" type="submit"> |             Save changes <kbd>enter</kbd></button> | ||||||
|           Save changes <kbd>enter</kbd></button> |  | ||||||
|  |  | ||||||
|         <button class="btn btn-sm" type="button" data-help-page="Attributes"> |           <button class="btn btn-sm" type="button" data-help-page="Attributes"> | ||||||
|           <i class="glyphicon glyphicon-info-sign"></i> Help |             <i class="glyphicon glyphicon-info-sign"></i> Help | ||||||
|         </button> |           </button> | ||||||
|       </div> |         </div> | ||||||
|  |       </form> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user