(function ($, Drupal, once) { var _boussole; Drupal.behaviors.boussole = { attach: function (context, settings) { console.log("Drupal Boussole behavior", context); let $boussole_wrapper = document.querySelector('#boussole-layout'); once('boussole-class-behaviour', '#boussole-layout', context).forEach(element => { let tabs = // div.field-group-tabs-wrapper // input.horizontal-tabs-active-tab _boussole = new Boussole($boussole_wrapper); }); let $form = $boussole_wrapper.querySelector(':scope>.form-item'); console.log($form); let $tabledrag = $form.querySelector('table.field-multiple-table'); console.log($tabledrag); let $form_entities = $tabledrag.querySelectorAll(':scope>tbody>tr>td:not(.field-multiple-drag):not(.delta-order)>div'); console.log($form_entities); let entities = {}; [...$form_entities].forEach($entity => { console.log($entity); let ajax_new_content = $entity.classList.contains('ajax-new-content'); if(ajax_new_content){ // its a new paragraph just added by ajax $entity = $entity.querySelector(':scope>div'); } let id = $entity.getAttribute('id'); let $af = $entity.querySelector('.paragraphs-subform .field--name-field-actuel-future'); let $p = $entity.querySelector('.paragraphs-subform .field--name-field-prise input'); let $mm = $entity.querySelector('.paragraphs-subform .field--name-field-menace-maintien-degres input'); let e = { wrapper:{ $dom: $entity, id: id }, title: { $dom: null, value: null }, af: { $dom: $af, value: parseInt($af.querySelector('input:checked').value) }, p: { $dom: $p, value: parseFloat($p.value) }, mm: { $dom: $mm, value: parseFloat($mm.value) } } // title does not exists when we add an "entité intégrée" as we also have to create the referenced node let $title = $entity.querySelector('.paragraphs-subform>.field--type-entity-reference.field--name-field-entite td.inline-entity-form-node-label'); if ($title) { e.title= { $dom: $title, value: $title.textContent }; } entities[id] = e; }); console.log(entities); _boussole.updateCreateEntities(entities); } }; class Boussole { constructor($boussole_wrapper) { this.$boussole_wrapper = $boussole_wrapper; this.$boussole = document.querySelector('#boussole-layout>.boussole-wrapper>.boussole'); this.entities = {} this.initTabChecking(); this.enableDragArea() } initTabChecking(){ // debugger this.$field_group_tab = this.$boussole_wrapper.closest('details.field-group-tab'); // watch for tabs opening and closing // hidden input do not triggers events // so we have to use mutationObserver tomake it trigger the change event this.$tabInput = this.$field_group_tab.parentElement.querySelector(':scope>input[type="hidden"]'); let observer = new MutationObserver(function(mutations, observer) { if(mutations[0].attributeName == "value") { // console.log('mutation observed', mutations, observer); let event = new Event('change'); mutations[0].target.dispatchEvent(event); } }); observer.observe(this.$tabInput, {attributes: true}); // then we listen to the event this.$tabInput.addEventListener('change', event => { // console.log('tab input changed', event, this); this.wrapperTabChanged(event); }); this.checkTabIsOpen(); } checkTabIsOpen(){ this.tabisopen = this.$field_group_tab.getAttribute('id') === this.$tabInput.value; } wrapperTabChanged(event){ this.checkTabIsOpen(); if(this.tabisopen){ this.updateEntities(); } } enableDragArea(){ this.$boussole.addEventListener("dragenter", (event) => { event.preventDefault(); }); this.$boussole.addEventListener("dragover", (event) => { event.preventDefault(); }); this.$boussole.addEventListener("drop", (event) => { event.preventDefault(); let data = JSON.parse(event.dataTransfer.getData('text/plain')); console.log('boussole drop', event, data); data.clientX = event.clientX; data.clientY = event.clientY; // return on the entity class this.entities[data.entity_id].dropedOnBoussole(data); }); } updateCreateEntities(entities){ // console.log('Class Boussole update entities', entities); for (const [id, values] of Object.entries(entities)) { console.log(id, typeof this.entities[id], values); if (typeof this.entities[id] === 'undefined') { this.entities[id] = new Entity(this.$boussole, id, values); }else{ this.entities[id].updateValuesFromForm(values); } } } updateEntities(){ for (const [id, entity] of Object.entries(this.entities)) { entity.updatePos(); } } } class Entity { constructor($container, id, values){ console.log('Entity constructor', $container, id, values); this.$container = $container; this.updateSceneSize(); this.id = id; this.values = values; this.listenToFormValue(); this.createDomElemt(); } updateSceneSize(){ this.sceneSize = { w: this.$container.clientWidth, h: this.$container.clientHeight }; } createDomElemt(){ this.$e = document.createElement('div'); this.$e.classList.add('entity'); this.$e.setAttribute('id', `${this.id}-boussole-point`); if (this.values.title.value) { // the paragraph as an entity referenced this.$e.setAttribute('title', this.values.title.value); }else{ // new paragraph with no entity referenced this.$e.classList.add('ajax-new'); } this.$container.append(this.$e); console.log(this.$e); this.updatePos(); this.initDraggable() } updateDomElemt(){ if (this.values.title.value) { // the paragraph as an entity referenced this.$e.setAttribute('title', this.values.title.value); this.$e.classList.remove('ajax-new'); }else{ // new paragraph with no entity referenced this.$e.classList.add('ajax-new'); } } initDraggable(){ this.$e.setAttribute('draggable', true); this.$e.addEventListener('dragstart', event => { // console.log('dragstart', event); let data = { entity_id : this.id, dragging_start_x : event.target.offsetLeft - event.clientX, dragging_start_y : event.target.offsetTop - event.clientY }; event.dataTransfer.setData('text/plain', JSON.stringify(data)); // event.dataTransfer.effectAllowed = "move"; setTimeout(() => {event.target.classList.add('hide');}, 0); }); this.$e.addEventListener('dragend', event => { // console.log('dragend', event); event.target.classList.remove('hide'); }); // CAN'T DO THAT, EVENT IS FIRED FOR EACH ENTITIES // TAKING CARE OF THE DROP IN THE BOUSSOLE CLASS // this.$container.addEventListener('drop', event =>{ // event.preventDefault(); // console.log('drop', event); // }) } dropedOnBoussole(data){ this.$e.style.left = data.clientX + data.dragging_start_x + 'px'; this.$e.style.top = data.clientY + data.dragging_start_y + 'px'; this.updateFormValuesFromBoussole(); } updateFormValuesFromBoussole(values){ let compstyle = window.getComputedStyle(this.$e); // AF let top = parseInt(compstyle.top); let af = top >= this.sceneSize.h/2 ? 0 : 1; this.values.af.value = af; this.values.af.$dom.querySelector(`input[value="${af}"]`).checked = true; // (x, y) let x = parseInt(compstyle.left) - this.sceneSize.h/2; let y = Math.abs(this.sceneSize.h/2 - top); let a = parseFloat(Math.atan(x/y) * (180/Math.PI)).toFixed(2); let r = Math.sqrt(Math.pow(x,2)+Math.pow(y,2)); this.values.mm.value = this.values.mm.$dom.value = a; // https://stackoverflow.com/questions/5731863/mapping-a-numeric-range-onto-another // from range 0 -> rayon_de_la_boussole to range 0 -> 100 let r_max = this.sceneSize.w/2; let r_slope = (100 - 0) / (r_max - 0); this.values.p.value = this.values.p.$dom.value = 100 - parseFloat(0 + r_slope * (r - 0)).toFixed(2); // this.values.p.$dom.value = this.values.p.value; console.log('x:', x, 'r:', r, 'a:', a, "y:", y); } listenToFormValue(){ ['af','p','mm'].forEach(v =>{ this.values[v].$dom.addEventListener('change', function(v, event) { switch (v) { case 'af': this.values[v].value = parseInt(this.values[v].$dom.querySelector('input:checked').value); break; default: this.values[v].value = parseInt(this.values[v].$dom.value); break; } console.log(`new ${v}`, this.values[v].value); this.updatePos(); }.bind(this, v)); }) } updateValuesFromForm(values){ this.values = values; this.updateDomElemt(); } updatePos(){ console.log('update pos'); this.updateSceneSize() // converte degres to radian, inverse it and rotate it let a = parseInt((this.values.mm.value) * -1 + 90) * (Math.PI/180); // https://stackoverflow.com/questions/5731863/mapping-a-numeric-range-onto-another // from range 0 -> 100 to range 0 -> diagonale de la scene // let diagonale = Math.sqrt(Math.pow(this.sceneSize.w, 2) + Math.pow(this.sceneSize.h, 2))/2; // let r_max = diagonale; // actualy we don't use diagonale, we stay on the circle let r_max = this.sceneSize.w/2; let r_slope = (r_max - 0) / (100 - 0) // we inverse p as the more p you have the closest you are from the center let r = parseInt(0 + r_slope * ((100 - this.values.p.value) - 0)); let x = r * Math.cos(a); let y = r * Math.sin(a); // console.log('x:', x, 'r:', r, 'a:', a, "y:", y); let af = this.values.af.value; let left = this.sceneSize.w / 2 + x; let top = this.sceneSize.h / 2 - (af ? y : -y); // console.log('left:', left, "top:", top); this.$e.style.left = left+'px'; this.$e.style.top = top+'px'; } } })(jQuery, Drupal, once);