2018-11-09 03:01:25 +08:00
import server from "./server.js" ;
2019-08-27 02:21:43 +08:00
import ws from "./ws.js" ;
2018-11-09 03:01:25 +08:00
import treeUtils from "./tree_utils.js" ;
import noteAutocompleteService from "./note_autocomplete.js" ;
2019-05-05 04:44:25 +08:00
class Attributes {
/ * *
2019-05-09 01:55:24 +08:00
* @ param { TabContext } ctx
2019-05-05 04:44:25 +08:00
* /
constructor ( ctx ) {
this . ctx = ctx ;
2019-05-09 01:55:24 +08:00
this . $promotedAttributesContainer = ctx . $tabContent . find ( ".note-detail-promoted-attributes" ) ;
this . $savedIndicator = ctx . $tabContent . find ( ".saved-indicator" ) ;
2019-05-05 04:44:25 +08:00
this . attributePromise = null ;
}
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
invalidateAttributes ( ) {
this . attributePromise = null ;
}
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
reloadAttributes ( ) {
this . attributePromise = server . get ( ` notes/ ${ this . ctx . note . noteId } /attributes ` ) ;
}
2018-12-25 06:08:43 +08:00
2019-05-05 04:44:25 +08:00
async refreshAttributes ( ) {
this . reloadAttributes ( ) ;
2018-12-25 06:08:43 +08:00
2019-05-05 04:44:25 +08:00
await this . showAttributes ( ) ;
}
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
async getAttributes ( ) {
if ( ! this . attributePromise ) {
this . reloadAttributes ( ) ;
}
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
return await this . attributePromise ;
2018-12-25 06:08:43 +08:00
}
2019-05-05 04:44:25 +08:00
async showAttributes ( ) {
this . $promotedAttributesContainer . empty ( ) ;
2019-05-03 04:24:43 +08:00
2019-05-05 04:44:25 +08:00
const attributes = await this . getAttributes ( ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
const promoted = attributes . filter ( attr =>
( attr . type === 'label-definition' || attr . type === 'relation-definition' )
&& ! attr . name . startsWith ( "child:" )
&& attr . value . isPromoted ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
const hidePromotedAttributes = attributes . some ( attr => attr . type === 'label' && attr . name === 'hidePromotedAttributes' ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
if ( promoted . length > 0 && ! hidePromotedAttributes ) {
const $tbody = $ ( "<tbody>" ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
for ( const definitionAttr of promoted ) {
const definitionType = definitionAttr . type ;
const valueType = definitionType . substr ( 0 , definitionType . length - 11 ) ;
2018-11-14 05:14:41 +08:00
2019-05-05 04:44:25 +08:00
let valueAttrs = attributes . filter ( el => el . name === definitionAttr . name && el . type === valueType ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
if ( valueAttrs . length === 0 ) {
valueAttrs . push ( {
attributeId : "" ,
type : valueType ,
name : definitionAttr . name ,
value : ""
} ) ;
}
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
if ( definitionAttr . value . multiplicityType === 'singlevalue' ) {
valueAttrs = valueAttrs . slice ( 0 , 1 ) ;
}
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
for ( const valueAttr of valueAttrs ) {
const $tr = await this . createPromotedAttributeRow ( definitionAttr , valueAttr ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
$tbody . append ( $tr ) ;
}
2018-11-09 03:01:25 +08:00
}
2019-05-05 04:44:25 +08:00
// 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 ) ;
2018-11-09 03:01:25 +08:00
}
2019-05-05 04:44:25 +08:00
return attributes ;
}
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
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" )
. 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 ;
}
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
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" ) ;
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
let step = 1 ;
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
for ( let i = 0 ; i < ( definition . numberPrecision || 0 ) && i < 10 ; i ++ ) {
step /= 10 ;
}
2018-12-01 00:36:41 +08:00
2019-05-05 04:44:25 +08:00
$input . prop ( "step" , step ) ;
}
else if ( definition . labelType === 'boolean' ) {
$input . prop ( "type" , "checkbox" ) ;
2018-12-01 00:36:41 +08:00
2019-05-05 04:44:25 +08:00
if ( valueAttr . value === "true" ) {
$input . prop ( "checked" , "checked" ) ;
}
2018-12-01 00:36:41 +08:00
}
2019-05-05 04:44:25 +08:00
else if ( definition . labelType === 'date' ) {
$input . prop ( "type" , "date" ) ;
}
else if ( definition . labelType === 'url' ) {
$input . prop ( "placeholder" , "http://website..." ) ;
2018-12-01 00:36:41 +08:00
2019-05-05 04:44:25 +08:00
const $openButton = $ ( "<span>" )
. addClass ( "input-group-text open-external-link-button jam jam-arrow-up-right" )
. prop ( "title" , "Open external link" )
. click ( ( ) => window . open ( $input . val ( ) , '_blank' ) ) ;
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
$input . after ( $ ( "<div>" )
. addClass ( "input-group-append" )
. append ( $openButton ) ) ;
}
else {
2019-08-27 02:21:43 +08:00
ws . logError ( "Unknown labelType=" + definitionAttr . labelType ) ;
2018-11-14 02:25:59 +08:00
}
}
2019-05-05 04:44:25 +08:00
else if ( valueAttr . type === 'relation' ) {
if ( valueAttr . value ) {
$input . val ( await treeUtils . getNoteTitle ( valueAttr . value ) ) ;
}
// no need to wait for this
noteAutocompleteService . initNoteAutocomplete ( $input ) ;
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
$input . on ( 'autocomplete:selected' , ( event , suggestion , dataset ) => {
this . promotedAttributeChanged ( event ) ;
} ) ;
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
$input . setSelectedPath ( valueAttr . value ) ;
2018-11-14 02:25:59 +08:00
}
else {
2019-08-27 02:21:43 +08:00
ws . logError ( "Unknown attribute type=" + valueAttr . type ) ;
2019-05-05 04:44:25 +08:00
return ;
2018-11-14 02:25:59 +08:00
}
2019-05-05 04:44:25 +08:00
if ( definition . multiplicityType === "multivalue" ) {
const addButton = $ ( "<span>" )
. addClass ( "jam jam-plus pointer" )
. prop ( "title" , "Add new attribute" )
. click ( async ( ) => {
const $new = await this . createPromotedAttributeRow ( definitionAttr , {
attributeId : "" ,
type : valueAttr . type ,
name : definitionAttr . name ,
value : ""
} ) ;
$tr . after ( $new ) ;
$new . find ( 'input' ) . focus ( ) ;
2018-11-14 02:25:59 +08:00
} ) ;
2019-05-05 04:44:25 +08:00
const removeButton = $ ( "<span>" )
. addClass ( "jam jam-trash-alt pointer" )
. prop ( "title" , "Remove this attribute" )
. click ( async ( ) => {
if ( valueAttr . attributeId ) {
await server . remove ( "notes/" + noteId + "/attributes/" + valueAttr . attributeId ) ;
}
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
$tr . remove ( ) ;
} ) ;
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
$multiplicityCell . append ( addButton ) . append ( " " ) . append ( removeButton ) ;
}
2018-11-14 02:25:59 +08:00
2019-05-05 04:44:25 +08:00
return $tr ;
2018-11-14 02:25:59 +08:00
}
2019-05-05 04:44:25 +08:00
async promotedAttributeChanged ( event ) {
const $attr = $ ( event . target ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
let value ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
if ( $attr . prop ( "type" ) === "checkbox" ) {
value = $attr . is ( ':checked' ) ? "true" : "false" ;
}
else if ( $attr . prop ( "attribute-type" ) === "relation" ) {
const selectedPath = $attr . getSelectedPath ( ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
value = selectedPath ? treeUtils . getNoteIdFromNotePath ( selectedPath ) : "" ;
}
else {
value = $attr . val ( ) ;
}
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
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
} ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
$attr . prop ( "attribute-id" , result . attributeId ) ;
2018-11-09 03:01:25 +08:00
2019-05-05 04:44:25 +08:00
// 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 ( ) ;
}
2018-12-01 00:18:19 +08:00
}
2019-08-07 05:20:27 +08:00
syncDataReceived ( syncData ) {
if ( syncData . find ( sd => sd . entityName === 'attributes' && sd . noteId === this . ctx . note . noteId ) ) {
this . reloadAttributes ( ) ;
}
}
2018-11-09 03:01:25 +08:00
}
2019-05-05 04:44:25 +08:00
export default Attributes ;