function toggleFullScreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); } else if (document.exitFullscreen) { document.exitFullscreen(); } } let cards = {}; let totalcolumns = 0; let columns = []; let currentTheme = 'bigcards'; let boardInitialized = false; let keyTrap = null; const baseurl = location.pathname.substring( 0, location.pathname.lastIndexOf('/'), ); const socket = io.connect({ path: `${baseurl}/socket.io` }); console.log(socket); moment.locale(navigator.language || navigator.languages[0]); marked.setOptions({ sanitize: true }); // an action has happened, send it to the // server function sendAction(a, d) { // console.log('--> ' + a); const message = { action: a, data: d, }; socket.emit(a, message); } socket.on('connect', () => { // console.log('successful socket.io connect'); // let the final part of the path be the room name const room = location.pathname.substring(location.pathname.lastIndexOf('/')); // imediately join the room which will trigger the initializations sendAction('joinRoom', room); }); socket.on('disconnect', () => { blockUI( 'Serveur déconnecté. Veuillez rafraîchir la page pour essayer de vous reconnecter…', ); // $('.blockOverlay').on('click', $.unblockUI); }); socket.on('message', (data) => { getMessage(data); }); function unblockUI() { $.unblockUI({ fadeOut: 50 }); } function blockUI(message) { message = message || 'En attente…'; $.blockUI({ message, css: { border: 'none', padding: '15px', backgroundColor: '#000', '-webkit-border-radius': '10px', '-moz-border-radius': '10px', opacity: 0.5, color: '#fff', fontSize: '20px', }, fadeOut: 0, fadeIn: 10, }); } // respond to an action event function getMessage(m) { const message = m; // JSON.parse(m); const { action } = message; const { data } = message; // console.log('<-- ' + action); switch (action) { case 'roomAccept': // okay we're accepted, then request initialization // (this is a bit of unnessary back and forth but that's okay for now) sendAction('initializeMe', null); break; case 'roomDeny': // this doesn't happen yet break; case 'editBoardMetas': sendAction('editBoardMetas', { prop: 'opacity', value: 0.5 }); break; case 'moveCard': moveCard($(`#${data.id}`), data.position); break; case 'initCards': initCards(data); break; case 'createCard': // console.log(data); drawNewCard( data.id, data.text, data.x, data.y, data.rot, data.colour, null, ); break; case 'deleteCard': $(`#${data.id}`).fadeOut(500, function () { $(this).remove(); }); break; case 'editCard': $(`#${data.id}`).children('.content:first').attr('data-text', data.value); $(`#${data.id}`).children('.content:first').html(marked(data.value)); break; case 'initColumns': initColumns(data); break; case 'updateColumns': initColumns(data); break; case 'changeTheme': changeThemeTo(data); break; case 'join-announce': displayUserJoined(data.sid, data.user_name); break; case 'leave-announce': displayUserLeft(data.sid); break; case 'initialUsers': displayInitialUsers(data); break; case 'nameChangeAnnounce': updateName(message.data.sid, message.data.user_name); break; case 'addSticker': addSticker(message.data.cardId, message.data.stickerId); break; case 'setBoardSize': resizeBoard(message.data); break; case 'export': download(message.data.filename, message.data.text); break; case 'addRevision': addRevision(message.data); break; case 'deleteRevision': $(`#revision-${message.data}`).remove(); break; case 'initRevisions': $('#revisions-list').empty(); for (let i = 0; i < message.data.length; i++) { addRevision(message.data[i]); } break; default: // unknown message alert(`action inconnue : ${JSON.stringify(message)}`); break; } } $(document).on('keyup', (event) => { keyTrap = event.which; }); function drawNewCard( id, text, x, y, rot, colour, sticker, animationspeed, mx = 0, my = 0, ) { // cards[id] = {id: id, text: text, x: x, y: y, rot: rot, colour: colour}; const h = `
\ \
${marked( text, )}
\
`; const card = $(h); card.appendTo('#board'); $(`#${id}`).children('.content:first').attr('data-text', text); // @TODO // Draggable has a bug which prevents blur event // http://bugs.jqueryui.com/ticket/4261 // So we have to blur all the cards and editable areas when // we click on a card // The following doesn't work so we will do the bug // fix recommended in the above bug report // card.on('click', function() { // $(this).focus(); // } ); card.draggable({ snap: false, snapTolerance: 5, containment: [0, 0, 2000, 2000], stack: '.card', start(event, ui) { keyTrap = null; }, drag(event, ui) { if (keyTrap == 27) { ui.helper.css(ui.originalPosition); return false; } }, handle: 'div.content', }); // After a drag: card.on('dragstop', function (event, ui) { if (keyTrap == 27) { keyTrap = null; return; } const data = { id: this.id, position: ui.position, oldposition: ui.originalPosition, }; sendAction('moveCard', data); }); card.children('.droppable').droppable({ accept: '.sticker', drop(event, ui) { const stickerId = ui.draggable.attr('id'); const cardId = $(this).parent().attr('id'); addSticker(cardId, stickerId); const data = { cardId, stickerId, }; sendAction('addSticker', data); // remove hover state to everything on the board to prevent // a jquery bug where it gets left around $('.card-hover-draggable').removeClass('card-hover-draggable'); }, hoverClass: 'card-hover-draggable', }); let speed = Math.floor(Math.random() * 1000); if (typeof animationspeed !== 'undefined') speed = animationspeed; if (mx == 0 && my == 0) { const startPosition = $('#create-card').position(); mx = startPosition.left; my = startPosition.top; } card.css('top', my); card.css('left', mx); card.animate( { left: `${x}px`, top: `${y}px`, }, speed, ); card.children('.delete-card-icon').on('click', () => { $(`#${id}`).remove(); // notify server of delete sendAction('deleteCard', { id }); }); card.children('.content').editable( (value, settings) => { $(`#${id}`).children('.content:first').attr('data-text', value); onCardChange(id, value); return marked(value); }, { type: 'textarea', data() { return $(`#${id}`).children('.content:first').attr('data-text'); }, submit: 'OK', style: 'inherit', cssclass: 'card-edit-form', placeholder: 'Double cliquez pour m’éditer', onblur: 'submit', event: 'dblclick', // event: 'mouseover' }, ); // add applicable sticker if (sticker !== null) addSticker(id, sticker); } function onCardChange(id, text) { sendAction('editCard', { id, value: text, }); } function moveCard(card, position) { card.animate( { left: `${position.left}px`, top: `${position.top}px`, }, 500, ); } function addSticker(cardId, stickerId) { stickerContainer = $(`#${cardId} .filler`); if (stickerId === 'nosticker') { stickerContainer.html(''); return; } if (Array.isArray(stickerId)) { for (const i in stickerId) { stickerContainer.prepend( ``, ); } } else if (stickerContainer.html().indexOf(stickerId) < 0) { stickerContainer.prepend(``); } } //---------------------------------- // cards //---------------------------------- function createCard(id, text, x, y, rot, colour, mx = 0, my = 0) { drawNewCard(id, text, x, y, rot, colour, null, null, mx, my); const action = 'createCard'; const data = { id, text, x, y, rot, colour, }; sendAction(action, data); } function randomCardColour() { const colours = ['yellow', 'green', 'blue', 'white']; const i = Math.floor(Math.random() * colours.length); return colours[i]; } function initCards(cardArray) { // first delete any cards that exist $('.card').remove(); cards = cardArray; for (const i in cardArray) { card = cardArray[i]; drawNewCard( card.id, card.text, card.x, card.y, card.rot, card.colour, card.sticker, 0, ); } boardInitialized = true; unblockUI(); } //---------------------------------- // cols //---------------------------------- function drawNewColumn(columnName) { let cls = 'col'; if (totalcolumns === 0) { cls = 'col first'; } $('#icon-col').before( `

${columnName}

`, ); $('.editable').editable( function (value, settings) { onColumnChange(this.id, value); return value; }, { style: 'inherit', cssclass: 'card-edit-form', type: 'textarea', placeholder: 'Nouveau', onblur: 'submit', width: '', height: '', xindicator: '', event: 'dblclick', // event: 'mouseover' }, ); $('.col:last').fadeIn(500); totalcolumns++; } function onColumnChange(id, text) { const names = []; // console.log(id + " " + text ); // Get the names of all the columns right from the DOM $('.col').each(function () { // get ID of current column we are traversing over const thisID = $(this).children('h2').attr('id'); if (id == thisID) { names.push(text); } else { names.push($(this).text()); } }); updateColumns(names); } function displayRemoveColumn() { if (totalcolumns <= 0) return false; $('.col:last').fadeOut(150, function () { $(this).remove(); }); totalcolumns--; } function createColumn(name) { if (totalcolumns >= 8) return false; drawNewColumn(name); columns.push(name); const action = 'updateColumns'; const data = columns; sendAction(action, data); } function deleteColumn() { if (totalcolumns <= 0) return false; displayRemoveColumn(); columns.pop(); const action = 'updateColumns'; const data = columns; sendAction(action, data); } function updateColumns(c) { columns = c; const action = 'updateColumns'; const data = columns; sendAction(action, data); } function deleteColumns(next) { // delete all existing columns: $('.col').fadeOut('slow', next()); } function initColumns(columnArray) { totalcolumns = 0; columns = columnArray; $('.col').remove(); for (const i in columnArray) { column = columnArray[i]; drawNewColumn(column); } } function changeThemeTo(theme) { currentTheme = theme; if (theme == 'bigcards') { $('#board').removeClass('smallcards'); } else { $('#board').removeClass('bigcards'); } $('#board').addClass(theme); } /// /////////////////////////////////////////////////////// /// /////// NAMES STUFF /////////////////////////////////// /// /////////////////////////////////////////////////////// function setCookie(c_name, value, exdays) { const exdate = new Date(); exdate.setDate(exdate.getDate() + exdays); const c_value = `${escape(value) + (exdays === null ? '' : `; expires=${exdate.toUTCString()}`)};SameSite=Strict`; document.cookie = `${c_name}=${c_value}`; } function getCookie(c_name) { let i; let x; let y; const ARRcookies = document.cookie.split(';'); for (i = 0; i < ARRcookies.length; i++) { x = ARRcookies[i].substr(0, ARRcookies[i].indexOf('=')); y = ARRcookies[i].substr(ARRcookies[i].indexOf('=') + 1); x = x.replace(/^\s+|\s+$/g, ''); if (x == c_name) { return unescape(y); } } } function setName(name) { sendAction('setUserName', name); setCookie('scrumscrum-username', name, 365); } function displayInitialUsers(users) { for (const i in users) { // console.log(users); displayUserJoined(users[i].sid, users[i].user_name); } } function displayUserJoined(sid, user_name) { name = ''; if (user_name) name = user_name; else name = sid.substring(0, 5); $('#names-ul').append(`
  • ${name}
  • `); } function displayUserLeft(sid) { name = ''; if (name) name = user_name; else name = sid; const id = `#user-${sid.toString()}`; $('#names-ul') .children(id) .fadeOut(1000, function () { $(this).remove(); }); } function updateName(sid, name) { const id = `#user-${sid.toString()}`; $('#names-ul').children(id).text(name); } /// /////////////////////////////////////////////////////// /// /////////////////////////////////////////////////////// function boardResizeHappened(event, ui) { const newsize = ui.size; sendAction('setBoardSize', newsize); } function resizeBoard(size) { $('.board-outline').animate({ height: size.height, width: size.width, }); } /// /////////////////////////////////////////////////////// /// /////////////////////////////////////////////////////// function calcCardOffset() { const offsets = {}; $('.card').each(function () { const card = $(this); $('.col').each(function (i) { const col = $(this); if ( col.offset().left + col.outerWidth() > card.offset().left + card.outerWidth() || i === $('.col').length - 1 ) { offsets[card.attr('id')] = { col, x: (card.offset().left - col.offset().left) / col.outerWidth(), }; return false; } }); }); return offsets; } // moves cards with a resize of the Board // doSync is false if you don't want to synchronize // with all the other users who are in this room function adjustCard(offsets, doSync) { $('.card').each(function () { const card = $(this); const offset = offsets[this.id]; if (offset) { const data = { id: this.id, position: { left: offset.col.position().left + offset.x * offset.col.outerWidth(), top: parseInt(card.css('top').slice(0, -2)), }, oldposition: { left: parseInt(card.css('left').slice(0, -2)), top: parseInt(card.css('top').slice(0, -2)), }, }; // use .css() instead of .position() because css' rotate // console.log(data); if (!doSync) { card.css('left', data.position.left); card.css('top', data.position.top); } else { // note that in this case, data.oldposition isn't accurate since // many moves have happened since the last sync // but that's okay becuase oldPosition isn't used right now moveCard(card, data.position); sendAction('moveCard', data); } } }); } /// /////////////////////////////////////////////////////// /// /////////////////////////////////////////////////////// function download(filename, text) { const element = document.createElement('a'); let mime = 'text/plain'; if (filename.match(/.csv$/)) { mime = 'text/csv'; } element.setAttribute( 'href', `data:${mime};charset=utf-8,${encodeURIComponent(text)}`, ); element.setAttribute('download', filename); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } function addRevision(timestamp) { const li = $(`
  • `); const s1 = $(''); const s2 = $( 'delete revision', ); if (typeof timestamp === 'string') { timestamp = parseInt(timestamp); } s1.text(moment(timestamp).format('LLLL')); li.append(s1); li.append(s2); $('#revisions-list').append(li); // $('body').on("click", s1, function () { // socket.json.send({ // action: "exportRevision", // data: timestamp, // }) // }) // $('body').on("click", s2, function () { // socket.json.send({ // action: "deleteRevision", // data: timestamp, // }) // }) } /// /////////////////////////////////////////////////////// /// /////////////////////////////////////////////////////// $(() => { // disable image dragging // window.ondragstart = function() { return false; }; if (boardInitialized === false) blockUI(''); // setTimeout($.unblockUI, 2000); $('.add-post-it').on('click', function (e) { const rotation = Math.random() * 10 - 5; // add a bit of random rotation (+/- 10deg) const cardLeft = 150 + Math.random() * 400; const cardTop = 20 + Math.random() * 50; const uniqueID = Math.round(Math.random() * 99999999); // is this big enough to assure uniqueness? console.log(e.clientX, e.clientY); createCard( `card${uniqueID}`, '', cardLeft, cardTop, rotation, $(this).data('color'), e.clientX, e.clientY, ); }); // Style changer $('#smallify').on('click', () => { if (currentTheme == 'bigcards') { changeThemeTo('smallcards'); } else if (currentTheme == 'smallcards') { changeThemeTo('bigcards'); } sendAction('changeTheme', currentTheme); return false; }); $('#icon-col').on( 'hover', () => { $('.col-icon').fadeIn(10); }, () => { $('.col-icon').fadeOut(150); }, ); $('#add-col').on('click', () => { createColumn('Nouvelle colonne'); return false; }); $('#delete-col').on('click', () => { deleteColumn(); return false; }); const user_name = getCookie('scrumscrum-username'); $('#yourname-input').on('focus', function () { if ($(this).val() == 'anonyme') { $(this).val(''); } $(this).addClass('focused'); }); $('#yourname-input').on('blur', function () { if ($(this).val() === '') { $(this).val('anonyme'); } $(this).removeClass('focused'); setName($(this).val()); }); $('#yourname-input').val(user_name); $('#yourname-input').trigger('blur'); $('#yourname-li').hide(); $('#yourname-input').on('keypress', function (e) { code = e.keyCode ? e.keyCode : e.which; if (code == 10 || code == 13) { $(this).trigger('blur'); } }); $('.sticker').draggable({ revert: true, zIndex: 1000, }); $('.board-outline').resizable({ ghost: false, minWidth: 640, minHeight: 480, maxWidth: 1140, maxHeight: 855, }); // A new scope for precalculating (function () { let offsets; $('.board-outline').on('resizestart', () => { offsets = calcCardOffset(); }); $('.board-outline').on('resize', (event, ui) => { adjustCard(offsets, false); }); $('.board-outline').on('resizestop', (event, ui) => { boardResizeHappened(event, ui); adjustCard(offsets, true); }); })(); $('#marker').draggable({ axis: 'x', containment: 'parent', }); $('#eraser').draggable({ axis: 'x', containment: 'parent', }); $('#export-txt').on('click', () => { socket.json.send({ action: 'exportTxt', data: $('.col').length !== 0 ? $('.col').css('width').replace('px', '') : null, }); }); $('#export-csv').on('click', () => { socket.json.send({ action: 'exportCsv', data: $('.col').length !== 0 ? $('.col').css('width').replace('px', '') : null, }); }); $('#export-json').on('click', () => { socket.json.send({ action: 'exportJson', data: { width: $('.board-outline').css('width').replace('px', ''), height: $('.board-outline').css('height').replace('px', ''), }, }); }); $('#import-file').on('click', (evt) => { evt.stopPropagation(); evt.preventDefault(); const f = $('#import-input').get(0).files[0]; const fr = new FileReader(); fr.onloadend = function () { const text = fr.result; socket.json.send({ action: 'importJson', data: JSON.parse(text), }); }; fr.readAsText(f); }); $('#create-revision').on('click', () => { socket.json.send({ action: 'createRevision', data: { width: $('.board-outline').css('width').replace('px', ''), height: $('.board-outline').css('height').replace('px', ''), }, }); }); }); /** Doubleclick on mobile + Layout Framemo with tabs * */ $(document).ready(() => { if ( window.location.href != `${window.location.protocol}//${window.location.host}/` ) { // Not on homepage /** Double click on mobile interface * */ let clickTimer = null; let clickTarget = null; let editTarget = null; function doubletapCards(selector) { $(`${selector} .stickertarget`).addClass('doubletap'); // Escape multi bound $(`${selector} .doubletap`).on('click', () => { clickTarget = selector.replace('#', ''); if (clickTimer == null) { clickTimer = setTimeout(() => { clickTimer = null; }, 1000); } else { // console.log('doubleclick : '+clickTimer+':'+editTarget); clearTimeout(clickTimer); clickTimer = null; if ( editTarget == clickTarget && clickTarget !== undefined && clickTarget !== null ) { $(`#${clickTarget.replace('content:', '')} .doubletap`).trigger( 'dblclick', ); } } editTarget = clickTarget; }); } function doubletapTitle(selector) { $(selector).addClass('doubletap'); // Escape multi bound $(`${selector}.doubletap`).on('click', () => { clickTarget = selector.replace('#', ''); if (clickTimer == null) { clickTimer = setTimeout(() => { clickTimer = null; }, 1000); } else { // console.log('doubleclick : '+clickTimer+':'+editTarget); clearTimeout(clickTimer); clickTimer = null; if ( editTarget == clickTarget && clickTarget !== undefined && clickTarget !== null ) { $(`#${clickTarget}.doubletap`).trigger('dblclick'); } } editTarget = clickTarget; }); } setInterval(() => { // Add periodically the doubletap event on new cards $('.stickertarget:not(.doubletap)').each(function () { doubletapCards(`#${$(this).attr('id').replace('content:', '')}`); }); $('#board-table .col h2:not(.doubletap)').each(function () { doubletapTitle(`#${$(this).attr('id')}`); }); }, 500); /** Layout Framemo - Tabs * */ // Defaut board real size (not 'auto' or 'inherit') saved in database // in order to be able to center it var boardReady = setInterval(() => { if (boardInitialized) { // when board is ready if ($('.board-outline').attr('style') === undefined) { // check if size is imported from db $('.board-outline').css({ width: `${$('.board-outline.ui-resizable').width() + 16}px`, height: '466px', }); const data = {}; data.size = { height: 466, width: $('.board-outline.ui-resizable').width() + 16, }; boardResizeHappened('resizestop', data); // using scrumblr function that keep size in db after a resize } clearInterval(boardReady); } }, 500); // $("#scrumblr") // .append($(".names, .stickers, .buttons")) // .after( // '
    ' + // '
    ' + // '
    ' + // '
    ' // ) // $("#export-import").append($(".export, .import")) // $("#share").append($(".share")) // $("#revisions").append($(".revisions")) // $("#about").append($("#tuto-faq, #le-logiciel, #jardin")) // Style $('#smallify').on('click', function () { if (currentTheme == 'bigcards') { $(this) .children('i') .removeClass('fa-search-plus') .addClass('fa-search-minus'); } else { $(this) .children('i') .removeClass('fa-search-minus') .addClass('fa-search-plus'); } }); $('#full-page').on('click', function () { if ($(this).children('i').hasClass('fa-expand')) { $(this).children('i').removeClass('fa-expand').addClass('fa-compress'); $('#header-bar').hide(); } else { $(this).children('i').removeClass('fa-compress').addClass('fa-expand'); $('#header-bar').show(); } toggleFullScreen(); }); /** Mode iframe * */ if (top.location != self.document.location) { $('#header-bar').hide(); } // put URL in share input const mainurl = location.toString().split('#')[0]; $('.replace-url').val(mainurl); $('.share-iframe').text( $('.share-iframe').text().replace('{{replace-url}}', mainurl), ); // copy URL to clipboard $('#copyurl').on('click', (e) => { e.preventDefault(); const node = document.getElementById('taburl'); node.disabled = null; node.select(); const success = document.execCommand('copy'); if (success) { getSelection().removeAllRanges(); node.disabled = 'disabled'; alert('URL du tableau copiée dans votre presse-papier !'); } else { alert( "Impossible de copier l'URL du tableau dans votre presse-papier. Veuillez copier son adresse manuellement (Ctrl+C).", ); } }); } }); function go() { let { value } = document.forms[0].elements.name; value = value.replace(/[\/\?&#]/g, ''); window.location.href = value; return false; } $(() => { const headerBarUrl = $('#header-bar').data('url'); if (headerBarUrl) { const getJSON = function (url, callback) { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'json'; xhr.onload = function () { const { status } = xhr; if (status === 200) { callback(null, xhr.response); } else { callback(status, xhr.response); } }; xhr.send(); }; getJSON(headerBarUrl, (err, data) => { if (err !== null) { console.log(`Something went wrong: ${err}`); } else { document.getElementById('header-bar').innerHTML = data.markup; const styleElement = document.createElement('style'); styleElement.innerHTML = data.style; document.getElementById('header-bar').appendChild(styleElement); } }); } }); $(() => { // check if hash used to show informations if (window.location.hash == '#settings' || window.location.hash == '#share') { toggleNav(window.location.hash); } // Toggle Nav on Click $('.toggle-nav').on('click', function () { let target = $(this).attr('href'); if ( target === '#' || ($('#site-wrapper').hasClass('show-nav') && target == window.location.hash) ) { target = false; history.replaceState('', '', '#'); } else { history.replaceState('', '', target); } toggleNav(target); return false; }); // When nav opened, a click on the canvas hides the menu $('body').on( 'click', '.show-nav #site-canvas main, .show-nav .main-header', (e) => { history.replaceState('', '', '#'); toggleNav(false); return false; }, ); $('.backgrounds .bg').on('click', function () { if ($(this).hasClass('selected')) { $('body').css('background-image', 'none'); $(this).removeClass('selected'); } else { $('.selected').removeClass('selected'); $('.bgurl').val(''); $('body').css('background-image', `url("/${$(this).attr('src')}")`); $(this).addClass('selected'); } }); $('.bgurl').on('change', function () { const url = $(this).val(); if (url) { $('.selected').removeClass('selected'); $('body').css('background-image', `url("${url}")`); } }); }); function toggleNav(target) { if ($('#site-wrapper').hasClass('show-nav') && target === false) { $('#site-wrapper').removeClass('show-nav'); } else { $('#share, #settings').hide(); if (target !== false) { $(target).show(); } $('#site-wrapper').addClass('show-nav'); } return false; }