camman3d/jerboa.js

View on GitHub
src/html-manip.js

Summary

Maintainability
F
3 days
Test Coverage
import { getGlobalOffset } from './positioning';
import { emit } from './events';
import state from './state';
import { isSafari } from './utils';
/*
 HTML Manipulation Methods
 -------------------------
 */

let openSpot;
export let annotationPositions = [];


// Methods for testing
export function __getOpenSpot() {
    return openSpot;
}

export function __setOpenSpot(spot) {
    openSpot = spot;
}

// second parameter is true if the point is being initially loaded
export function createMarker(payload, init) {
    const pos = payload.position;
    const container = document.querySelector(pos.container);
    let spot = document.createElement('div');
    let left, top;
    spot.classList.add('feedback-spot');
    if (init) {
        spot.classList.add('off');
    }

    annotationPositions.push([spot, pos, container]);

    let calculatedPosition = calculatePosition(pos, container);
    // const offset = getGlobalOffset(container);
    // if (pos.positioning === 'PIXEL') {
    //     left = offset[0] + pos.offset['x'];
    //     top = offset[1] + pos.offset['y'];
    // } else if (pos.positioning === 'PERCENT') {
    //     const percentX = pos.offset['x'] / pos.containerSize.width;
    //     const percentY = pos.offset['y'] / pos.containerSize.height;
    //     const rect = container.getBoundingClientRect();
    //     left = offset[0] + rect.width * percentX;
    //     top = offset[1] + rect.height * percentY;
    // }
    spot.style.top = calculatedPosition.top + 'px';
    spot.style.left = calculatedPosition.left + 'px';


    document.body.appendChild(spot);
    return spot;
}

function calculatePosition(pos, container) {
    const offset = getGlobalOffset(container);
    let top, left;

    if (pos.positioning === 'PIXEL') {
        left = offset[0] + pos.offset['x'];
        top = offset[1] + pos.offset['y'];
    } else if (pos.positioning === 'PERCENT') {
        const percentX = pos.offset['x'] / pos.containerSize.width;
        const percentY = pos.offset['y'] / pos.containerSize.height;
        const rect = container.getBoundingClientRect();
        left = offset[0] + rect.width * percentX;
        top = offset[1] + rect.height * percentY;
    }

    return {
        top: top,
        left: left
    }
}

export function resetPositioning(spot, pos, container) {
    let calculatedPosition = calculatePosition(pos, container);
    spot.style.top = calculatedPosition.top + 'px';
    spot.style.left = calculatedPosition.left + 'px';
}

export function addBox(spot, toggled) {
    spot.addEventListener('click', event => {
        if (!state.active) {
            return;
        }
        event.stopPropagation();

        if (toggled) {
            spot.classList.toggle('active');
            if (openSpot !== spot) {
                closeInfoBox();
                openSpot = spot;
            } else {
                openSpot = null;
            }
        }
    });

    let box = document.createElement('div');
    box.classList.add('feedback-box');
    if (toggled) {
        box.classList.add('toggled');
    }
    box.addEventListener('click', event => {
        event.stopPropagation();
    });
    spot.appendChild(box);

    let container = document.createElement('div');
    container.classList.add('feedback-container');
    box.appendChild(container);

    return {box, container};
}

export function closeInfoBox() {
    if (openSpot) {
        openSpot.classList.remove('active');
        openSpot = null;
    }
}

// addText function renders a an annotation or comment
export function addText(container, payload, className) {
    let text = document.createElement('div');
    text.classList.add('feedback-text');
    if (className) {
        text.classList.add(className);
    }
    container.appendChild(text);


    let comment = document.createElement('div');
    comment.classList.add('feedback-comment');
    comment.textContent = payload.text;
    text.appendChild(comment);

    let info = document.createElement('div');
    info.classList.add('feedback-info');
    const rawTime = !!payload.time.match(/.*[Z]$/) ? payload.time : payload.time + 'Z';
    let parsedTime = isSafari() ? (Date.parse(rawTime) + new Date().getTimezoneOffset() * 60000) : Date.parse(rawTime);
    const time = new Date(parsedTime);
    info.textContent = 'By ' + (payload.user || state.currentUser || 'unknown user') + ' at ' + time.toLocaleString('en-US', {weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC', hour12: true, hour: 'numeric', minute: 'numeric'});
    text.appendChild(info);

    if (parseInt(payload.userId) === parseInt(state.currentUserId)) {
        let deleteBtn = document.createElement('a');
        deleteBtn.classList.add('delete-button');
        deleteBtn.innerText = 'X';
        deleteBtn.setAttribute('role', 'button');
        deleteBtn.setAttribute('href', '#');
        // don't render delete button for original annotation comment
        if (!payload.hasOwnProperty('comments')) {
            info.appendChild(deleteBtn);
        };

        let editBtn = document.createElement('a');
        editBtn.classList.add('edit-button');
        editBtn.innerText = 'Edit';
        editBtn.setAttribute('role', 'button');
        editBtn.setAttribute('href', '#');
        info.appendChild(editBtn);

        deleteBtn.addEventListener('click', (e) => {
            e.preventDefault();
            container.removeChild(text);
            emit('deleteComment', payload);
        });

        editBtn.addEventListener('click', (e) => {
            e.preventDefault();
            let editCommentTextField = addTextField(null, null, 'comment-textfield');
            editCommentTextField.textarea.value = payload.text;

            editCommentTextField.cancel.addEventListener('click', () => {
                container.replaceChild(text, editCommentTextField.container);
            });

            editCommentTextField.save.addEventListener('click', () => {
                // if content didn't change, just put back old comment (don't change)
                if (payload.text === editCommentTextField.textarea.value) {
                    container.replaceChild(text, editCommentTextField.container);
                } else {
                    // change content
                    payload.text = editCommentTextField.textarea.value;
                    text.children[0].textContent = payload.text;
                    container.replaceChild(text, editCommentTextField.container);
                    emit('saveEdittedComment', payload);
                }
            });

            container.replaceChild(editCommentTextField.container, text);
            emit('editComment');
        });
    }

    // let replyBtn = document.createElement('a');
    // replyBtn.classList.add('reply-button');
    // replyBtn.innerText = 'Reply';
    // replyBtn.setAttribute('role', 'button');
    // replyBtn.setAttribute('href', '#');
    // text.appendChild(replyBtn);

    // // if there are replies, render them
    // let repliesContainer = document.createElement('div');
    // repliesContainer.classList.add('replies-container');
    // text.appendChild(repliesContainer);

    // if (payload.replies) {
    //     payload.replies.forEach(reply => {
    //         addReply(repliesContainer, reply);
    //     });
    // };

    // replyBtn.addEventListener('click', (e) => {
    //     e.preventDefault();
    //     let parts;

    //     // if the repliesContainer is empty or if the last element in the replies container contains the class 'reply-textfield'
    //     if (repliesContainer && (!repliesContainer.lastElementChild || !repliesContainer.lastElementChild.classList.contains('reply-textfield'))) {
    //         parts = addTextField(repliesContainer, 'Reply:', 'reply-textfield');

    //         parts.cancel.addEventListener('click', () => {
    //             const reply = generateReply(parts.textarea.value);
    //             parts.textarea.value = '';
    //             emit('cancelReply', reply);
    //             closeInfoBox();
    //         });

    //         parts.save.addEventListener('click', () => {
    //             const reply = generateReply(parts.textarea.value);
    //             parts.textarea.value = '';
    //             payload.replies.push(reply);
    //             emit('saveReply', payload);

    //             repliesContainer.removeChild(parts.container);
    //             addReply(repliesContainer, reply);
    //         });
    //     }

    // });

    return text;
}

// export function addReply(container, payload) {
//     let replyContainer = document.createElement('div');
//     replyContainer.classList.add('reply-container');
//     container.appendChild(replyContainer);

//     let reply = document.createElement('div');
//     reply.classList.add('feedback-reply');
//     reply.textContent = payload.text;
//     replyContainer.appendChild(reply);

//     let info = document.createElement('div');
//     info.classList.add('feedback-info');
//     const time = new Date(payload.time);
//     info.textContent = 'By ' + (payload.user || 'unknown user') + ' at ' + time.toLocaleString();
//     // if the user is the creator of the comment, show the delete and edit
//     if (payload.user === state.currentUser) {
//         let deleteBtn = document.createElement('a');
//         deleteBtn.classList.add('delete-button');
//         deleteBtn.innerText = 'X';
//         deleteBtn.setAttribute('role', 'button');
//         deleteBtn.setAttribute('href', '#');
//         info.appendChild(deleteBtn);

//         let editBtn = document.createElement('a');
//         editBtn.classList.add('edit-button');
//         editBtn.innerText = 'Edit';
//         editBtn.setAttribute('role', 'button');
//         editBtn.setAttribute('href', '#');
//         info.appendChild(editBtn);

//         deleteBtn.addEventListener('click', (e) => {
//             e.preventDefault();
//             container.removeChild(replyContainer);
//             emit('deleteReply');
//         });

//         editBtn.addEventListener('click', (e) => {
//             e.preventDefault();
//             emit('editReply');
//         });
//     }

//     replyContainer.appendChild(info);

//     return replyContainer;
// }

export function addTextField(boxContainer, label, containerClass) {
    let container = document.createElement('div');
    if (containerClass) {
        container.classList.add(containerClass)
    };
    if (boxContainer) {
        boxContainer.appendChild(container)
    };

    if (label) {
        let fieldLabel = document.createElement('label');
        fieldLabel.textContent = label;
        container.appendChild(fieldLabel);
    };

    let textarea = document.createElement('textarea');
    container.appendChild(textarea);

    let buttonHolder = document.createElement('div');
    buttonHolder.classList.add('button-holder');
    container.appendChild(buttonHolder);

    let cancel = document.createElement('button');
    cancel.classList.add('cancel-button');
    cancel.innerText = 'Cancel';
    buttonHolder.appendChild(cancel);

    let save = document.createElement('button');
    save.classList.add('save-button');
    save.innerText = 'Save';
    buttonHolder.appendChild(save);

    return {cancel, save, textarea, container};
}

// const generateReply = text => ({
//     time: new Date().toISOString(),
//     user: state.currentUser,
//     text
// });

const generateComment = text => ({
    time: new Date(Date.now() - (new Date().getTimezoneOffset() * 60000)).toISOString(),
    user: state.currentUser,
    userId: state.currentUserId,
    text
    // replies: []
});



export function createInfoBox(spot, payload) {
    function changeColor(classList, currentClassName, newClassName) {
        if (classList.contains(currentClassName)) {classList.remove(currentClassName)};
        if (!classList.contains(newClassName)) {classList.add(newClassName)};
    }

    // function changeOuterColor(classList, className) {
    //     classList.forEach((value, index) => {
    //         if (value.includes('owner-')) {
    //             classList.remove(value);
    //         }
    //     });
    //     classList.add(className);
    // }
    // function changeInnerColor(classList, className) {
    //     classList.forEach((value, index) => {
    //         if (value.includes('status-')) {
    //             classList.remove(value);
    //         }
    //     });
    //     classList.add(className);
    // }

    const boxParts = addBox(spot, true);
    const ownerOptions = {
        'PM': 'pm',
        'Client': 'client',
        'ID': 'id',
        'IxD': 'ixd',
        'Video': 'video'
    };
    const statusOptions = {
        'Open': 'open',
        'Closed': 'closed',
        'No Action': 'no-action'
    };

    // add owner and status
    const defaultOwner = 'pm';
    let ownerLabel = document.createElement('label');
    ownerLabel.innerHTML = 'Assigned To:';
    let statusLabel = document.createElement('label');
    statusLabel.innerHTML = 'Status:';
    let ownerSelect = document.createElement('select');
    if (!state.isAdmin) {
        ownerSelect.disabled = true;
    }
    boxParts.container.appendChild(ownerLabel);
    boxParts.container.appendChild(ownerSelect);
    let ownerOptionsArr = Object.keys(ownerOptions);
    for (let i = 0; i < ownerOptionsArr.length; i++) {
        let option = document.createElement('option');
        option.value = ownerOptions[ownerOptionsArr[i]];
        option.text = ownerOptionsArr[i];
        ownerSelect.appendChild(option);
    }
    let currentAssigneeRole = payload.assigneeRole;
    if (payload.assigneeRole && (payload.assigneeRole !== defaultOwner)) {
        ownerSelect.value = payload.assigneeRole;
        changeColor(spot.classList, null, `owner-${payload.assigneeRole}`)
    } else {
        ownerSelect.value = defaultOwner;
    }
    ownerSelect.addEventListener('change', (e) => {
        payload.assigneeRole = e.target.value;
        changeColor(spot.classList, `owner-${currentAssigneeRole}`, `owner-${payload.assigneeRole}`)
        currentAssigneeRole = payload.assigneeRole;
        emit('changeOwner', payload);
    });

    const defaultStatus = 'open';
    let statusSelect = document.createElement('select');
    if (!state.isAdmin) {
        statusSelect.disabled = true;
    }
    boxParts.container.appendChild(statusLabel);
    boxParts.container.appendChild(statusSelect);
    let statusOptionsArr = Object.keys(statusOptions);
    for (let i = 0; i < statusOptionsArr.length; i++) {
        let option = document.createElement('option');
        option.value = statusOptions[statusOptionsArr[i]];
        option.text = statusOptionsArr[i];
        statusSelect.appendChild(option);
    }
    let currentStatus = payload.status;
    if (payload.status && (payload.status !== defaultStatus)) {
        statusSelect.value = payload.status;
        changeColor(spot.classList, null, `status-${payload.status}`)
    } else {
        statusSelect.value = defaultStatus;
    }
    statusSelect.addEventListener('change', (e) => {
        payload.status = e.target.value;
        changeColor(spot.classList, `status-${currentStatus}`, `status-${payload.status}`)
        currentStatus = payload.status;
        emit('changeStatus', payload);
    });

    // add each comment to container
    addText(boxParts.container, payload); // first comment in root of payload
    payload.comments.forEach(comment => {
        addText(boxParts.container, comment, 'comment-reply');
    });

    let parts = addTextField(boxParts.container, 'Comment:', 'comment-textfield');
    parts.cancel.addEventListener('click', () => {
        const comment = generateComment(parts.textarea.value);
        parts.textarea.value = '';
        emit('cancelComment', comment);
        closeInfoBox();
    });

    parts.save.addEventListener('click', () => {
        if (parts.textarea.value) {
            const comment = generateComment(parts.textarea.value);
            parts.textarea.value = '';
            payload.comments.push(comment);
            emit('saveComment', payload);

            boxParts.container.removeChild(parts.container);
            addText(boxParts.container, comment, 'comment-reply');
            boxParts.container.appendChild(parts.container);
        } else {
            const comment = generateComment(parts.textarea.value);
            parts.textarea.value = '';
            emit('cancelComment', comment);
            closeInfoBox();
        }
    });

    return Object.assign({}, parts, boxParts);
}