## html
<div class="node" style="
	transform: translate({{ x }}px, {{ y }}px);
	height: {{ data.height }}px;
	width: {{ data.width }}px;
	{{ data.color ? ('background: ' + data.color + '6e;') : '' }}
	color: {{ data.fontColor ? data.fontColor : 'black' }};
">
  	<div class="header"
	  	style="{{ data.color ? ('background: ' + data.color + '6e;') : '' }}"
		@dblclick="doubleClick()"
		@contextmenu.stopAll.prevent="nodeMenu(event)"
		@dragmove="moveNode(event)"
		@pointerdown="swapZIndex(event)"
		@pointerover="nodeHovered(event)"
	  	@pointerout="nodeUnhovered(event)"
	>{{ data.title }}</div>

	<div class="sides">
		<div @pointerdown="resize('n')" class="n"></div>
		<div @pointerdown="resize('w')" class="w"></div>
		<div @pointerdown="resize('s')" class="s"></div>
		<div @pointerdown="resize('e')" class="e"></div>
		<div @pointerdown="resize('nw')" class="nw"></div>
		<div @pointerdown="resize('ne')" class="ne"></div>
		<div @pointerdown="resize('se')" class="se"></div>
		<div @pointerdown="resize('sw')" class="sw"></div>
	</div>

	<div class="text-content">{{ data.textContent }}</div>
	<sf-template path="Blackprint/nodes/template/other.sf"></sf-template>
</div>

## scss-global
// Element name based on current path, BPIC/Decoration/Group/Default.sf
sf-space[blackprint] .nodes bpic-decoration-group-default .node {
	background: #b8b8ff6e;
	pointer-events: none;
	&.highlighted {
		box-shadow: 0 0 20px 3px #fbff00;
		outline: 2px solid yellow;
	}
	.text-content{
		padding: 5px;
		font-size: 12px;
		white-space: pre-wrap;
		overflow: hidden;
		max-width: 100%;
		max-height: 100%;
	}
	.header{
		background: #b8b8ff91;
		font-weight: bold;
		font-size: 14px;
		pointer-events: all;
		white-space: pre-line;
	}
	.other{
		color: white;
	}

	.sides {
		pointer-events: all;
		div{ position: absolute; opacity: 0.01;}

		// n= north, s= south, w= west, e= east
		.nw{cursor: nw-resize; top: 0; left: 0; width: 5px; height: 5px;}
		.ne{cursor: ne-resize; top: 0; right: 0; width: 5px; height: 5px;}
		.se{cursor: se-resize; bottom: 0; right: 0; width: 5px; height: 5px;}
		.sw{cursor: sw-resize; bottom: 0; left: 0; width: 5px; height: 5px;}

		.n{cursor: n-resize; top: 0; left: 0; margin-left: 5px; width: calc(100% - 10px); height: 5px;}
		.w{cursor: w-resize; top: 0; left: 0; margin-top: 5px; width: 5px; height: calc(100% - 10px);}
		.s{cursor: s-resize; bottom: 0; left: 0; margin-left: 5px; width: calc(100% - 10px); height: 5px;}
		.e{cursor: e-resize; top: 0; right: 0; margin-top: 5px; width: 5px; height: calc(100% - 10px);}
	}
}


## js-global
Blackprint.Sketch.registerInterface('BPIC/Decoration/Group/Default',
class extends Blackprint.Interface{
	constructor(node){
		super(node);

		this._groups = []; // Other decoration group references
		this._ifaces = []; // Object references
		this._cables = []; // Object references
		this.data = new DecorationGroupDefaultData(this);

		this._listenSelection = ev => {
			// ToDo
		}
	}

	init(){
		this.node.instance.on('container.selection', this._listenSelection);
	}

	exportData(){
		return {
			width: Math.round(this.data.width),
			height: Math.round(this.data.height),
			title: this.data.title,
			textContent: this.data.textContent,
			color: this.data.color,
			fontColor: this.data.fontColor,
		};
	}

	resize(compass){
		let containerScale = this.$space('container').scale;

		const onMove = ev => {
			let mX = ev.movementX / containerScale;
			let mY = ev.movementY / containerScale;

			if(compass === 'n'){
				this.y += mY;
				this.data.height -= mY;
			}
			else if(compass === 's') this.data.height += mY;
			else if(compass === 'w'){
				this.x += mX;
				this.data.width -= mX;
			}
			else if(compass === 'e') this.data.width += mX;

			if(compass === 'nw'){
				this.x += mX;
				this.y += mY;
				this.data.width -= mX;
				this.data.height -= mY;
			}
			else if(compass === 'ne'){
				this.y += mY;
				this.data.width += mX;
				this.data.height -= mY;
			}
			else if(compass === 'sw'){
				this.x += mX;
				this.data.width -= mX;
				this.data.height += mY;
			}
			else if(compass === 'se'){
				this.data.width += mX;
				this.data.height += mY;
			}

			if(this.data.width < 100) this.data.width = 100;
			if(this.data.height < 100) this.data.height = 100;
		}

		let temp = $(window)
			.once('pointerup', ev => {
				temp.off('pointermove', onMove);
				this.refreshContent(ev);
			})
			.on('pointermove', onMove);
	}

	doubleClick(){
		// Force open properties panel and focus to edit panel's title
		this.$space.sketch.emit('editor.properties.open');
		setTimeout(() => {
			$('bppc-decoration-group-default .title textarea').focus();
		}, 50);
	}

	// Disable selection for this node
	onSelect(){ return false; }

	// Refresh content when pointerup
	moveNode(ev){
		super.moveNode(ev);
		if(ev.ctrlKey) return;

		let { _ifaces, _cables } = this;

		for (var i = 0; i < _cables.length; i++)
			_cables[i].moveCableHead(ev, true);

		for (var i = 0; i < _ifaces.length; i++){
			let temp = _ifaces[i];

			// Avoid maximum call stack
			// if(temp.namespace === "Decoration/Group/Default") continue;

			temp.moveNode(ev, true);
		}
	}

	refreshContent(ev, _isSync){
		let { ifaceList } = this.node.instance;
		let modelIfaceList = this.$space('nodes').list;
		let cableList = this.$space('cables').list;

		// s = start, e = end; (X, Y position)
		let sx = this.x;
		let sy = this.y;
		let ex = this.x + this.data.width;
		let ey = this.y + this.data.height;

		let { _groups, _ifaces, _cables } = this;
		_groups.length = _ifaces.length = _cables.length = 0;

		for (var i = 0, n = cableList.length; i < n; i++) {
			let temp = cableList[i];
			if(!(temp.hasBranch !== false || !temp.connected)) continue;

			let [x, y] = temp.head2;
			if(x >= sx && x <= ex
			&& y >= sy && y <= ey){
				_cables.push(temp);
			}
		}

		let lowestIndex = -1;
		for (var i = 0, n = ifaceList.length; i < n; i++) {
			let temp = ifaceList[i];

			// Skip this node
			if(temp === this) continue;

			let {x, y} = temp;
			let elChild = (_isSync ? temp.$el[0] : sf.Window.source(temp.$el, ev)).firstElementChild;
			let {offsetWidth, offsetHeight} = elChild;

			let ox = offsetWidth + x;
			let oy = offsetHeight + y;

			if(ox >= sx && x <= ex
			&& oy >= sy && y <= ey){
				let indexInModel = modelIfaceList.indexOf(temp);

				if(lowestIndex == -1 || lowestIndex > indexInModel){
					if(temp.namespace === "Decoration/Group/Default"){
						let isInnerGroup = (temp.x > this.x && temp.w < this.w) || (temp.y > this.y && temp.h < this.h);
						if(isInnerGroup) lowestIndex = indexInModel;
					}
					else lowestIndex = indexInModel;
				}

				if(temp.namespace === "Decoration/Group/Default") {
					// Skip outer group
					let isInnerGroup = (temp.x > this.x && temp.w < this.w) || (temp.y > this.y && temp.h < this.h);
					if(!isInnerGroup) continue;

					_groups.push(temp);
				}
				_ifaces.push(temp);
			}
		}

		if(lowestIndex !== -1) {
			let thisIndex = modelIfaceList.indexOf(this);

			if(lowestIndex < thisIndex)
				modelIfaceList.move(thisIndex, lowestIndex);
		}

		if(!_isSync)
			this.node.syncOut('trigger', 'refreshContent');

		// Remove any node/cable that contained on inner group
		for (let i=0; i < _groups.length; i++) {
			let temp = _groups[i];

			let { _ifaces: A, _cables: B } = temp;
			for (let i=_cables.length-1; i >= 0; i--) {
				if(B.includes(_cables[i])) _cables.splice(i, 1);
			}
			for (let i=_ifaces.length-1; i >= 0; i--) {
				if(A.includes(_ifaces[i])) _ifaces.splice(i, 1);
			}
		}
	}

	// Refresh content and disable changing index
	swapZIndex(ev){
		this.refreshContent(ev);
		super.swapZIndex(ev, true);
		Coloris.close(); // Close color picker if exist

		// Clear any selections
		let container = this.$space('container');
		container.nodeScope.deselectAll();
		container.cableScope.deselectAll();
	}

	destroy(){
		this.node.instance.off('container.selection', this._listenSelection);
	}
});

class DecorationGroupDefaultData {
	#iface = null;

	constructor(iface){
		this.#iface = iface;

		this.title = 'No title';
		this.textContent = '';
		this.color = '';
		this.fontColor = '';
		this.width = 100;
		this.height = 100;
	}

	dataChanged(name, val){
		let node = this.#iface.node;
		if(window.event?.isTrusted) node.syncOut(name, val);

		// Let editor know if this iface changed and unsaved
		node.notifyEditorDataChanged();
	}

	// Triggered when the value was changed from view (by user interaction on the UI)
	on$title(val){ this.dataChanged('title', val) }
	on$textContent(val){ this.dataChanged('textContent', val) }
	v2m$color(val){ this.dataChanged('color', val) }
	v2m$fontColor(val){ this.dataChanged('fontColor', val) }
	on$width(val){ this.dataChanged('width', val) }
	on$height(val){ this.dataChanged('height', val) }
}