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 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;