From 8b9c235465d23691380c08b06a4f4c16f6a360d5 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 13 Jan 2020 20:25:56 +0100 Subject: [PATCH] widgetizing promoted attributes --- .../javascripts/services/app_context.js | 5 + src/public/javascripts/services/attributes.js | 235 --------------- .../widgets/promoted_attributes.js | 269 ++++++++++++++++++ src/public/stylesheets/style.css | 15 - src/views/center.ejs | 4 - 5 files changed, 274 insertions(+), 254 deletions(-) create mode 100644 src/public/javascripts/widgets/promoted_attributes.js diff --git a/src/public/javascripts/services/app_context.js b/src/public/javascripts/services/app_context.js index 6b7724df1..9e91ebe55 100644 --- a/src/public/javascripts/services/app_context.js +++ b/src/public/javascripts/services/app_context.js @@ -9,6 +9,7 @@ import server from "./server.js"; import keyboardActionService from "./keyboard_actions.js"; import TabRowWidget from "./tab_row.js"; import NoteTitleWidget from "../widgets/note_title.js"; +import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; class AppContext { constructor() { @@ -28,6 +29,9 @@ class AppContext { $("#global-menu-wrapper").after(contents); + this.promotedAttributes = new PromotedAttributesWidget(this); + $("#center-pane").prepend(this.promotedAttributes.render()); + this.noteTitleWidget = new NoteTitleWidget(this); $("#center-pane").prepend(this.noteTitleWidget.render()); @@ -47,6 +51,7 @@ class AppContext { } this.widgets.push(this.noteTitleWidget); + this.widgets.push(this.promotedAttributes); } trigger(name, data) { diff --git a/src/public/javascripts/services/attributes.js b/src/public/javascripts/services/attributes.js index 096a13ad4..805093ce7 100644 --- a/src/public/javascripts/services/attributes.js +++ b/src/public/javascripts/services/attributes.js @@ -9,8 +9,6 @@ class Attributes { */ constructor(ctx) { this.ctx = ctx; - this.$promotedAttributesContainer = ctx.$tabContent.find(".note-detail-promoted-attributes"); - this.$savedIndicator = ctx.$tabContent.find(".saved-indicator"); this.attributePromise = null; } @@ -24,8 +22,6 @@ class Attributes { async refreshAttributes() { this.reloadAttributes(); - - await this.showAttributes(); } async getAttributes() { @@ -36,237 +32,6 @@ class Attributes { return this.attributePromise; } - async showAttributes() { - this.$promotedAttributesContainer.empty(); - - const attributes = await this.getAttributes(); - - const promoted = attributes.filter(attr => - (attr.type === 'label-definition' || attr.type === 'relation-definition') - && !attr.name.startsWith("child:") - && attr.value.isPromoted); - - const hidePromotedAttributes = attributes.some(attr => attr.type === 'label' && attr.name === 'hidePromotedAttributes'); - - if (promoted.length > 0 && !hidePromotedAttributes) { - const $tbody = $(""); - - for (const definitionAttr of promoted) { - const definitionType = definitionAttr.type; - const valueType = definitionType.substr(0, definitionType.length - 11); - - let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType); - - if (valueAttrs.length === 0) { - valueAttrs.push({ - attributeId: "", - type: valueType, - name: definitionAttr.name, - value: "" - }); - } - - if (definitionAttr.value.multiplicityType === 'singlevalue') { - valueAttrs = valueAttrs.slice(0, 1); - } - - for (const valueAttr of valueAttrs) { - const $tr = await this.createPromotedAttributeRow(definitionAttr, valueAttr); - - $tbody.append($tr); - } - } - - // we replace the whole content in one step so there can't be any race conditions - // (previously we saw promoted attributes doubling) - this.$promotedAttributesContainer.empty().append($tbody); - } - - return attributes; - } - - async createPromotedAttributeRow(definitionAttr, valueAttr) { - const definition = definitionAttr.value; - const $tr = $(""); - const $labelCell = $("").append(valueAttr.name); - const $input = $("") - .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") - .on('change', event => this.promotedAttributeChanged(event)); - - const $inputCell = $("").append($("
").addClass("input-group").append($input)); - - const $actionCell = $(""); - const $multiplicityCell = $("") - .addClass("multiplicity") - .attr("nowrap", true); - - $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; - } - - attributeValues = attributeValues.map(attribute => { return { value: attribute }; }); - - $input.autocomplete({ - appendTo: document.querySelector('body'), - hint: false, - autoselect: false, - openOnFocus: true, - minLength: 0, - tabAutocomplete: false - }, [{ - 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') { - $input.prop("type", "number"); - - let step = 1; - - for (let i = 0; i < (definition.numberPrecision || 0) && i < 10; i++) { - step /= 10; - } - - $input.prop("step", step); - } - 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", "date"); - } - else if (definition.labelType === 'url') { - $input.prop("placeholder", "http://website..."); - - const $openButton = $("") - .addClass("input-group-text open-external-link-button bx bx-trending-up") - .prop("title", "Open external link") - .on('click', () => window.open($input.val(), '_blank')); - - $input.after($("
") - .addClass("input-group-append") - .append($openButton)); - } - else { - ws.logError("Unknown labelType=" + definitionAttr.labelType); - } - } - else if (valueAttr.type === 'relation') { - if (valueAttr.value) { - $input.val(await treeUtils.getNoteTitle(valueAttr.value)); - } - - // no need to wait for this - noteAutocompleteService.initNoteAutocomplete($input); - - $input.on('autocomplete:selected', (event, suggestion, dataset) => { - this.promotedAttributeChanged(event); - }); - - $input.setSelectedPath(valueAttr.value); - } - else { - ws.logError("Unknown attribute type=" + valueAttr.type); - return; - } - - if (definition.multiplicityType === "multivalue") { - const addButton = $("") - .addClass("bx bx-plus pointer") - .prop("title", "Add new attribute") - .on('click', async () => { - const $new = await this.createPromotedAttributeRow(definitionAttr, { - attributeId: "", - type: valueAttr.type, - name: definitionAttr.name, - value: "" - }); - - $tr.after($new); - - $new.find('input').trigger('focus'); - }); - - const removeButton = $("") - .addClass("bx bx-trash pointer") - .prop("title", "Remove this attribute") - .on('click', async () => { - if (valueAttr.attributeId) { - await server.remove("notes/" + this.ctx.note.noteId + "/attributes/" + valueAttr.attributeId); - } - - $tr.remove(); - }); - - $multiplicityCell.append(addButton).append("  ").append(removeButton); - } - - return $tr; - } - - async promotedAttributeChanged(event) { - const $attr = $(event.target); - - let value; - - if ($attr.prop("type") === "checkbox") { - value = $attr.is(':checked') ? "true" : "false"; - } - else if ($attr.prop("attribute-type") === "relation") { - const selectedPath = $attr.getSelectedPath(); - - value = selectedPath ? treeUtils.getNoteIdFromNotePath(selectedPath) : ""; - } - else { - value = $attr.val(); - } - - const result = await server.put(`notes/${this.ctx.note.noteId}/attribute`, { - attributeId: $attr.prop("attribute-id"), - type: $attr.prop("attribute-type"), - name: $attr.prop("attribute-name"), - value: value - }); - - $attr.prop("attribute-id", result.attributeId); - - // animate only if it's not being animated already, this is important especially for e.g. number inputs - // which can be changed many times in a second by clicking on higher/lower buttons. - if (this.$savedIndicator.queue().length === 0) { - this.$savedIndicator.fadeOut(); - this.$savedIndicator.fadeIn(); - } - } - eventReceived(name, data) { if (!this.ctx.note) { return; diff --git a/src/public/javascripts/widgets/promoted_attributes.js b/src/public/javascripts/widgets/promoted_attributes.js new file mode 100644 index 000000000..3b334e184 --- /dev/null +++ b/src/public/javascripts/widgets/promoted_attributes.js @@ -0,0 +1,269 @@ +import server from "../services/server.js"; +import ws from "../services/ws.js"; +import treeUtils from "../services/tree_utils.js"; +import noteAutocompleteService from "../services/note_autocomplete.js"; +import TabAwareWidget from "./tab_aware_widget.js"; + +const TPL = ` + +`; + +export default class PromotedAttributesWidget extends TabAwareWidget { + doRender() { + const $widget = $(TPL); + + this.$container = $widget.find(".promoted-attributes"); + + return $widget; + } + + async activeTabChanged() { + this.$container.empty(); + + const attributes = await this.tabContext.attributes.getAttributes(); + + const promoted = attributes.filter(attr => + (attr.type === 'label-definition' || attr.type === 'relation-definition') + && !attr.name.startsWith("child:") + && attr.value.isPromoted); + + const hidePromotedAttributes = attributes.some(attr => attr.type === 'label' && attr.name === 'hidePromotedAttributes'); + + if (promoted.length > 0 && !hidePromotedAttributes) { + const $tbody = $(""); + + for (const definitionAttr of promoted) { + const definitionType = definitionAttr.type; + const valueType = definitionType.substr(0, definitionType.length - 11); + + let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType); + + if (valueAttrs.length === 0) { + valueAttrs.push({ + attributeId: "", + type: valueType, + name: definitionAttr.name, + value: "" + }); + } + + if (definitionAttr.value.multiplicityType === 'singlevalue') { + valueAttrs = valueAttrs.slice(0, 1); + } + + for (const valueAttr of valueAttrs) { + const $tr = await this.createPromotedAttributeRow(definitionAttr, valueAttr); + + $tbody.append($tr); + } + } + + // we replace the whole content in one step so there can't be any race conditions + // (previously we saw promoted attributes doubling) + this.$container.empty().append($tbody); + } + + return attributes; + } + + async createPromotedAttributeRow(definitionAttr, valueAttr) { + const definition = definitionAttr.value; + const $tr = $(""); + const $labelCell = $("").append(valueAttr.name); + const $input = $("") + .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") + .on('change', event => this.promotedAttributeChanged(event)); + + const $inputCell = $("").append($("
").addClass("input-group").append($input)); + + const $actionCell = $(""); + const $multiplicityCell = $("") + .addClass("multiplicity") + .attr("nowrap", true); + + $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; + } + + attributeValues = attributeValues.map(attribute => { return { value: attribute }; }); + + $input.autocomplete({ + appendTo: document.querySelector('body'), + hint: false, + autoselect: false, + openOnFocus: true, + minLength: 0, + tabAutocomplete: false + }, [{ + 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') { + $input.prop("type", "number"); + + let step = 1; + + for (let i = 0; i < (definition.numberPrecision || 0) && i < 10; i++) { + step /= 10; + } + + $input.prop("step", step); + } + 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", "date"); + } + else if (definition.labelType === 'url') { + $input.prop("placeholder", "http://website..."); + + const $openButton = $("") + .addClass("input-group-text open-external-link-button bx bx-trending-up") + .prop("title", "Open external link") + .on('click', () => window.open($input.val(), '_blank')); + + $input.after($("
") + .addClass("input-group-append") + .append($openButton)); + } + else { + ws.logError("Unknown labelType=" + definitionAttr.labelType); + } + } + else if (valueAttr.type === 'relation') { + if (valueAttr.value) { + $input.val(await treeUtils.getNoteTitle(valueAttr.value)); + } + + // no need to wait for this + noteAutocompleteService.initNoteAutocomplete($input); + + $input.on('autocomplete:selected', (event, suggestion, dataset) => { + this.promotedAttributeChanged(event); + }); + + $input.setSelectedPath(valueAttr.value); + } + else { + ws.logError("Unknown attribute type=" + valueAttr.type); + return; + } + + if (definition.multiplicityType === "multivalue") { + const addButton = $("") + .addClass("bx bx-plus pointer") + .prop("title", "Add new attribute") + .on('click', async () => { + const $new = await this.createPromotedAttributeRow(definitionAttr, { + attributeId: "", + type: valueAttr.type, + name: definitionAttr.name, + value: "" + }); + + $tr.after($new); + + $new.find('input').trigger('focus'); + }); + + const removeButton = $("") + .addClass("bx bx-trash pointer") + .prop("title", "Remove this attribute") + .on('click', async () => { + if (valueAttr.attributeId) { + await server.remove("notes/" + this.tabContext.note.noteId + "/attributes/" + valueAttr.attributeId); + } + + $tr.remove(); + }); + + $multiplicityCell.append(addButton).append("  ").append(removeButton); + } + + return $tr; + } + + async promotedAttributeChanged(event) { + const $attr = $(event.target); + + let value; + + if ($attr.prop("type") === "checkbox") { + value = $attr.is(':checked') ? "true" : "false"; + } + else if ($attr.prop("attribute-type") === "relation") { + const selectedPath = $attr.getSelectedPath(); + + value = selectedPath ? treeUtils.getNoteIdFromNotePath(selectedPath) : ""; + } + else { + value = $attr.val(); + } + + const result = await server.put(`notes/${this.tabContext.note.noteId}/attribute`, { + attributeId: $attr.prop("attribute-id"), + type: $attr.prop("attribute-type"), + name: $attr.prop("attribute-name"), + value: value + }); + + $attr.prop("attribute-id", result.attributeId); + + // FIXME + // animate only if it's not being animated already, this is important especially for e.g. number inputs + // which can be changed many times in a second by clicking on higher/lower buttons. + // if (this.$savedIndicator.queue().length === 0) { + // this.$savedIndicator.fadeOut(); + // this.$savedIndicator.fadeIn(); + // } + } +} \ No newline at end of file diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index cfb68cd10..204dd3442 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -478,21 +478,6 @@ button.icon-button { padding: 0; } -.note-detail-promoted-attributes { - margin: auto; - /* setting the display to block since "table" doesn't support scrolling */ - display: block; - /** flex-basis: content; - use once "content" is implemented by chrome */ - flex-shrink: 0; - flex-grow: 0; - max-height: 30%; - overflow: auto; -} - -.note-detail-promoted-attributes td, .note-detail-promoted-attributes th { - padding: 5px; -} - .note-detail-image { text-align: center; } diff --git a/src/views/center.ejs b/src/views/center.ejs index 69cc9ed49..7317f344e 100644 --- a/src/views/center.ejs +++ b/src/views/center.ejs @@ -2,10 +2,6 @@
-
- -
-