/* view/vfloater.js Jake Read at the Center for Bits and Atoms (c) Massachusetts Institute of Technology 2019 This work may be reproduced, modified, distributed, performed, and displayed for any purpose, but must acknowledge the squidworks and cuttlefish projects. Copyright is retained and must be preserved. The work is provided as is; no warranty is provided, and users accept all liability. */ import DomTools from './vdom.js' function Floater(view, grouptype) { // want one, // altho, non-ideal ... let dt = new DomTools(view) // TODO: /* ... think ... think ... -> button to expand, and gather? div-related? -> button drags as well ? titlematter does hover, not drag, two funcitons ? -> !!!!!!!!!!!!! button for fixed ! fixed !!!!!!!!!!!!!!! -> roll sim back, so that fixed / unfixed is meaningful -> explosions afterwards */ // a floater is an individual that can be dragged, fixed/unfixed, etc // it is exactly the thing we give to d3 to lay out this.x = 0 this.y = 0 // want these, this.w = 101 this.h = 101 // if it has fx, fy, it is fixed this.r = 101 // to default, // these are handy, this.bb = {} this.bb.x1 = -50 this.bb.y1 = -50 this.bb.x2 = 51 this.bb.y2 = 51 // by default, no this.isFixed = false // will have this to track within simulation links this.simIndex = null // every floater has *at least* this ... // this is where we put the buttonz this.ownElement = $(`<div>`).addClass('float').get(0) $(this.ownElement).append(`<i class="em em-pushpin"></i>`) $(this.ownElement).on('click', (evt) => { if (this.isFixed === true) { this.free() } else { this.fix() } }).on('mouseover', (evt) => { document.body.style.cursor = 'pointer' }).on('mouseout', (evt) => [ document.body.style.cursor = 'auto' ]) $(view.plane).append(this.ownElement) // fixed <i class="em em-round_pushpin"></i> // open <i class="em em-pushpin"></i> // this is a list of elements to carry about this.bag = [] // gets {element, x, y} // fuuu let writeTitleMatter = (div, def) => { let md = false div.onmouseover = () => { def.highlight() if (!md) document.body.style.cursor = 'grab' } div.onmouseout = () => { def.unhighlight() if (!md) document.body.style.cursor = 'auto' } div.onmousedown = (evt) => { md = true document.body.style.cursor = 'grabbing' // pin to current position, this.fix() evt.preventDefault() evt.stopPropagation() let domElemMouseMove = (evt) => { // TRANSFORMS here to move div about on drag evt.preventDefault() evt.stopPropagation() // need to find that top-level view's scale transform this.fx += evt.movementX / view.tls this.fy += evt.movementY / view.tls this.newFix() this.tick() } function rmOnMouseUp(evt) { md = false document.body.style.cursor = 'auto' document.removeEventListener('mousemove', domElemMouseMove) document.removeEventListener('mouseup', rmOnMouseUp) } document.addEventListener('mousemove', domElemMouseMove) document.addEventListener('mouseup', rmOnMouseUp) } } // -------------------------------------------- TAKE // take up new things this.typeset = [] this.take = (div, def, stall, resize) => { let type = '' // cover view-containing ... ?? // if ($(div).is('.native') && $(div).children().contains('.view')) let item = { type: '', de: div, def: def, offsetX: 0, offsetY: 0 } // always nice to do this first, // defs -> the dom $(view.plane).append(item.de) // only a few cases we will allow, if ($(div).is('.inputs')) { item.type = 'inputs' } else if ($(div).is('.outputs')) { item.type = 'outputs' } else if ($(div).is('.defcore')) { item.type = 'core' } else if ($(div).is('.nativewrap')) { item.type = 'native' // at the gitgo, set this to our current width... // this width thing is happening because you're rewriting the // so ... not a take, a swap ? !!! FFUUUUU // native element's wrapper ... this.calculateSizes() if (this.w < 200) { $(item.de).css('width', `200px`) } else { $(item.de).css('width', `${this.w}px`) } // and for non-spec'd heights, if (item.de.clientHeight < 10) { $(item.de).css('height', '200px') } // ok then, resizeable also dt.makeResizeable(item.de, this.onElementResize, resize) } else if ($(div).is('.defbutton')) { item.type = 'button' } else { // best guess, but.. item.type = 'core' console.error('no thx, no recognized type when taking div into floater') } // put it in the baaaag this.typeset.push(item.type) this.bag.push(item) // (also) write the drag content, if there's a header on board let header = $(div).children('.header').get(0) if (header) { writeTitleMatter(header, item.def) } // just want to do this once, if (stall !== true) this.onChange() } // end take this.fitNativeToFloater = () => { this.calculateSizes(true) // find it, let nt = this.bag.find((cnd) => { return cnd.type === 'native' }) if (!nt) { console.error('not finding a native element to resize here') } else { $(nt.de).css('width', `${this.w}px`) // want also to reset the resizing handle ... // kind of messy call, but let w = nt.de.clientWidth let h = nt.de.clientHeight let handle = $(nt.de).children('.resizehandle').get(0) dt.writeTransform(handle, { x: w, y: h }) } } // to catch, this.onElementResize = () => { // this is a lot, this.onChange() // our parent, view.kick() } // GROUP TYPES: // 'edges' ... (core) with (outputs) and .edges || (inputs) // 'unwrapped' ... (core) with (outputs) and .unwrapped || (inputs) // 'wrapped' ... (core) with (inputs) and (outputs) and (native .view) and .wrapped // 'std' ... (core) with some set of (inputs) and (outputs) and (native) this.makeEdges = () => { this.grouptype === 'edges' this.onChange() } // -------------------------------------------- CHANGE this.grouptype = grouptype // don't want to do this all the time // to 'open' ... put core with outputs, // to 'unwrap' ... put core with inputs, it will go below this.onChange = () => { // find types, let core = this.bag.find((cnd) => { return cnd.type === 'core' }) let inputs = this.bag.find((cnd) => { return cnd.type === 'inputs' }) let outputs = this.bag.find((cnd) => { return cnd.type === 'outputs' }) let native = this.bag.find((cnd) => { return cnd.type === 'native' }) // possible to have many, let buttons = [] for (let item of this.bag) { if (item.type === 'button') buttons.push(item) } let spaces = 5 // TYPE CASES BY TYPESET //console.log('FLT onchange, floater has', core ? '(core)' : '', inputs ? '(inputs)' : '', outputs ? '(outputs)' : '', native ? '(native)' : '') if (this.grouptype === 'std') { // 'std' ... (core) with some set of (inputs) and (outputs) and (native) core.offsetX = -core.de.clientWidth core.offsetY = 0 if (inputs) { inputs.offsetX = core.offsetX - inputs.de.clientWidth - spaces - 2 inputs.offsetY = 0 } if (outputs) { outputs.offsetX = spaces outputs.offsetY = 0 } if (native) { // calc sizes, ignoring native this.calculateSizes(true) native.offsetX = this.bb.x1 native.offsetY = this.bb.y2 + spaces // for resizing, if (outputs) { let gap = native.de.clientWidth - this.w if (gap > 1) { outputs.offsetX += gap } } } } else if (this.grouptype === 'unwrapped') { // 'unwrapped' ... (core) with (outputs) and .unwrapped || (inputs) if (core) { // unwrapped, left side, core, outputs, etc core.offsetX = -core.de.clientWidth core.offsetY = 0 if (outputs) { outputs.offsetX = spaces outputs.offsetY = 0 } if (native) { this.calculateSizes(true) native.offsetX = this.bb.x1 native.offsetY = this.bb.y2 + spaces if (outputs) { let gap = native.de.clientWidth - this.w if (gap > 1) { outputs.offsetX += gap } } } } else { // unwrapped, right side, it's the lonely inputs if (inputs) { inputs.offsetX = -inputs.de.clientWidth inputs.offsetY = 0 } } } else if (this.grouptype === 'wrapped') { // should have 'em all let's just wyld out // the view attached is king, let ntw = native.de.clientWidth let nth = native.de.clientHeight // left ... native.offsetX = -core.de.clientWidth native.offsetY = core.de.clientHeight + spaces // core core.offsetX = -core.de.clientWidth core.offsetY = 0 // inputs, well left inputs.offsetX = core.offsetX - inputs.de.clientWidth - spaces inputs.offsetY = $(inputs.de).children('.header').get(0).clientHeight + spaces + native.offsetY // ouputs, as is tradition outputs.offsetX = ntw + spaces + core.offsetX outputs.offsetY = inputs.offsetY } else if (this.grouptype === 'edges') { if(core && outputs){ this.edgetype = 'left' // and on top of self, core.offsetX = -core.de.clientWidth core.offsetY = -core.de.clientHeight - 25 outputs.offsetX = core.offsetX outputs.offsetY = core.offsetY - outputs.de.clientHeight - spaces } else { this.edgetype = 'right' inputs.offsetX = -inputs.de.clientWidth inputs.offsetY = -inputs.de.clientHeight - 25 // aaand view.msgbox.setTopMargin( - inputs.offsetY + 20) } } else { console.error(`this floater grouptype not written or recognized: '${this.grouptype}'`) } // now we can cover buttons, using core offsets // cover buttons, if(buttons.length){ if (buttons.length > 0) { if (!core) console.error('watch buttons not-on core?') let inc = 0 for (let btn of buttons) { btn.offsetX = core.offsetX + inc btn.offsetY = -20 - core.offsetY inc += 20 } } // ok, // now we can execute this, this.moveTo() // oy oy oy } // end onChange let writePosition = (element, x, y) => { $(element).css('left', `${x}px`).css('top', `${y}px`) } let readPosition = (element) => { let it = $(element) return { x: parseInt(it.css('left')), y: parseInt(it.css('top')) } } let readSize = (element) => { return { w: element.clientWidth, h: element.clientHeight } } this.moveTo = (x, y, backdoor) => { // but still probably want to check our offsets, if (this.isFixed && !backdoor) { this.x = this.fx this.y = this.fy } // to just draw new position, run with no args if (x === undefined) { x = this.x y = this.y } else { this.x = x this.y = y } // will return this ... let desire = { x: 0, y: 0 } // check bounds, if(!view.isTopLevel){ // bummer to do so many times ! let bounds = view.getCurrentBounds() this.calculateSizes() // if(this.grouptype === 'edges'){ //console.log('bounds', bounds) // x1, y1, x2, y2, w, h if(this.edgetype === 'left'){ this.x = - this.bb.x1 this.y = - this.bb.y1 + 17 + 5 } else { this.x = bounds.x2 - this.bb.x2 - 2 this.y = bounds.y1 - this.bb.y1 + 17 + 5 } } else { //console.log('place bounds', this.bb) //console.log('bounds', bounds) // x needs to contend with offset, // this.bb.x1 probably -ve, // this.bb.y1 probably -ve, // want to track, but won't push things left or up, this.x = Math.max(bounds.x1 - this.bb.x1, this.x) // lower bounds this.y = Math.max(bounds.y1 - this.bb.y1, this.y) // want-right term, let rbnd = bounds.x2 - this.bb.x2 if(this.x > rbnd){ desire.x = this.x - rbnd this.x = rbnd // wants to go right } // want-down term, let lbnd = bounds.y2 - this.bb.y2 if(this.y > lbnd){ desire.y = this.y - lbnd this.y = lbnd // wants to go down } } } // do work writePosition(this.ownElement, x - 16.5, y - 20) for (let item of this.bag) { writePosition(item.de, this.x + item.offsetX, this.y + item.offsetY) } // aaaand tell em what we waaaaant return desire } this.newFix = () => { this.moveTo(this.fx, this.fy, true) } // as is tradition, we'll keep things local, // and just make sure we re-calculate during relevant events... ? this.calculateSizes = (nonNative) => { // from core of xy, values like // // * -------------- (y1) -------- * // | | | // |(x1) --------- (x,y) ---- (x2)| // | | | // | | | // * ------------ (y2) ---------- * let x1 = 0 let y1 = 0 let x2 = 0 let y2 = 0 for (let item of this.bag) { // items are str8 cut dom elements, if (nonNative && item.type === 'native') continue let wt = readSize(item.de) // we know item offsets, so if (item.offsetX < x1) x1 = item.offsetX if (item.offsetY < y1) y1 = item.offsetY if (item.offsetX + wt.w > x2) x2 = item.offsetX + wt.w if (item.offsetY + wt.h > y2) y2 = item.offsetY + wt.h // corners } this.bb.x1 = x1 this.bb.y1 = y1 this.bb.x2 = x2 this.bb.y2 = y2 // ok, this.w = x2 - x1 this.h = y2 - y1 // also, this.r = Math.max(this.w, this.h) / 2 // check, let aspect = this.w / this.h if (aspect > 8 || aspect < 0.125) { //console.error(`warning: deviously large aspect for 'circular' collisions: ${aspect}, logging floater`, this) } } this.tick = () => { // will use to bother the simulation view.tick() } this.free = () => { this.x = this.fx this.y = this.fy delete this.fx delete this.fy this.isFixed = false $(this.ownElement).html(`<i class="em em-pushpin"></i>`) this.tick() } this.fix = () => { $(this.ownElement).html(`<i class="em em-round_pushpin"></i>`) this.isFixed = true this.fx = this.x this.fy = this.y this.moveTo(this.fx, this.fy, true) } this.fixTo = (x, y) => { this.x = x this.y = y this.fix() } // RIP this.cleanup = () => { // js deletes things that are unreachable, so // there are a few objects that are on the canvas we want to get rid of $(this.ownElement).remove() // also, any handle added to a native element // not being able to search by js subset is kind of a bummer here, but let native = this.bag.find((cnd) => { return cnd.type === 'native' }) if (native) { $(native.de).children('.resizehandle').remove() } } // } export default Floater