taggd
Version:
A tool to show and add tags to images
387 lines (320 loc) • 10.6 kB
JavaScript
const EventEmitter = require('../util/event-emitter');
const ObjectIs = require('../util/object-is');
const TypeErrorMessage = require('../util/type-error-message');
class Tag extends EventEmitter {
/**
* Create a new Tag instance
* @param {{ x: Number, y: Number }} position - The tag’s coordinates
* @param {String|Function} text - The tag’s content
* @param {Object} [buttonAttributes = {}] - The button’s attributes
* @param {Object} [popupAttributes = {}] - The popup’s attributes
*/
constructor(position, text, buttonAttributes = {}, popupAttributes = {}) {
if (!ObjectIs.ofType(position, 'object') || Array.isArray(position)) {
throw new TypeError(TypeErrorMessage.getObjectMessage(position));
} else if (!('x' in position) || !('y' in position)) {
throw new Error(`${position} should have x and y property`);
}
super();
this.wrapperElement = document.createElement('div');
this.wrapperElement.classList.add('taggd__wrapper');
this.buttonElement = document.createElement('button');
this.buttonElement.classList.add('taggd__button');
this.popupElement = document.createElement('span');
this.popupElement.classList.add('taggd__popup');
this.wrapperElement.appendChild(this.buttonElement);
this.wrapperElement.appendChild(this.popupElement);
this.isControlsEnabled = false;
this.inputLabelElement = undefined;
this.buttonSaveElement = undefined;
this.buttonDeleteElement = undefined;
this.buttonSaveElementClickHandler = () => this.setText(this.inputLabelElement.value);
this.buttonDeleteElementClickHandler = () => {
this.emit('taggd.tag.delete', this);
};
this.text = undefined;
this.setButtonAttributes(buttonAttributes);
this.setPopupAttributes(popupAttributes);
this.setPosition(position.x, position.y);
this.setText(text);
this.hide();
}
/**
* Subscribe to an event.
* @param {String} eventName - The event to subscribe to.
* @param {Function} handler - The handler to execute.
* @return {Taggd} Current Taggd instance
*/
on(eventName, handler) {
return super.on(eventName, handler);
}
/**
* Unsubscribe from an event.
* @param {String} eventName - The event to unsubscribe from.
* @param {Function} handler - The handler that was used to subscribe.
* @return {Taggd} Current Taggd instance
*/
off(eventName, handler) {
return super.off(eventName, handler);
}
/**
* Subscribe to an event and unsubscribe once triggered.
* @param {String} eventName - The event to subscribe to.
* @param {Function} handler - The handler to execute.
* @return {Taggd} Current Taggd instance
*/
once(eventName, handler) {
return super.once(eventName, handler);
}
/**
* Test whether the tag is hidden or not
* @return {Boolean} A boolean indicating the tag’s state
*/
isHidden() {
return this.popupElement.style.display === 'none';
}
/**
* Show the tag
* @return {Taggd.Tag} Current Tag
*/
show() {
const isCanceled = !this.emit('taggd.tag.show', this);
if (!isCanceled) {
this.popupElement.style.display = '';
this.emit('taggd.tag.shown', this);
}
return this;
}
/**
* Hide the tag
* @return {Taggd.Tag} Current Tag
*/
hide() {
const isCanceled = !this.emit('taggd.tag.hide', this);
if (!isCanceled) {
this.popupElement.style.display = 'none';
this.emit('taggd.tag.hidden', this);
}
return this;
}
/**
* Set the tag’s text
* @param {String|Function} text - The tag’s content
* @return {Taggd.Tag} Current Tag
*/
setText(text) {
if (!ObjectIs.ofType(text, 'string') && !ObjectIs.function(text)) {
throw new TypeError(TypeErrorMessage.getMessage(text, 'a string or a function'));
}
const isCanceled = !this.emit('taggd.tag.change', this);
if (!isCanceled) {
if (ObjectIs.function(text)) {
this.text = text(this);
} else {
this.text = text;
}
if (!this.isControlsEnabled) {
this.popupElement.innerHTML = this.text;
} else {
this.inputLabelElement.value = this.text;
}
this.emit('taggd.tag.changed', this);
}
return this;
}
/**
* Set the tag’s position
* @param {Number} x - The tag’s x-coordinate
* @param {Number} y - The tag’s y-coordinate
* @return {Taggd.Tag} Current Tag
*/
setPosition(x, y) {
if (!ObjectIs.number(x)) {
throw new TypeError(TypeErrorMessage.getFloatMessage(x));
}
if (!ObjectIs.number(y)) {
throw new TypeError(TypeErrorMessage.getFloatMessage(y));
}
const isCanceled = !this.emit('taggd.tag.change', this);
if (!isCanceled) {
const positionStyle = Tag.getPositionStyle(x, y);
this.wrapperElement.style.left = positionStyle.left;
this.wrapperElement.style.top = positionStyle.top;
this.emit('taggd.tag.changed', this);
}
return this;
}
/**
* Set the tag button’s attributes
* @param {Object} atttributes = {} - The attributes to set
* @return {Taggd.Tag} Current tag
*/
setButtonAttributes(attributes = {}) {
if (!ObjectIs.ofType(attributes, 'object') || Array.isArray(attributes)) {
throw new TypeError(TypeErrorMessage.getObjectMessage(attributes));
}
const isCanceled = !this.emit('taggd.tag.change', this);
if (!isCanceled) {
Tag.setElementAttributes(this.buttonElement, attributes);
this.emit('taggd.tag.changed', this);
}
return this;
}
/**
* Set the tag popup’s attributes
* @param {Object} atttributes = {} - The attributes to set
* @return {Taggd.Tag} Current tag
*/
setPopupAttributes(attributes = {}) {
if (!ObjectIs.ofType(attributes, 'object') || Array.isArray(attributes)) {
throw new TypeError(TypeErrorMessage.getObjectMessage(attributes));
}
const isCanceled = !this.emit('taggd.tag.change', this);
if (!isCanceled) {
Tag.setElementAttributes(this.popupElement, attributes);
this.emit('taggd.tag.changed', this);
}
return this;
}
/**
* Enables the tag controls
* @return {Taggd.Tag} Current tag
*/
enableControls() {
this.isControlsEnabled = true;
this.inputLabelElement = document.createElement('input');
this.buttonSaveElement = document.createElement('button');
this.buttonDeleteElement = document.createElement('button');
this.inputLabelElement.classList.add('taggd__editor-input');
this.buttonSaveElement.classList.add(
'taggd__editor-button',
'taggd__editor-button--save'
);
this.buttonDeleteElement.classList.add(
'taggd__editor-button',
'taggd__editor-button--delete'
);
this.buttonSaveElement.innerHTML = Tag.LABEL_BUTTON_SAVE;
this.buttonDeleteElement.innerHTML = Tag.LABEL_BUTTON_DELETE;
this.buttonSaveElement.addEventListener('click', this.buttonSaveElementClickHandler);
this.buttonDeleteElement.addEventListener('click', this.buttonDeleteElementClickHandler);
this.popupElement.innerHTML = '';
this.popupElement.appendChild(this.inputLabelElement);
this.popupElement.appendChild(this.buttonSaveElement);
this.popupElement.appendChild(this.buttonDeleteElement);
// Set input content
this.setText(this.text);
return this;
}
/**
* Disabled the tag controls
* @return {Taggd.Tag} Current tag
*/
disableControls() {
this.isControlsEnabled = false;
this.inputLabelElement = undefined;
this.buttonSaveElement = undefined;
this.buttonDeleteElement = undefined;
// Remove elements and set set content
this.setText(this.text);
return this;
}
/**
* Get a Taggd.createFromObject-compatible object
* @return {Object} A object for JSON
*/
toJSON() {
function getAttributes(rawAttributes) {
const attributes = {};
Array.prototype.forEach.call(rawAttributes, (attribute) => {
if (attribute.name === 'class' || attribute.name === 'style') {
return;
}
attributes[attribute.name] = attribute.value;
});
return attributes;
}
return {
position: {
x: parseFloat(this.wrapperElement.style.left) / 100,
y: parseFloat(this.wrapperElement.style.top) / 100,
},
text: this.text,
buttonAttributes: getAttributes(this.buttonElement.attributes),
popupAttributes: getAttributes(this.popupElement.attributes),
};
}
/**
* Set element attributes
* @param {DomNode} element - The element the attributes should be set to
* @param {Object} [attributes = {}] - A map of attributes to set
* @return {DomNode} The original element
*/
static setElementAttributes(element, attributes = {}) {
if (!ObjectIs.ofType(attributes, 'object') || Array.isArray(attributes)) {
throw new TypeError(TypeErrorMessage.getObjectMessage(attributes));
}
Object.entries(attributes).forEach((attribute) => {
const [attributeName, attributeValue] = attribute;
if (attributeName === 'class' && element.getAttribute(attributeName)) {
const classValue = `${element.getAttribute(attributeName)} ${attributeValue}`;
element.setAttribute(attributeName, classValue);
return;
}
element.setAttribute(attributeName, attributeValue);
});
return element;
}
/**
* Get the position style
* @param {Number} x - The tag’s x-coordinate
* @param {Number} y - The tag’s y-coordinate
* @return {Object} The style
*/
static getPositionStyle(x, y) {
if (!ObjectIs.number(x)) {
throw new TypeError(TypeErrorMessage.getFloatMessage(x));
}
if (!ObjectIs.number(y)) {
throw new TypeError(TypeErrorMessage.getFloatMessage(y));
}
return {
left: `${x * 100}%`,
top: `${y * 100}%`,
};
}
/**
* Create a tag from object
* @param {Object} object - The object containing all information
* @return {Tag} The created Tag instance
*/
static createFromObject(object) {
return new Tag(
object.position,
object.text,
object.buttonAttributes,
object.popupAttributes
);
}
}
/**
* Label for a new tag
* @const
* @type {String}
* @ignore
*/
Tag.LABEL_NEW_TAG = 'New tag';
/**
* Label for save button
* @const
* @type {String}
* @ignore
*/
Tag.LABEL_BUTTON_SAVE = 'save';
/**
* Label for delete button
* @const
* @type {String}
*/
Tag.LABEL_BUTTON_DELETE = 'delete';
module.exports = Tag;