labo-components
Version:
1,051 lines (934 loc) • 36.3 kB
JavaScript
import AnnotationAPI from "../../api/AnnotationAPI";
import SegmentLayers from "./SegmentLayers";
import Project from "../../model/Project";
import MediaObject from "../../model/MediaObject";
//import Events from '../../util/Events';
import {
ANNOTATION_TARGET,
CLASSIFICATION,
METADATA,
COMMENT,
LINK,
} from "../../util/AnnotationConstants";
export const AnnotationEvents = {
ON_SAVE: "on-save-annotation",
ON_DELETE: "on-delete-annotation",
ON_EDIT: "on-edit-annotation",
ON_PLAY: "on-play-annotation",
ON_SET_ANNOTATION: "on-set-annotation",
ON_SET_SELECTION: "on-set-selection",
ON_CHANGE_TARGET: "on-change-target",
//TODO somehow add the resource IDs that can be triggered as well
};
export class ObjectUtil {
static clone = (object) => {
return object == null ? null : JSON.parse(JSON.stringify(object));
};
static filterNullValues = (object) => {
return Object.keys(object)
.filter((key) => object[key] != null)
.reduce((obj, key) => {
obj[key] = object[key];
return obj;
}, {});
};
}
/*
The activeTarget is central for generating annotations: it contains:
- nestedPID
- nonW3Cparams that need to be saved into the annotation as well (e.g. project & user IDs)
- additional semantic types related to the leaf (thus current target) of the nestedPID
*/
/*
{
"templates" : {
"mediaResource" : {
"nestedPID" : [
{
"type": ["Collection"],
"property": "isPartOf"
},
{
"type": ["Resource"],
"property": "isPartOf"
},
{
"type": ["MediaObject", "Representation"],
"property": "isRepresentation",
"additionalType": ["Video", "Audio", "Image"]
}
],
"selectionType": "Segment",
"listTypes" : ["Resource"]
}
},
"editOptions" : {
"Resource" : {
"motivations" : ["classification", "comment", "link", "metadata"]
},
"MediaObject" : {
"motivations" : ["classification", "comment", "link", "metadata"]
},
"Segment" : {
"motivations" : ["classification", "comment", "link", "metadata"]
}
},
"motivationConfig" : {
"classification" : {
"vocabularies" : ["GTAA", "UNESCO"]
},
"link" : {
"apis" : [
{"name" : "wikidata"},
{"name" : "europeana"},
{"name" : "custom"}
]
},
"metadata" : {
"mediaObject" : {
"templates" : ["generic"],
"cardsPerUser" : 10
},
"mediaSegment" : {
"templates" : ["generic"],
"cardsPerUser" : 10
},
"templates" : {
"generic" : {
"id" : "generic",
"label" : "Generic",
"properties" : [
{"key" : "title", "type" : "string"},
{"key" : "description", "type" : "string"},
{"key" : "date", "type" : "date"}
]
}
}
},
"comment" : {},
"config" : {
"role": "tiers",
"data": { }
}
}
}
*/
export class AnnotationClient {
constructor(config, events) {
if (!this.validateConfig(config)) {
alert("error: Please provide a valid STAN-client config");
return;
}
this.config = config;
this.activeTemplate =
config.templates[Object.keys(config.templates)[0]]; //always just set the first template
this.events = events; //new Events(); TODO fallback to new events object/function
this.activeTarget = null; //set via setActiveTarget
this.activeAnnotation = null; //only one annotation is active
this.activeBody = null; //always null by default
this.activeSelection = null; //only one selection is active
this.segmentLayers = new SegmentLayers({ annotationClient: this }); // handles segment layers
this.annotations = []; //filled when setActiveTarget is called (or setActiveProject)
AnnotationClient.instance = this;
}
static singleton(config, events) {
if (AnnotationClient.instance) {
AnnotationClient.instance.config = config;
AnnotationClient.instance.events = events;
return AnnotationClient.instance;
}
return new AnnotationClient(config, events);
}
validateConfig = (config) => {
//TODO extend this much more, for now it roughly checks if there is a template defined
return (
config && config.templates && typeof config.templates === "object"
);
};
setActiveTemplate = (templateId) => {
this.activeTemplate = this.config.templates[templateId];
};
/* ------------------------- LOADING ANNOTATIONS OF ACTIVE TARGET (CHANGE TO STAN-SERVER) --------------------------------- */
async refreshTargetAnnotations() {
if (!this.activeTarget) return [];
let annotations = await this.__loadTargetAnnotations(
this.activeTarget.npid,
this.activeTarget.nonW3Cparams
);
if (this.activeTemplate.listTypes) {
const additionalAnnotations = await this.__loadAnnotationsOfTypes(
this.activeTemplate.listTypes
);
annotations = annotations
? annotations.concat(additionalAnnotations)
: additionalAnnotations;
}
return annotations;
}
async __loadAnnotationsOfTypes(types) {
let annotations = [];
for (let i = 0; i < types.length; i++) {
//NOTE: forEach is not Promise aware, therefore a simple for-loop
const temp = await this.__loadAnnotationsOfType(
this.activeTarget.npid,
types[i], //semanticType
this.activeTarget.nonW3Cparams
);
if (temp) {
annotations = annotations.concat(temp);
}
}
return new Promise((resolve) => {
resolve(annotations);
});
}
//loads the annotations directly related to the active target (leaf node)
async __loadTargetAnnotations(npid, nonW3Cparams = {}) {
let annotations = null;
if (npid) {
const leaf = npid[npid.length - 1];
annotations = await this.__loadFromAPI(
{
"target.source": leaf.id, //leave out the type, since it can be e.g. Segment / MediaObject
},
nonW3Cparams
);
}
return new Promise((resolve) => {
resolve(annotations);
});
}
//loads annotations directly related to a certain part (indicated by the semanticType) of the npid
async __loadAnnotationsOfType(npid, semanticType, nonW3Cparams = {}) {
let annotations = null;
if (npid) {
const pidOfType = npid.find(
(pid) => pid.type.indexOf(semanticType) != -1
);
annotations = pidOfType
? await this.__loadFromAPI(
{
"target.source": pidOfType.id,
"target.type": semanticType,
},
nonW3Cparams
)
: null;
}
return new Promise((resolve) => {
resolve(annotations);
});
}
__loadFromAPI = (filter, nonW3Cparams = {}) => {
return new Promise((resolve) => {
//add the non-null nonW3Cparams to the filter
Object.assign(filter, ObjectUtil.filterNullValues(nonW3Cparams));
//hack for now:
filter["user.keyword"] = filter["user"];
delete filter["user"];
const notFilter = {
motivation: "bookmarking",
};
AnnotationAPI.getFilteredAnnotations(
filter["user.keyword"], //user ID
filter,
notFilter,
resolve
);
});
};
/* ------------------------- CRUD (REWIRE TO STAN) --------------------------------- */
save = (setActive = true, notify = true) => {
if (!this.activeAnnotation) {
alert("no active annotation, cannot save");
return;
}
return new Promise((resolve) => {
AnnotationAPI.saveAnnotation(this.activeAnnotation, (data) => {
if (data == null) resolve(null);
//assign the annotation data returned from the server (which now has annotation IDs)
this.activeAnnotation = setActive ? data : null;
//add or update the annotation in the list
if (!this.annotations.find((a) => a.id === data.id)) {
this.annotations.push(ObjectUtil.clone(data));
} else {
this.annotations[
this.annotations.findIndex((a) => a.id === data.id)
] = ObjectUtil.clone(data);
}
if (notify) {
this.events.trigger(
AnnotationEvents.ON_SAVE,
ObjectUtil.clone(data)
); // also trigger all listeners
//this.events.trigger(annotation.target.source, annotation); // TODO also trigger everyone listening to the media object
}
resolve(this.activeAnnotation); // callback for the async function
});
});
};
delete = (annotation, notify = true) => {
return new Promise((resolve) => {
AnnotationAPI.deleteAnnotation(annotation, (data, annotation) => {
if (data == null) resolve(null, null);
resolve(data, annotation);
this.annotations = this.annotations.filter(
(a) => a.id !== annotation.id
);
this.activeAnnotation = null;
if (notify) {
this.events.trigger(AnnotationEvents.ON_DELETE, annotation);
//this.events.trigger(annotation.target.source, annotation);
}
});
});
};
/* ------------------------ IMPORT CONVENIENCE METHODS FOR CRUD ON BODY ELEMENTS --------------------- */
async saveBodyElement(
bodyElement,
setActive = true,
notify = true,
annotation = null
) {
if (annotation != null) this.activeAnnotation = annotation; //enables "stateless" calls
if (!this.activeAnnotation || bodyElement == null) return;
const body = this.activeAnnotation.body
? this.activeAnnotation.body
: [];
if (bodyElement.annotationId) {
// update
body[
body.findIndex(
(b) => b.annotationId === bodyElement.annotationId
)
] = ObjectUtil.clone(bodyElement);
} else {
// add
bodyElement.annotationId = IDUtil.guid(); // generate on the client, so we know how to track it
body.push(ObjectUtil.clone(bodyElement));
}
this.activeAnnotation.body = body;
this.activeAnnotation = await this.save(true, notify);
this.activeBody = setActive ? ObjectUtil.clone(bodyElement) : null; // the newly saved body element becomes the active one or null
//return the promise and trigger listeners if needed TODO merge with save as well
return new Promise((resolve) => {
resolve({
annotation: this.activeAnnotation,
bodyElement: this.activeBody,
});
if (notify) {
this.events.trigger(AnnotationEvents.ON_SET_ANNOTATION, {
annotation: this.activeAnnotation,
bodyElement: this.activeBody,
});
}
});
}
async removeBodyElement(
motivation,
index,
setActive = true,
notify = true,
annotation = null
) {
if (annotation != null) this.activeAnnotation = annotation; //enables "stateless" calls
if (!this.activeAnnotation) return;
const bodyElementsOfMotivation = this.activeAnnotation.body
? this.activeAnnotation.body.filter(
(b) => b.annotationType === motivation
)
: [];
bodyElementsOfMotivation.splice(index, 1);
this.activeAnnotation.body = this.activeAnnotation.body
.filter((b) => b.annotationType !== motivation)
.concat(bodyElementsOfMotivation);
this.activeAnnotation = await this.save(true, notify);
const savedBodyElementsOfMotivation = this.activeAnnotation.body.filter(
(b) => b.annotationType === motivation
);
if (setActive) {
this.activeBody =
savedBodyElementsOfMotivation.length > 0
? ObjectUtil.clone(savedBodyElementsOfMotivation[0])
: null;
} else {
this.activeBody = null;
}
//return the promise and trigger listeners if needed TODO merge with save as well
return new Promise((resolve) => {
resolve({
annotation: this.activeAnnotation,
bodyElement: this.activeBody,
});
if (notify) {
this.events.trigger(AnnotationEvents.ON_SET_ANNOTATION, {
annotation: this.activeAnnotation,
bodyElement: this.activeBody,
});
}
});
}
// Get (temporal) segment end, before given position
getSegmentEndBefore(position, layerId = null) {
// All user segments
const segments = this.annotations
? this.annotations.filter(
(annotation) =>
annotation.target.type === ANNOTATION_TARGET.SEGMENT &&
(layerId == null || annotation.target.layerId == layerId)
)
: [];
let closestEndValue = 0;
let closestDeltaPos = Infinity;
let deltaPos = 0;
// Find closest segment end value
segments.forEach((segment) => {
deltaPos = position - segment.target.selector.refinedBy.end;
if (deltaPos > 0 && deltaPos < closestDeltaPos) {
closestDeltaPos = deltaPos;
closestEndValue = segment.target.selector.refinedBy.end;
}
});
return closestEndValue;
}
/* ------------------------- SELECTING PARTS OF THE SCREEN TO UPDATE ANNOTATION TARGETS ---------- */
/*
temporal segment looks like:
{start: start, end: end}
spatial segment looks like:
{
rect : {
x : rect.x,
y : rect.y,
w : rect.width,
h : rect.height
},
rotation : rect.rotation
}
*/
//TODO rework so it works for text, spatial, temporal selections
//use it to create an active refinedBy/selection object, that can be visualised by whoever can (see saveSelection())
//TODO create a new event for notifying: ON_SET_SELECTION
setActiveSelection = (selection, notify = true) => {
//TODO merge with setActiveTarget, called by the TierView only
return new Promise((resolve) => {
this.activeSelection = selection
? ObjectUtil.clone(selection)
: null;
if (selection) {
this.activeSelection.semanticType = this.activeTemplate.selectionType; //always add the selectionType
}
resolve(this.activeSelection);
if (notify) {
this.events.trigger(
AnnotationEvents.ON_SET_SELECTION,
this.activeSelection
);
}
});
};
// Saves an annotation based on the current selection.
// if there is an active annotation, that means the selection updates the target of that annotation
// always make sure to SET the selection before saving it, since this gives other components the chance to
// respond to the ON_SET_SELECTION event triggered (see setActiveSelection)
async saveSelection(
selection = null,
setActive = true,
notify = true,
layerId = null
) {
//merge with save()
if (selection != null) {
this.activeSelection = await this.setActiveSelection(selection); //enables "stateless" calls
}
if (!this.activeSelection) {
alert("no active selection, cannot save");
return;
}
if (
this.activeAnnotation &&
this.activeAnnotation.target.selector.refinedBy
) {
//update the selection
this.activeAnnotation.target.selector.refinedBy = MSAnnotationUtil.__generateRefinedBy(
this.activeSelection
);
} else if (this.activeTarget) {
//create new annotation with the selection
this.activeAnnotation = this.newAnnotation();
} else {
alert("error: trying to save selection without an active target");
return new Promise((resolve) => {
resolve(null);
});
}
// set Layer ID to annotation target
if (layerId !== null) {
this.activeAnnotation.target.layerId = layerId;
}
await this.save(setActive, notify);
//return the promise and trigger listeners if needed TODO merge with save as well
return new Promise((resolve) => {
const data = {
annotation: this.activeAnnotation,
segment: this.activeSelection,
};
resolve(data);
});
}
/* ------------------------- EDIT, PLAY, SET/ACTIVATE ANNOTATION --------------------------------- */
edit = (annotation, bodyElement = null, notify = true) => {
this.__activateAndTrigger(
AnnotationEvents.ON_EDIT,
annotation,
bodyElement,
notify
);
};
play = (annotation, bodyElement = null, notify = true) => {
this.__activateAndTrigger(
AnnotationEvents.ON_PLAY,
annotation,
bodyElement,
notify
);
};
setActiveAnnotation = (annotation, bodyElement = null, notify = true) => {
// (currently) the can only be one active annotation
this.__activateAndTrigger(
AnnotationEvents.ON_SET_ANNOTATION,
annotation,
bodyElement,
notify
);
};
setActiveBodyElement = (bodyElement, notify = true) => {
// activate a bodyElement within the active annotation
this.__activateAndTrigger(
AnnotationEvents.ON_SET_ANNOTATION,
this.activeAnnotation,
bodyElement,
notify
);
};
//all called by edit, play & setActiveAnnotation. The only difference is the event triggered
__activateAndTrigger = (
eventType,
annotation,
bodyElement = null,
notify = true
) => {
this.activeAnnotation = ObjectUtil.clone(annotation);
this.activeBody = ObjectUtil.clone(bodyElement);
if (annotation && annotation.target) {
//TODO simplify these functions as well
//TODO this should not perse trigger a new ON_SET_SELECTION
this.setActiveSelection(
MSAnnotationUtil.extractSelectionFromTarget(annotation.target)
);
}
if (notify) {
if (eventType != AnnotationEvents.ON_SET_ANNOTATION) {
this.events.trigger(eventType, {
annotation: this.activeAnnotation,
bodyElement: this.activeBody,
});
}
//for "edit" & "play" events also trigger the "set" listeners
this.events.trigger(AnnotationEvents.ON_SET_ANNOTATION, {
annotation: this.activeAnnotation,
bodyElement: this.activeBody,
});
}
};
/* ------------------------- CHANGE ANNOTATION TARGET --------------------------------- */
/*
TODO This function needs to be called whenever something is selected on the screen. This function
then takes care of finding or creating the corresponding annotation.
Concerning the parameters, either an annotation or a list of consecutive PID elements is supplied:
[collectionId, resourceId, mediaObject, segment] TODO better think about passing this
Most importantly: setting an active target ALWAYS results in (re)setting the active annotation.
This way it is clear that changing/making a target on the screen OR selecting an existing annotation,
results in the same thing: a new active annotation.
The only difference (maybe) is that an existing annotation is already saved on the server.
*/
//only called by the resourceviewer.jsx, so update there
//TODO rename to loadAnnotationsForTarget()
//TODO add the NPID template, ids & nonW3Cparams as params
async setActiveTarget(
ids,
nonW3Cparams = {},
additionalTypes = [],
notify = true
) {
this.activeTarget = {
ids: ids,
nonW3Cparams: nonW3Cparams,
additionalTypes: additionalTypes, //remember these too (see saveSelection())
npid: MSAnnotationUtil.constructNPID(
this.activeTemplate,
ids,
false,
additionalTypes
),
};
this.activeAnnotation = null; //no active annotation when this is changed
this.activeSelection = null; //nothing is selected when changing the active (annotatable) target
this.annotations = await this.refreshTargetAnnotations(); //always uses this.activeTarget to refresh the annotations
return new Promise((resolve) => {
resolve(this.activeTarget);
if (notify) {
this.events.trigger(
AnnotationEvents.ON_CHANGE_TARGET,
this.activeTarget
);
}
});
}
//constructs a new annotation based on the active target, selection & a desired semantic type (if any)
newAnnotation = (semanticType = null, useSelection = true) => {
if (!this.activeTarget) {
console.error(
"Something is wrong with your active target: do you have an active media object?",
"TODO fix annotation of resources without a media object"
);
return null;
}
const level = MSAnnotationUtil.getLevelOfSemanticType(
this.activeTemplate.nestedPID,
semanticType
);
if (level === -1 && semanticType) {
alert(
"error: please supply a valid semantic type; " +
semanticType +
" is not part of the template"
);
return null;
}
const selection = useSelection === true ? this.activeSelection : null;
const idsToUse = semanticType
? this.activeTarget.ids.slice(0, level + 1)
: this.activeTarget.ids;
const additionalTypesToUse = semanticType
? level === this.activeTarget.ids.length - 1
? this.activeTarget.additionalTypes
: []
: this.activeTarget.additionalTypes;
const annotation = MSAnnotationUtil.newAnnotationFromNPID(
MSAnnotationUtil.constructNPID(
this.activeTemplate,
idsToUse,
useSelection, //whether to use add the selectionType to the NPID or not
additionalTypesToUse //only use the additional type if it matches the level
),
selection, //FIXME: what if there is no selection, but useSelection is true?
this.activeTarget.nonW3Cparams
);
return annotation;
};
/* --------------------------- TEMPORAL SEGMENTATION HELPERS -------------------- */
setTemporalSegment = (start, end) => {
this.setActiveSelection({ type: "temporal", start: start, end: end });
};
newTemporalSegment = (start = -1, end = -1) => {
this.setActiveAnnotation(null);
this.setTemporalSegment(start, end);
};
// TODO add callback function
newTemporalSegmentFromLast = (seekFunc) => {
if (this.activeSelection.end > 0) {
this.setActiveAnnotation(null);
this.setTemporalSegment(this.activeSelection.end, -1);
if (seekFunc && typeof seekFunc === "function") {
seekFunc(this.activeSelection.end);
}
} else {
this.newTemporalSegment();
}
};
setStart = (pos) =>
this.setTemporalSegment(
pos,
this.activeSelection ? this.activeSelection.end : -1
);
setEnd = (pos) =>
this.setTemporalSegment(
this.activeSelection ? this.activeSelection.start : -1,
pos
);
}
export class MSAnnotationUtil {
static constructNPID = (
npidTemplate,
ids,
useSelection = false,
additionalTypes = []
) => {
if (!npidTemplate) {
alert(
"Error: you have to supply an NPID template before a NPID can be constructed"
);
return null;
}
const template = ObjectUtil.clone(npidTemplate); //make sure to always create a new object to avoid weird behaviour
//then grab the relevant part of the template
const nestedPID = template.nestedPID
.slice(0, ids.length)
.map((e, i) => {
return {
id: ids[i], //assign the supplied id
type: e.type,
property: e.property,
};
});
//add the selection type (e.g. Segment)
if (useSelection && template.selectionType) {
nestedPID[nestedPID.length - 1].type.push(template.selectionType);
}
//add the additional types
nestedPID[nestedPID.length - 1].type.push(...additionalTypes);
return nestedPID;
};
static newAnnotationFromNPID = (npid, selection, nonW3Cparams = {}) => {
let target = {
type: npid[npid.length - 1].type[0], // always use the first type for the target.source
source: npid[npid.length - 1].id,
selector: {
type: "NestedPIDSelector",
value: npid,
},
};
//Then assign the selection bit as a refinedBy element
const refinedBy = selection
? MSAnnotationUtil.__generateRefinedBy(selection)
: null;
if (refinedBy) {
target.selector.refinedBy = refinedBy;
target.type = selection.semanticType;
}
//finally return the generated annotation
return Object.assign(
{
id: null, //generated on the server
body: null,
target: target,
},
nonW3Cparams
); //merge the custom params
};
/*
temporal segment looks like:
{start: start, end: end}
spatial segment looks like:
{
rect : {
x : rect.x,
y : rect.y,
w : rect.width,
h : rect.height
},
rotation : rect.rotation
}
*/
static __generateRefinedBy = (selection) => {
if (!selection) return null;
if (selection.rect && typeof selection.rect == "object") {
//spatial
return {
type: "FragmentSelector",
conformsTo: "http://www.w3.org/TR/media-frags/",
value:
"#xywh=" +
selection.rect.x +
"," +
selection.rect.y +
"," +
selection.rect.w +
"," +
selection.rect.h,
rect: selection.rect, //TODO remove custom MS prop
};
} else if (selection.start >= 0 && selection.end >= 0) {
//temporal
return {
type: "FragmentSelector",
conformsTo: "http://www.w3.org/TR/media-frags/",
value: "#t=" + selection.start + "," + selection.end,
start: selection.start, //TODO remove custom MS prop
end: selection.end, //TODO remove custom MS prop
};
}
return null;
};
//almost as important as being able to extract & construct an NPID.
//TODO model selections in a class
//TODO think about multi-target annotations; is it likely there will be multiple targets with selections?
static extractSelectionFromTarget = (target) => {
//selections are ALWAYS inside the refinedBy part of the target's selector
if (!target || !target.selector || !target.selector.refinedBy)
return null;
if (target.selector.refinedBy.start !== undefined) {
return {
type: "temporal",
start: target.selector.refinedBy.start,
end: target.selector.refinedBy.end,
};
} else if (target.selector.refinedBy.rect) {
return {
type: "spatial",
rect: target.selector.refinedBy.rect,
};
} //TODO add text selection bit
};
/* --------------------------- MISC HELPER FUNCTIONS --------------------------- */
//this is so components can request part of the currently active NPID, e.g.
//"give me the ID for the currently selected Image / MediaObject / etc"
//NB not used yet!
static getIdFromTarget = (activeTarget, semanticType) => {
if (!activeTarget || !semanticType) return null;
//first find how deep the semanticType is in the pid template
const level = activeTemplate.findIndex(
(ot) => ot.type.indexOf(semanticType) != -1
);
//then just return the id stored in that level
return activeTarget.ids[level];
};
static getLevelOfSemanticType = (npidTemplate, semanticType) => {
return npidTemplate.findIndex(
(pid) => pid.type.indexOf(semanticType) !== -1
);
};
static getNPIDLength = (annotation) => {
return MSAnnotationUtil.hasNPID(annotation)
? annotation.target.selector.value.length
: -1;
};
static hasNPID = (annotation) =>
annotation &&
annotation.target &&
annotation.target.selector &&
annotation.target.selector.type === "NestedPIDSelector";
}
export class InformationCardUtil {
/*
{
"mediaObject" : {
"templates" : ["generic"],
"cardsPerUser" : 10
},
"mediaSegment" : {
"templates" : ["generic"],
"cardsPerUser" : 10
},
"templates" : {
"generic" : {
"id" : "generic",
"label" : "Generic",
"properties" : [
{"key" : "title", "type" : "string"},
{"key" : "description", "type" : "string"},
{"key" : "date", "type" : "date"}
]
}
}
}
*/
static determinePossibleTemplates = (metadataConfig, annotation) => {
if (!annotation.target) return null;
if (
annotation.target.selector.refinedBy &&
metadataConfig.mediaSegment
) {
//return the first template defined for media segments
return metadataConfig.mediaSegment.templates
? metadataConfig.mediaSegment.templates.map((key) => {
return metadataConfig.templates[key];
})
: null;
} else if (
!annotation.target.selector.refinedBy &&
metadataConfig.mediaObject
) {
//return the first template defined for media objects
return metadataConfig.mediaObject.templates
? metadataConfig.mediaObject.templates.map((key) => {
return metadataConfig.templates[key];
})
: null;
}
return null;
};
//this is for determining whether the user only able to edit a single card. If so the card list won't be dispalyed
static determineSingleCardMode(metadataConfig, annotation) {
if (!annotation.target) return false;
//if there is more than one template, always return false, otherwise check whether cardsPerUser is set to 1
if (
annotation.target.selector.refinedBy &&
metadataConfig.mediaSegment
) {
//do this check for the media segment config
return (
metadataConfig.mediaSegment.templates &&
metadataConfig.mediaSegment.cardsPerUser === 1
);
} else if (
!annotation.target.selector.refinedBy &&
metadataConfig.mediaObject
) {
//do this check for the media object config
return (
metadataConfig.mediaObject.templates &&
metadataConfig.mediaObject.cardsPerUser === 1
);
}
return false;
}
}
export class IDUtil {
//used to generate a more compact form for unique strings (e.g. collection names) to be used as guid
static hashCode = (s) => {
let hash = 0,
i,
chr,
len;
if (s.length === 0) return hash;
for (i = 0, len = s.length; i < len; i++) {
chr = s.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
//generates a guid from nothing
static guid = () => {
return (
IDUtil.__s4() +
IDUtil.__s4() +
"-" +
IDUtil.__s4() +
"-" +
IDUtil.__s4() +
"-" +
IDUtil.__s4() +
"-" +
IDUtil.__s4() +
IDUtil.__s4() +
IDUtil.__s4()
);
};
//only used by the guid function
static __s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
//all component specific class names should be generated with this function
//class names consist of: bg__[component-prefix]__[semantically-intelligble-component-attribute]
static cssClassName = (componentAttribute, componentPrefix) => {
if (componentPrefix) {
return "bg__" + componentPrefix + "__" + componentAttribute;
}
return "bg__" + componentAttribute;
};
}