2020-01-14 03:25:56 +08:00
import server from "../services/server.js" ;
import ws from "../services/ws.js" ;
2020-01-25 16:56:08 +08:00
import treeService from "../services/tree.js" ;
2020-01-14 03:25:56 +08:00
import noteAutocompleteService from "../services/note_autocomplete.js" ;
import TabAwareWidget from "./tab_aware_widget.js" ;
const TPL = `
< div class = "promoted-attributes-wrapper" >
< style >
. promoted - attributes - wrapper {
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 ;
overflow : auto ;
}
. promoted - attributes td , . promoted - attributes th {
padding : 5 px ;
}
< / s t y l e >
< table class = "promoted-attributes" > < / t a b l e >
< / d i v >
` ;
export default class PromotedAttributesWidget extends TabAwareWidget {
doRender ( ) {
2020-01-19 01:01:16 +08:00
this . $widget = $ ( TPL ) ;
2020-01-14 03:25:56 +08:00
2020-01-19 01:01:16 +08:00
this . $container = this . $widget . find ( ".promoted-attributes" ) ;
2020-01-14 03:25:56 +08:00
2020-01-19 01:01:16 +08:00
return this . $widget ;
2020-01-14 03:25:56 +08:00
}
2020-01-25 21:37:12 +08:00
async refreshWithNote ( note ) {
2020-01-14 03:25:56 +08:00
this . $container . empty ( ) ;
2020-01-25 21:37:12 +08:00
const attributes = await note . getAttributes ( ) ;
2020-01-14 03:25:56 +08:00
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 = $ ( "<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 ) ;
2020-01-19 18:20:02 +08:00
this . toggle ( true ) ;
2020-01-19 17:29:21 +08:00
}
else {
2020-01-19 18:20:02 +08:00
this . toggle ( false ) ;
2020-01-14 03:25:56 +08:00
}
return attributes ;
}
async createPromotedAttributeRow ( definitionAttr , valueAttr ) {
const definition = definitionAttr . value ;
const $tr = $ ( "<tr>" ) ;
const $labelCell = $ ( "<th>" ) . append ( valueAttr . name ) ;
const $input = $ ( "<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 = $ ( "<td>" ) . append ( $ ( "<div>" ) . addClass ( "input-group" ) . append ( $input ) ) ;
const $actionCell = $ ( "<td>" ) ;
const $multiplicityCell = $ ( "<td>" )
. 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 = $ ( "<span>" )
. 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 ( $ ( "<div>" )
. addClass ( "input-group-append" )
. append ( $openButton ) ) ;
}
else {
ws . logError ( "Unknown labelType=" + definitionAttr . labelType ) ;
}
}
else if ( valueAttr . type === 'relation' ) {
if ( valueAttr . value ) {
2020-01-25 16:56:08 +08:00
$input . val ( await treeService . getNoteTitle ( valueAttr . value ) ) ;
2020-01-14 03:25:56 +08:00
}
// 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 = $ ( "<span>" )
. 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 = $ ( "<span>" )
. 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 ( ) ;
2020-01-25 16:56:08 +08:00
value = selectedPath ? treeService . getNoteIdFromNotePath ( selectedPath ) : "" ;
2020-01-14 03:25:56 +08:00
}
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();
// }
}
}