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` }) 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.json.send(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 }