pict-section-moodboard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/package.json +30 -0
- package/source/Pict-Section-Moodboard.js +21 -0
- package/source/cards/MoodImage-Card.js +85 -0
- package/source/cards/MoodNote-Card.js +91 -0
- package/source/cards/MoodText-Card.js +90 -0
- package/source/sources/ImageSource-Base.js +173 -0
- package/source/views/PictView-Moodboard.js +800 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steven Velozo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# pict-section-moodboard
|
|
2
|
+
|
|
3
|
+
[pict-section-moodboard on npm](https://www.npmjs.com/package/pict-section-moodboard) | [MIT License](LICENSE)
|
|
4
|
+
|
|
5
|
+
A free-form moodboard canvas for the Pict application framework: a board of draggable, resizable
|
|
6
|
+
image tiles, sticky notes, and big-type text statements. It is a thin, heavily customized layer over
|
|
7
|
+
[pict-section-flow](https://www.npmjs.com/package/pict-section-flow) (no ports, no connections, a
|
|
8
|
+
zero-height title bar so cards fill edge to edge), so you get drag, resize, pan, zoom, multi-select,
|
|
9
|
+
marquee, alignment guides, and save/restore for free.
|
|
10
|
+
|
|
11
|
+
Each card is edited through its on-graph properties panel (double-click a card): a textarea plus the
|
|
12
|
+
card's parameters (image URL and fit, note color, text font size, rotation). The card body itself is
|
|
13
|
+
a read-only display, so the whole card drags from anywhere.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install pict-section-moodboard
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It expects `pict-section-flow` (^1.3.0) and `pict-view` (^1.0.68) alongside it.
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
Register the view on a Pict instance and render it:
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
const libMoodboard = require('pict-section-moodboard');
|
|
29
|
+
|
|
30
|
+
pict.addView('Moodboard', libMoodboard.default_configuration, libMoodboard);
|
|
31
|
+
pict.views['Moodboard'].render();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The board reads and writes its whole state through two methods:
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
let tmpBoard = pict.views['Moodboard'].getBoard(); // { Nodes, Connections, ViewState }
|
|
38
|
+
pict.views['Moodboard'].setBoard(tmpBoard); // restore a saved board
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Images
|
|
42
|
+
|
|
43
|
+
A board is self-contained by default: dropped, pasted, or file-picked images are kept as base64 data
|
|
44
|
+
URLs right in the board JSON, and the built-in gallery remembers them.
|
|
45
|
+
|
|
46
|
+
An embedding application can take over image storage by passing its own `ImageSource`:
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
pict.addView('Moodboard', Object.assign({}, libMoodboard.default_configuration,
|
|
50
|
+
{
|
|
51
|
+
ImageSource: myImageSource, // see the interface below
|
|
52
|
+
onBoardChanged: (pBoard) => save(pBoard)
|
|
53
|
+
}), libMoodboard);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
An `ImageSource` declares its own fields, so the gallery builds its filter, sort, and search controls
|
|
57
|
+
from whatever metadata the host has:
|
|
58
|
+
|
|
59
|
+
- `getFields()` returns field descriptors (`{ Key, Label, Type, Searchable, Filterable, Sortable }`).
|
|
60
|
+
- `getFilterOptions(key)` returns the distinct values for a filterable field.
|
|
61
|
+
- `list({ Search, Filters, Sort })` returns the matching images (an array, or a Promise of one for a
|
|
62
|
+
remote store) as `{ Id, Name, Url, Thumbnail, Metadata }`.
|
|
63
|
+
- `upload(file, dataUrl, callback)` stores a dropped or pasted file and calls back with `{ Url }`.
|
|
64
|
+
- `add(record)` registers an image added by URL (optional).
|
|
65
|
+
|
|
66
|
+
The bundled `ImageSource` (exported as `require('pict-section-moodboard').ImageSource`) implements all
|
|
67
|
+
of this over an in-memory base64 collection, so the gallery works stand-alone with no backend.
|
|
68
|
+
|
|
69
|
+
## Autosave
|
|
70
|
+
|
|
71
|
+
Pass `onBoardChanged` to be called with the current board after any change (debounce it on your side;
|
|
72
|
+
it can fire rapidly during a drag). Loading a board with `setBoard` does not trigger it.
|
|
73
|
+
|
|
74
|
+
## Underlying flow options
|
|
75
|
+
|
|
76
|
+
The moodboard turns on these `pict-section-flow` options, all available to any flow consumer:
|
|
77
|
+
`EnableNodeResizing`, `EnableGridSnap` / `GridSnapSize`, `EnableMultiSelect` (shift-click, marquee,
|
|
78
|
+
multi-drag, multi-delete), `EnableAlignmentGuides`, per-node `Rotation`, and `NodeTitleBarHeight: 0`.
|
|
79
|
+
|
|
80
|
+
## Demo
|
|
81
|
+
|
|
82
|
+
`example_applications/moodboard_demo` is a stand-alone Pict application that seeds a board and wires a
|
|
83
|
+
sample gallery with custom metadata. Build it with `npm run build` and serve its `dist` directory.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pict-section-moodboard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Free-form moodboard canvas (draggable, resizable image tiles and notes) built on pict-section-flow",
|
|
5
|
+
"main": "source/Pict-Section-Moodboard.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"source"
|
|
8
|
+
],
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/fable-retold/pict-section-moodboard.git"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "npx mocha -u tdd -R spec --exit test/*_tests.js",
|
|
15
|
+
"start": "node source/Pict-Section-Moodboard.js",
|
|
16
|
+
"build": "npx quack build && cd example_applications/moodboard_demo && npx quack build && npx quack copy"
|
|
17
|
+
},
|
|
18
|
+
"author": "steven velozo <steven@velozo.com>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"pict-section-flow": "^1.3.0",
|
|
22
|
+
"pict-view": "^1.0.68"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"chai": "^6.2.2",
|
|
26
|
+
"mocha": "^11.7.5",
|
|
27
|
+
"pict": "^1.0.372",
|
|
28
|
+
"quackage": "^1.3.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pict-section-moodboard entry point.
|
|
5
|
+
*
|
|
6
|
+
* Exports the Moodboard view (with its default_configuration) plus the two card classes and the
|
|
7
|
+
* note palette, so a host can register the view and, if it wants, reuse or subclass the cards.
|
|
8
|
+
*
|
|
9
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
10
|
+
* @license MIT
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const libMoodboardView = require('./views/PictView-Moodboard.js');
|
|
14
|
+
|
|
15
|
+
module.exports = libMoodboardView;
|
|
16
|
+
module.exports.default_configuration = libMoodboardView.default_configuration;
|
|
17
|
+
module.exports.MoodImageCard = require('./cards/MoodImage-Card.js');
|
|
18
|
+
module.exports.MoodNoteCard = require('./cards/MoodNote-Card.js');
|
|
19
|
+
module.exports.MoodTextCard = require('./cards/MoodText-Card.js');
|
|
20
|
+
module.exports.ImageSource = require('./sources/ImageSource-Base.js');
|
|
21
|
+
module.exports.NOTE_COLORS = libMoodboardView.NOTE_COLORS;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MoodImage: a moodboard image tile.
|
|
5
|
+
*
|
|
6
|
+
* A pict-section-flow card with no ports. Its body is an <img> filling the card edge to edge (the
|
|
7
|
+
* moodboard sets NodeTitleBarHeight 0). The image source lives in Data.ImageUrl (a direct URL or a
|
|
8
|
+
* base64 data URL); object-fit comes from Data.Fit ('cover' default, or 'contain'). You move the
|
|
9
|
+
* tile on the canvas and change its source and fit through the on-graph properties panel.
|
|
10
|
+
*
|
|
11
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
12
|
+
* @license MIT
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const libPictFlowCard = require('pict-section-flow').PictFlowCard;
|
|
16
|
+
|
|
17
|
+
class MoodImageCard extends libPictFlowCard
|
|
18
|
+
{
|
|
19
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
20
|
+
{
|
|
21
|
+
super(pFable, Object.assign(
|
|
22
|
+
{},
|
|
23
|
+
{
|
|
24
|
+
Title: 'Image',
|
|
25
|
+
Name: 'Image',
|
|
26
|
+
Code: 'MoodImage',
|
|
27
|
+
Description: 'An image tile. Move it on the canvas, drag a corner to resize.',
|
|
28
|
+
Category: 'Moodboard',
|
|
29
|
+
Width: 240,
|
|
30
|
+
Height: 200,
|
|
31
|
+
CornerRadius: 8,
|
|
32
|
+
ColorRole: 'none',
|
|
33
|
+
BodyStyle: { fill: 'var(--theme-color-background-tertiary, #eef1f4)' },
|
|
34
|
+
Inputs: [],
|
|
35
|
+
Outputs: [],
|
|
36
|
+
ShowTypeLabel: false,
|
|
37
|
+
BodyContent:
|
|
38
|
+
{
|
|
39
|
+
ContentType: 'html',
|
|
40
|
+
Padding: 0,
|
|
41
|
+
TemplateHash: 'Moodboard-Image-Body',
|
|
42
|
+
Templates:
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
Hash: 'Moodboard-Image-Body',
|
|
46
|
+
Template: /*html*/`<img class="mb-image mb-image-{~D:Record.Data.Fit~}" src="{~D:Record.Data.ImageUrl~}" alt="" draggable="false">`
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
PropertiesPanel:
|
|
51
|
+
{
|
|
52
|
+
PanelType: 'Template',
|
|
53
|
+
DefaultWidth: 280,
|
|
54
|
+
DefaultHeight: 190,
|
|
55
|
+
Title: 'Image',
|
|
56
|
+
Configuration:
|
|
57
|
+
{
|
|
58
|
+
TemplateHash: 'Moodboard-Image-Panel',
|
|
59
|
+
Templates:
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
Hash: 'Moodboard-Image-Panel',
|
|
63
|
+
Template: /*html*/`
|
|
64
|
+
<div class="mbp">
|
|
65
|
+
<label class="mbp-label">Image URL</label>
|
|
66
|
+
<input class="mbp-input" value="{~D:Record.Data.ImageUrl~}" oninput="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setImageUrl('{~D:Record.Hash~}', this.value)">
|
|
67
|
+
<label class="mbp-label">Fit</label>
|
|
68
|
+
<select class="mbp-input" onchange="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setFit('{~D:Record.Hash~}', this.value)">
|
|
69
|
+
<option value="cover">Cover (fill the tile)</option>
|
|
70
|
+
<option value="contain">Contain (show all)</option>
|
|
71
|
+
</select>
|
|
72
|
+
<label class="mbp-label">Rotation</label>
|
|
73
|
+
<input class="mbp-range" type="range" min="-180" max="180" step="1" value="{~D:Record.Rotation~}" oninput="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setRotation('{~D:Record.Hash~}', this.value)">
|
|
74
|
+
</div>`
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
pOptions),
|
|
81
|
+
pServiceHash);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = MoodImageCard;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MoodNote: a moodboard sticky note.
|
|
5
|
+
*
|
|
6
|
+
* A pict-section-flow card with no ports. Its color comes from the per-node Style (BodyFill +
|
|
7
|
+
* TitleBarColor set to the same swatch so the whole card reads as one). The body is a read-only
|
|
8
|
+
* display; you move the note on the canvas and edit it (text + color) through its on-graph
|
|
9
|
+
* properties panel (double-click), which calls back into the moodboard view by node hash.
|
|
10
|
+
*
|
|
11
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
12
|
+
* @license MIT
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const libPictFlowCard = require('pict-section-flow').PictFlowCard;
|
|
16
|
+
|
|
17
|
+
class MoodNoteCard extends libPictFlowCard
|
|
18
|
+
{
|
|
19
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
20
|
+
{
|
|
21
|
+
super(pFable, Object.assign(
|
|
22
|
+
{},
|
|
23
|
+
{
|
|
24
|
+
Title: 'Note',
|
|
25
|
+
Name: 'Note',
|
|
26
|
+
Code: 'MoodNote',
|
|
27
|
+
Description: 'A sticky note. Move it on the canvas; double-click to edit text and color.',
|
|
28
|
+
Category: 'Moodboard',
|
|
29
|
+
Width: 200,
|
|
30
|
+
Height: 160,
|
|
31
|
+
CornerRadius: 8,
|
|
32
|
+
ColorRole: 'none',
|
|
33
|
+
TitleBarColor: '#ffe08a',
|
|
34
|
+
BodyStyle: { fill: '#ffe08a' },
|
|
35
|
+
Inputs: [],
|
|
36
|
+
Outputs: [],
|
|
37
|
+
ShowTypeLabel: false,
|
|
38
|
+
BodyContent:
|
|
39
|
+
{
|
|
40
|
+
ContentType: 'html',
|
|
41
|
+
Padding: 0,
|
|
42
|
+
TemplateHash: 'Moodboard-Note-Body',
|
|
43
|
+
Templates:
|
|
44
|
+
[
|
|
45
|
+
{
|
|
46
|
+
Hash: 'Moodboard-Note-Body',
|
|
47
|
+
Template: /*html*/`<div class="mb-note" data-ph="Write a note">{~D:Record.Data.Text~}</div>`
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
PropertiesPanel:
|
|
52
|
+
{
|
|
53
|
+
PanelType: 'Template',
|
|
54
|
+
DefaultWidth: 260,
|
|
55
|
+
DefaultHeight: 230,
|
|
56
|
+
Title: 'Note',
|
|
57
|
+
Configuration:
|
|
58
|
+
{
|
|
59
|
+
TemplateHash: 'Moodboard-Note-Panel',
|
|
60
|
+
Templates:
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
Hash: 'Moodboard-Note-Panel',
|
|
64
|
+
Template: /*html*/`
|
|
65
|
+
<div class="mbp">
|
|
66
|
+
<label class="mbp-label">Text</label>
|
|
67
|
+
<textarea class="mbp-input mbp-textarea" placeholder="Write a note" oninput="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].editText('{~D:Record.Hash~}', this.value)">{~D:Record.Data.Text~}</textarea>
|
|
68
|
+
<label class="mbp-label">Color</label>
|
|
69
|
+
<div class="mbp-swatches">
|
|
70
|
+
<button class="mbp-swatch" style="background:#ffe08a" onclick="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setNoteColor('{~D:Record.Hash~}','#ffe08a')"></button>
|
|
71
|
+
<button class="mbp-swatch" style="background:#ffb3c1" onclick="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setNoteColor('{~D:Record.Hash~}','#ffb3c1')"></button>
|
|
72
|
+
<button class="mbp-swatch" style="background:#a8d8ff" onclick="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setNoteColor('{~D:Record.Hash~}','#a8d8ff')"></button>
|
|
73
|
+
<button class="mbp-swatch" style="background:#b7e4c7" onclick="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setNoteColor('{~D:Record.Hash~}','#b7e4c7')"></button>
|
|
74
|
+
<button class="mbp-swatch" style="background:#d8c2ff" onclick="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setNoteColor('{~D:Record.Hash~}','#d8c2ff')"></button>
|
|
75
|
+
<button class="mbp-swatch" style="background:#ffd6a5" onclick="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setNoteColor('{~D:Record.Hash~}','#ffd6a5')"></button>
|
|
76
|
+
<button class="mbp-swatch" style="background:#e6e6e6" onclick="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setNoteColor('{~D:Record.Hash~}','#e6e6e6')"></button>
|
|
77
|
+
</div>
|
|
78
|
+
<label class="mbp-label">Rotation</label>
|
|
79
|
+
<input class="mbp-range" type="range" min="-180" max="180" step="1" value="{~D:Record.Rotation~}" oninput="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setRotation('{~D:Record.Hash~}', this.value)">
|
|
80
|
+
</div>`
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
pOptions),
|
|
87
|
+
pServiceHash);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = MoodNoteCard;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MoodText: a big-type text statement, the kind that dominates a moodboard ("live deliberately"
|
|
5
|
+
* set giant between the photos). Unlike a MoodNote it has no card background, bold centered type,
|
|
6
|
+
* and a font that scales with the card box (CSS container units) unless a fixed size is set.
|
|
7
|
+
*
|
|
8
|
+
* The card body is a read-only display; you move it on the canvas and edit it through its on-graph
|
|
9
|
+
* properties panel (double-click), which carries the text plus parameters (font size). The panel
|
|
10
|
+
* calls back into the moodboard view by node hash through AppData.Moodboard.ViewID.
|
|
11
|
+
*
|
|
12
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const libPictFlowCard = require('pict-section-flow').PictFlowCard;
|
|
17
|
+
|
|
18
|
+
class MoodTextCard extends libPictFlowCard
|
|
19
|
+
{
|
|
20
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
21
|
+
{
|
|
22
|
+
super(pFable, Object.assign(
|
|
23
|
+
{},
|
|
24
|
+
{
|
|
25
|
+
Title: 'Text',
|
|
26
|
+
Name: 'Text',
|
|
27
|
+
Code: 'MoodText',
|
|
28
|
+
Description: 'A big-type statement. Resize the card to scale the words, or set a fixed size.',
|
|
29
|
+
Category: 'Moodboard',
|
|
30
|
+
Width: 360,
|
|
31
|
+
Height: 120,
|
|
32
|
+
CornerRadius: 8,
|
|
33
|
+
ColorRole: 'none',
|
|
34
|
+
TitleBarColor: 'transparent',
|
|
35
|
+
BodyStyle: { fill: 'transparent' },
|
|
36
|
+
Inputs: [],
|
|
37
|
+
Outputs: [],
|
|
38
|
+
ShowTypeLabel: false,
|
|
39
|
+
BodyContent:
|
|
40
|
+
{
|
|
41
|
+
ContentType: 'html',
|
|
42
|
+
Padding: 0,
|
|
43
|
+
TemplateHash: 'Moodboard-Text-Body',
|
|
44
|
+
Templates:
|
|
45
|
+
[
|
|
46
|
+
{
|
|
47
|
+
Hash: 'Moodboard-Text-Body',
|
|
48
|
+
Template: /*html*/`<div class="mb-text" data-ph="Big text" style="font-size:{~D:Record.Data.FontSizeCss~}">{~D:Record.Data.Text~}</div>`
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
PropertiesPanel:
|
|
53
|
+
{
|
|
54
|
+
PanelType: 'Template',
|
|
55
|
+
DefaultWidth: 280,
|
|
56
|
+
DefaultHeight: 250,
|
|
57
|
+
Title: 'Text',
|
|
58
|
+
Configuration:
|
|
59
|
+
{
|
|
60
|
+
TemplateHash: 'Moodboard-Text-Panel',
|
|
61
|
+
Templates:
|
|
62
|
+
[
|
|
63
|
+
{
|
|
64
|
+
Hash: 'Moodboard-Text-Panel',
|
|
65
|
+
Template: /*html*/`
|
|
66
|
+
<div class="mbp">
|
|
67
|
+
<label class="mbp-label">Text</label>
|
|
68
|
+
<textarea class="mbp-input mbp-textarea" placeholder="Big text" oninput="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].editText('{~D:Record.Hash~}', this.value)">{~D:Record.Data.Text~}</textarea>
|
|
69
|
+
<label class="mbp-label">Font size</label>
|
|
70
|
+
<select class="mbp-input" onchange="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setFontSize('{~D:Record.Hash~}', this.value)">
|
|
71
|
+
<option value="">Auto (fit the card)</option>
|
|
72
|
+
<option value="28">Small</option>
|
|
73
|
+
<option value="44">Medium</option>
|
|
74
|
+
<option value="68">Large</option>
|
|
75
|
+
<option value="104">Display</option>
|
|
76
|
+
</select>
|
|
77
|
+
<label class="mbp-label">Rotation</label>
|
|
78
|
+
<input class="mbp-range" type="range" min="-180" max="180" step="1" value="{~D:Record.Rotation~}" oninput="_Pict.views['{~D:AppData.Moodboard.ViewID~}'].setRotation('{~D:Record.Hash~}', this.value)">
|
|
79
|
+
</div>`
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
pOptions),
|
|
86
|
+
pServiceHash);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = MoodTextCard;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MoodboardImageSource: the gallery's pluggable image backend.
|
|
5
|
+
*
|
|
6
|
+
* The moodboard gallery does not know where images come from. It asks a source for its fields
|
|
7
|
+
* (so it can build filter / sort / search controls) and for a list of images matching a query.
|
|
8
|
+
* This base class is the built-in, stand-alone source: an in-memory collection of image records,
|
|
9
|
+
* each carrying a Url (a direct URL or a base64 data URL) and a metadata bag. It is what a board
|
|
10
|
+
* uses when no host source is supplied, so everything works on built-ins.
|
|
11
|
+
*
|
|
12
|
+
* An embedding application replaces it (options.ImageSource) with one that serves its own library
|
|
13
|
+
* (for plansheet, the Media blobs) and declares its own metadata fields; the gallery then filters,
|
|
14
|
+
* sorts, and searches over whatever that source exposes. A host source only has to implement
|
|
15
|
+
* getFields(), list(query, callback), and (optionally) add() and upload().
|
|
16
|
+
*
|
|
17
|
+
* Field shape: { Key, Label, Type: 'string'|'enum'|'number'|'date', Searchable?, Filterable?, Sortable? }.
|
|
18
|
+
* 'Name' is a top-level field on each record; every other field reads from record.Metadata[Key].
|
|
19
|
+
*
|
|
20
|
+
* Query shape: { Search: string, Filters: { <Key>: value }, Sort: { Field, Direction: 'asc'|'desc' } }.
|
|
21
|
+
*
|
|
22
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
23
|
+
* @license MIT
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const _DEFAULT_FIELDS =
|
|
27
|
+
[
|
|
28
|
+
{ Key: 'Name', Label: 'Name', Type: 'string', Searchable: true, Sortable: true },
|
|
29
|
+
{ Key: 'Type', Label: 'Type', Type: 'enum', Filterable: true, Sortable: true, Searchable: true },
|
|
30
|
+
{ Key: 'AddedAt', Label: 'Added', Type: 'date', Sortable: true }
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
let _IdCounter = 0;
|
|
34
|
+
|
|
35
|
+
class MoodboardImageSource
|
|
36
|
+
{
|
|
37
|
+
constructor(pOptions)
|
|
38
|
+
{
|
|
39
|
+
let tmpOptions = pOptions || {};
|
|
40
|
+
this._Items = [];
|
|
41
|
+
this._Fields = Array.isArray(tmpOptions.Fields) ? tmpOptions.Fields : _DEFAULT_FIELDS;
|
|
42
|
+
if (Array.isArray(tmpOptions.Items))
|
|
43
|
+
{
|
|
44
|
+
tmpOptions.Items.forEach((pItem) => this.add(pItem));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The metadata fields this source exposes. The gallery builds its controls from this list.
|
|
50
|
+
*/
|
|
51
|
+
getFields() { return this._Fields; }
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Distinct values for an enum field, for building a filter dropdown.
|
|
55
|
+
*/
|
|
56
|
+
getFilterOptions(pKey)
|
|
57
|
+
{
|
|
58
|
+
let tmpSeen = {};
|
|
59
|
+
let tmpValues = [];
|
|
60
|
+
for (let i = 0; i < this._Items.length; i++)
|
|
61
|
+
{
|
|
62
|
+
let tmpValue = this._fieldValue(this._Items[i], pKey);
|
|
63
|
+
if (tmpValue !== '' && tmpValue != null && !tmpSeen[tmpValue])
|
|
64
|
+
{
|
|
65
|
+
tmpSeen[tmpValue] = true;
|
|
66
|
+
tmpValues.push(tmpValue);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
tmpValues.sort();
|
|
70
|
+
return tmpValues;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Add an image record. Deduplicates by Url. Returns the stored record (with an Id).
|
|
75
|
+
* @param {Object} pRecord - { Url, Name?, Thumbnail?, Metadata? }
|
|
76
|
+
*/
|
|
77
|
+
add(pRecord)
|
|
78
|
+
{
|
|
79
|
+
if (!pRecord || !pRecord.Url) { return null; }
|
|
80
|
+
let tmpExisting = this._Items.find((pItem) => pItem.Url === pRecord.Url);
|
|
81
|
+
if (tmpExisting) { return tmpExisting; }
|
|
82
|
+
|
|
83
|
+
let tmpRecord =
|
|
84
|
+
{
|
|
85
|
+
Id: pRecord.Id || ('img-' + (++_IdCounter)),
|
|
86
|
+
Url: pRecord.Url,
|
|
87
|
+
Name: pRecord.Name || 'image',
|
|
88
|
+
Thumbnail: pRecord.Thumbnail || pRecord.Url,
|
|
89
|
+
Metadata: Object.assign({}, pRecord.Metadata)
|
|
90
|
+
};
|
|
91
|
+
this._Items.push(tmpRecord);
|
|
92
|
+
return tmpRecord;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Store an uploaded file. The base source keeps the data URL in memory; a host overrides this to
|
|
97
|
+
* push the bytes to its own store and hand back a record whose Url points there.
|
|
98
|
+
* @param {File} pFile
|
|
99
|
+
* @param {string} pDataUrl - the file read as a base64 data URL
|
|
100
|
+
* @param {Function} fCallback - function(error, record)
|
|
101
|
+
*/
|
|
102
|
+
upload(pFile, pDataUrl, fCallback)
|
|
103
|
+
{
|
|
104
|
+
let tmpRecord = this.add(
|
|
105
|
+
{
|
|
106
|
+
Url: pDataUrl,
|
|
107
|
+
Name: (pFile && pFile.name) || 'image',
|
|
108
|
+
Metadata: { Type: (pFile && pFile.type) || 'image', SizeBytes: (pFile && pFile.size) || 0, AddedAt: Date.now() }
|
|
109
|
+
});
|
|
110
|
+
if (typeof fCallback === 'function') { fCallback(null, tmpRecord); }
|
|
111
|
+
return tmpRecord;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Return the images matching a query (search, filters, sort). Node-style callback plus a direct
|
|
116
|
+
* return so callers can use whichever is convenient.
|
|
117
|
+
*/
|
|
118
|
+
list(pQuery, fCallback)
|
|
119
|
+
{
|
|
120
|
+
let tmpQuery = pQuery || {};
|
|
121
|
+
let tmpItems = this._Items.slice();
|
|
122
|
+
|
|
123
|
+
let tmpSearch = (tmpQuery.Search || '').trim().toLowerCase();
|
|
124
|
+
if (tmpSearch)
|
|
125
|
+
{
|
|
126
|
+
tmpItems = tmpItems.filter((pItem) => this._searchableText(pItem).indexOf(tmpSearch) >= 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let tmpFilters = tmpQuery.Filters || {};
|
|
130
|
+
Object.keys(tmpFilters).forEach((pKey) =>
|
|
131
|
+
{
|
|
132
|
+
let tmpValue = tmpFilters[pKey];
|
|
133
|
+
if (tmpValue === '' || tmpValue == null) { return; }
|
|
134
|
+
tmpItems = tmpItems.filter((pItem) => String(this._fieldValue(pItem, pKey)) === String(tmpValue));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (tmpQuery.Sort && tmpQuery.Sort.Field)
|
|
138
|
+
{
|
|
139
|
+
let tmpField = tmpQuery.Sort.Field;
|
|
140
|
+
let tmpDir = (tmpQuery.Sort.Direction === 'desc') ? -1 : 1;
|
|
141
|
+
tmpItems.sort((pA, pB) =>
|
|
142
|
+
{
|
|
143
|
+
let tmpAV = this._fieldValue(pA, tmpField);
|
|
144
|
+
let tmpBV = this._fieldValue(pB, tmpField);
|
|
145
|
+
if (tmpAV < tmpBV) { return -1 * tmpDir; }
|
|
146
|
+
if (tmpAV > tmpBV) { return 1 * tmpDir; }
|
|
147
|
+
return 0;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof fCallback === 'function') { fCallback(null, tmpItems); }
|
|
152
|
+
return tmpItems;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_fieldValue(pItem, pKey)
|
|
156
|
+
{
|
|
157
|
+
if (pKey === 'Name') { return pItem.Name || ''; }
|
|
158
|
+
return (pItem.Metadata && pItem.Metadata[pKey] != null) ? pItem.Metadata[pKey] : '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_searchableText(pItem)
|
|
162
|
+
{
|
|
163
|
+
let tmpParts = [pItem.Name || ''];
|
|
164
|
+
this._Fields.filter((pField) => pField.Searchable).forEach((pField) =>
|
|
165
|
+
{
|
|
166
|
+
tmpParts.push(String(this._fieldValue(pItem, pField.Key)));
|
|
167
|
+
});
|
|
168
|
+
return tmpParts.join(' ').toLowerCase();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = MoodboardImageSource;
|
|
173
|
+
module.exports.DEFAULT_FIELDS = _DEFAULT_FIELDS;
|