Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve and reenable links #151

Merged
merged 11 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ TeamMapper is based on mindmapp (https://github.com/cedoor/mindmapp , discontinu

- Host and create your own mindmaps
- Set node images, colors and font properties.
- Add links to nodes
- Shortcuts
- Import and export functionality (JSON, SVG, PDF, PNG...)
- Redo / Undo
Expand Down
3 changes: 3 additions & 0 deletions teammapper-backend/src/map/entities/mmpNode.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export class MmpNode {
@Column({ nullable: true, default: 60 })
imageSize: number;

@Column({ nullable: true })
linkHref: string;

@Column({ nullable: true })
locked: boolean;

Expand Down
1 change: 1 addition & 0 deletions teammapper-backend/src/map/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface IMmpClientNode {
id: string;
image: { src: string; size: number };
k: number;
link: { href: string }
locked: boolean;
name: string;
parent: string;
Expand Down
4 changes: 4 additions & 0 deletions teammapper-backend/src/map/utils/clientServerMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const mapMmpNodeToClient = (serverNode: MmpNode): IMmpClientNode => ({
size: serverNode.fontSize || 12,
weight: serverNode.fontWeight || '',
},
link: {
href: serverNode.linkHref || ''
},
id: serverNode.id,
image: { src: serverNode.imageSrc || '', size: serverNode.imageSize || 0 },
k: serverNode.k || 1,
Expand Down Expand Up @@ -47,6 +50,7 @@ const mapClientNodeToMmpNode = (clientNode: IMmpClientNode, mapId: string): Obje
imageSrc: clientNode.image?.src,
imageSize: clientNode.image?.size,
k: clientNode.k,
linkHref: clientNode.link?.href,
locked: clientNode.locked,
name: clientNode.name,
nodeParentId: clientNode.parent ? clientNode.parent : null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddLinkHrefToNode1678605712865 implements MigrationInterface {
name = 'AddLinkHrefToNode1678605712865'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "mmp_node" ADD "linkHref" character varying`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "mmp_node" DROP COLUMN "linkHref"`);
}

}
3 changes: 3 additions & 0 deletions teammapper-backend/test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ describe('AppController (e2e)', () => {
parent: '',
k: -15.361675447001142,
id: '51271bf2-81fa-477a-b0bd-10cecf8d6b65',
link: { href: '' },
locked: false,
isRoot: true,
},
Expand Down Expand Up @@ -118,6 +119,7 @@ describe('AppController (e2e)', () => {
coordinates: { x: 1, y: 2 },
font: {},
colors: {},
link: {}
},
});
});
Expand Down Expand Up @@ -150,6 +152,7 @@ describe('AppController (e2e)', () => {
coordinates: { x: 3, y: 4 },
font: {},
colors: {},
link: {}
},
});
});
Expand Down
2 changes: 0 additions & 2 deletions teammapper-frontend/mmp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,4 @@ export function create (id: string, options?: OptionParameters) {
return new MmpMap(id, options)
}

DOMPurify.setConfig({ADD_ATTR: ['contenteditable'], ADD_TAGS: ['mat-icon']})

export const NodePropertyMapping = PropertyMapping
2 changes: 0 additions & 2 deletions teammapper-frontend/mmp/src/map/handlers/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ export default class Drag {
* @param {Node} node
*/
private started(event: D3DragEvent<any, any, any>, node: Node) {
event.sourceEvent.preventDefault()

this.orientation = this.map.nodes.getOrientation(node)
this.descendants = this.map.nodes.getDescendants(node)

Expand Down
69 changes: 67 additions & 2 deletions teammapper-frontend/mmp/src/map/handlers/draw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default class Draw {

private map: Map
private base64regex: RegExp = /[^a-zA-Z0-9+\/;:,=]/i
private editing: boolean = false

/**
* Get the associated map instance.
Expand Down Expand Up @@ -61,6 +62,7 @@ export default class Draw {

const outer = dom.nodes.enter().append('g')
.style('cursor', 'pointer')
.style('touch-action', 'none')
.attr('class', this.map.id + '_node')
.attr('id', function (node: Node) {
node.dom = this
Expand All @@ -70,7 +72,14 @@ export default class Draw {
.on('dblclick', (event: MouseEvent, node: Node) => {
event.stopPropagation()
this.enableNodeNameEditing(node)
}).on('touchstart', (_event: TouchEvent, node: Node) => {
}).on('touchstart', (event: TouchEvent, node: Node) => {
// When not clicking a link and not in edit mode, disable all mobile native touch events
// A single tap is supposed to move the node in this application
if(!this.isLinkTarget(event) && !this.editing) {
event.preventDefault()
}

// a single tap should enter moving node mode - not a selection
if (!tapedTwice) {
tapedTwice = true

Expand Down Expand Up @@ -105,9 +114,10 @@ export default class Draw {
.style('stroke-width', 3)
.attr('d', (node: Node) => this.drawNodeBackground(node))

// Set image of the node
// Set image and link of the node
outer.each((node: Node) => {
this.setImage(node)
this.setLink(node)
})


Expand Down Expand Up @@ -199,6 +209,7 @@ export default class Draw {
d3.selectAll('.' + this.map.id + '_branch').attr('d', (node: Node) => this.drawBranch(node) as any)

this.updateImagePosition(node)
this.updateLinkPosition(node)

this.updateNodeNameContainer(node)
}
Expand Down Expand Up @@ -242,6 +253,35 @@ export default class Draw {
}
}

/**
* Set main properties of node image and create it if it does not exist.
* @param {Node} node
*/
public setLink(node: Node) {
let domLink = node.getLinkDOM()

if (!domLink) {
domLink = document.createElementNS('http://www.w3.org/2000/svg', 'a')
const domText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
domText.textContent = 'link'
domText.classList.add('link-text')
domText.classList.add('material-icons')
domText.style.setProperty('fill', DOMPurify.sanitize(node.colors.name))
domText.setAttribute('y', node.dimensions.height.toString())
domText.setAttribute('x', '-10')
node.dom.appendChild(domLink)
domLink.appendChild(domText)
}

if (DOMPurify.sanitize(node.link.href) !== '') {
domLink.setAttribute('href', DOMPurify.sanitize(node.link.href))
domLink.setAttribute('target', '_self')

} else {
domLink.remove()
}
}

/**
* Update the node image position.
* @param {Node} node
Expand All @@ -254,11 +294,24 @@ export default class Draw {
}
}

/**
* Update the node link position.
* @param {Node} node
*/
public updateLinkPosition(node: Node) {
if (DOMPurify.sanitize(node.link.href) !== '') {
const link = node.getLinkDOM(),
y = node.dimensions.height
link.setAttribute('y', y.toString())
}
}

/**
* Enable and manage all events for the name editing.
* @param {Node} node
*/
public enableNodeNameEditing(node: Node) {
this.editing = true
const name = node.getNameDOM()
name.innerHTML = DOMPurify.sanitize(node.name)

Expand Down Expand Up @@ -321,6 +374,8 @@ export default class Draw {
}

name.onblur = () => {
this.editing = false

if (name.innerHTML !== node.name) {
this.map.nodes.updateNode('name', DOMPurify.sanitize(name.innerHTML))
}
Expand Down Expand Up @@ -383,6 +438,7 @@ export default class Draw {
div.style.setProperty('font-weight', DOMPurify.sanitize(node.font.weight))
div.style.setProperty('text-decoration', DOMPurify.sanitize(node.font.decoration))

div.style.setProperty('touch-action', 'none')
div.style.setProperty('display', 'inline-block')
div.style.setProperty('white-space', 'pre')
div.style.setProperty('width', 'auto')
Expand All @@ -399,4 +455,13 @@ export default class Draw {
return div.outerHTML
}

/**
* Checks if the target of the event is the link below the node
* @param {TouchEvent} event
* @returns {boolean}
*/
private isLinkTarget(event: TouchEvent): boolean {
return event.target['classList'][0] === 'link-text'
}

}
30 changes: 18 additions & 12 deletions teammapper-frontend/mmp/src/map/handlers/history.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Map from '../map'
import Node, {Colors, Coordinates, ExportNodeProperties, Font, Image, NodeProperties} from '../models/node'
import Node, {Colors, Coordinates, ExportNodeProperties, Font, Image, Link, NodeProperties} from '../models/node'
import {Event} from './events'
import Log from '../../utils/log'
import Utils from '../../utils/utils'
import { DefaultNodeValues } from '../options'

/**
* Manage map history, for each change save a snapshot.
Expand Down Expand Up @@ -120,23 +121,26 @@ export default class History {
this.map.nodes.clear()

snapshot.forEach((property: ExportNodeProperties) => {
// in case the data model changes this makes sure all properties are at least present using defaults
const mergedProperty = { ...DefaultNodeValues, ...property } as ExportNodeProperties
const properties: NodeProperties = {
id: property.id,
parent: this.map.nodes.getNode(property.parent),
k: property.k,
name: property.name,
coordinates: Utils.cloneObject(property.coordinates) as Coordinates,
image: Utils.cloneObject(property.image) as Image,
colors: Utils.cloneObject(property.colors) as Colors,
font: Utils.cloneObject(property.font) as Font,
locked: property.locked,
isRoot: property.isRoot
id: mergedProperty.id,
parent: this.map.nodes.getNode(mergedProperty.parent),
k: mergedProperty.k,
name: mergedProperty.name,
coordinates: Utils.cloneObject(mergedProperty.coordinates) as Coordinates,
image: Utils.cloneObject(mergedProperty.image) as Image,
colors: Utils.cloneObject(mergedProperty.colors) as Colors,
font: Utils.cloneObject(mergedProperty.font) as Font,
link: Utils.cloneObject(mergedProperty.link) as Link,
locked: mergedProperty.locked,
isRoot: mergedProperty.isRoot
}

const node: Node = new Node(properties)
this.map.nodes.setNode(node.id, node)

if(property.isRoot) this.map.rootId = property.id
if(mergedProperty.isRoot) this.map.rootId = mergedProperty.id
})

this.map.draw.clear()
Expand Down Expand Up @@ -203,6 +207,8 @@ export default class History {
typeof node.k === 'number',
typeof node.name === 'string',
typeof node.locked === 'boolean',
// older maps do not include the link prop yet
(node.link === undefined || typeof node.link.href === 'string'),
node.coordinates
&& typeof node.coordinates.x === 'number'
&& typeof node.coordinates.y === 'number',
Expand Down
26 changes: 26 additions & 0 deletions teammapper-frontend/mmp/src/map/handlers/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Node, {
ExportNodeProperties,
Font,
Image,
Link,
NodeProperties,
UserNodeProperties
} from '../models/node'
Expand Down Expand Up @@ -264,6 +265,9 @@ export default class Nodes {
case 'imageSize':
updated = this.updateNodeImageSize(node, value, graphic)
break
case 'linkHref':
updated = this.updateNodeLinkHref(node, value)
break
case 'backgroundColor':
updated = this.updateNodeBackgroundColor(node, value, graphic)
break
Expand Down Expand Up @@ -370,6 +374,7 @@ export default class Nodes {
image: Utils.cloneObject(node.image) as Image,
colors: Utils.cloneObject(node.colors) as Colors,
font: Utils.cloneObject(node.font) as Font,
link: Utils.cloneObject(node.link) as Link,
locked: node.locked,
isRoot: node.isRoot,
k: node.k
Expand Down Expand Up @@ -862,6 +867,26 @@ export default class Nodes {
}
}

/**
* Update the node link href with a new value.
* @param {Node} node
* @param {string} href
* @returns {boolean}
*/
private updateNodeLinkHref = (node: Node, href: string) => {
if (href && typeof href !== 'string') {
Log.error('The link href must be a string', 'type')
}

if (node.link.href !== href) {
node.link.href = href

this.map.draw.setLink(node)
} else {
return false
}
}

/**
* Update the node font style.
* @param {Node} node
Expand Down Expand Up @@ -1021,6 +1046,7 @@ export const PropertyMapping = {
coordinates: ['coordinates'],
imageSrc: ['image', 'src'],
imageSize: ['image', 'size'],
linkHref: ['link', 'href'],
backgroundColor: ['colors', 'background'],
branchColor: ['colors', 'branch'],
fontWeight: ['font', 'weight'],
Expand Down
Loading