").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 @@
| | |