mirror of
https://github.com/slynn1324/tinypin.git
synced 2026-01-23 02:25:08 +00:00
initial import
This commit is contained in:
commit
dc3206c00b
13 changed files with 3791 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
images
|
||||
node_modules
|
||||
data.db
|
||||
.DS_Store
|
||||
332
app.js
Normal file
332
app.js
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const db = require('better-sqlite3')('data.db') //, {verbose:console.log});
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const sharp = require('sharp');
|
||||
const fs = require('fs').promises;
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const IMAGE_PATH = "./images";
|
||||
|
||||
// express config
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
app.use(express.static('static'));
|
||||
app.use(express.static('images'));
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
app.use(bodyParser.json());
|
||||
app.set('json spaces', 2);
|
||||
|
||||
const OK = {status: "ok"};
|
||||
const NOT_FOUND = {status: "error", error: "not found"};
|
||||
const ALREADY_EXISTS = {status: "error", error: "already exists"};
|
||||
const SERVER_ERROR = {status: "error", error: "server error"};
|
||||
|
||||
initDb();
|
||||
|
||||
// list boards
|
||||
app.get("/api/boards", (req, res) => {
|
||||
try{
|
||||
let boards = db.prepare("SELECT * FROM boards").all();
|
||||
|
||||
for( let i = 0; i < boards.length; ++i ){
|
||||
let result = db.prepare("SELECT id FROM pins WHERE boardId = ? order by createDate limit 1").get(boards[i].id);
|
||||
if ( result ) {
|
||||
boards[i].titlePinId = result.id;
|
||||
} else {
|
||||
boards[i].titlePinId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
res.send(boards);
|
||||
} catch (err) {
|
||||
console.log(`Error listing boards: ${err.message}`);
|
||||
res.status(500).send(SERVER_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
// get board
|
||||
app.get("/api/boards/:boardId", (req, res) => {
|
||||
try{
|
||||
let board = db.prepare("SELECT * FROM boards WHERE id = ?").get(req.params.boardId);
|
||||
if ( board ){
|
||||
|
||||
board.pins = db.prepare("SELECT * FROM pins WHERE boardId = ?").all(req.params.boardId);
|
||||
|
||||
res.send(board);
|
||||
} else {
|
||||
res.status(404).send(NOT_FOUND);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error getting board#${req.params.boardId}: ${err.message}`);
|
||||
res.status(500).send(SERVER_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
// create board
|
||||
app.post('/api/boards', (req, res) => {
|
||||
try{
|
||||
let result = db.prepare("INSERT INTO boards (name, createDate) VALUES (@name, @createDate)").run({name: req.body.name, createDate: new Date().toISOString()});
|
||||
let id = result.lastInsertRowid;
|
||||
let board = db.prepare("SELECT * FROM boards WHERE id = ?").get(id);
|
||||
res.send(board);
|
||||
console.log(`Created board#${id} ${req.body.name}`);
|
||||
|
||||
} catch (err){
|
||||
console.log("Error creating board: " + err.message);
|
||||
if ( err.message.includes('UNIQUE constraint failed:') ){
|
||||
res.status(409).send(ALREADY_EXISTS);
|
||||
} else {
|
||||
res.status(500).send(SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// update board
|
||||
app.post("/api/boards/:boardId", (req, res) =>{
|
||||
try{
|
||||
let result = db.prepare("UPDATE boards SET name = @name WHERE id = @boardId").run({name: req.body.name, boardId: req.params.boardId});
|
||||
if ( result.changes == 1 ){
|
||||
res.send(OK);
|
||||
} else {
|
||||
res.status(404).send(NOT_FOUND);
|
||||
}
|
||||
} catch (err){
|
||||
console.log(`Error updating board#${req.params.boardId}: ${err.message}`);
|
||||
res.status(500).send(SERVER_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
// delete board
|
||||
app.delete("/api/boards/:boardId", (req, res) => {
|
||||
try{
|
||||
let result = db.prepare("DELETE FROM boards WHERE id = ?").run(req.params.boardId);
|
||||
if ( result.changes == 1 ){
|
||||
res.send(OK);
|
||||
} else {
|
||||
res.status(404).send(NOT_FOUND);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error deleting board#${req.params.boardId}: ${err.message}`);
|
||||
res.status(500).send(SERVER_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
// get pin
|
||||
app.get("/api/pins/:pinId", (req, res) => {
|
||||
try {
|
||||
let pin = db.prepare('SELECT * FROM pins WHERE id = ?').get(req.params.pinId);
|
||||
if ( pin ){
|
||||
res.send(pin);
|
||||
} else {
|
||||
res.status(404).send(NOT_FOUND);
|
||||
}
|
||||
} catch (err){
|
||||
console.error(`Error getting pin#${req.params.pinId}: ${err.message}`, err);
|
||||
res.status(500).send(SERVER_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
// create pin
|
||||
app.post("/api/pins", async (req, res) => {
|
||||
try {
|
||||
|
||||
console.log(req.body);
|
||||
|
||||
let image = await downloadImage(req.body.imageUrl);
|
||||
console.log(image);
|
||||
|
||||
let result = db.prepare(`INSERT INTO PINS (
|
||||
boardId,
|
||||
imageUrl,
|
||||
siteUrl,
|
||||
description,
|
||||
sortOrder,
|
||||
originalHeight,
|
||||
originalWidth,
|
||||
thumbnailHeight,
|
||||
thumbnailWidth,
|
||||
createDate
|
||||
) VALUES (
|
||||
@boardId,
|
||||
@imageUrl,
|
||||
@siteUrl,
|
||||
@description,
|
||||
@sortOrder,
|
||||
@originalHeight,
|
||||
@originalWidth,
|
||||
@thumbnailHeight,
|
||||
@thumbnailWidth,
|
||||
@createDate)
|
||||
`).run({
|
||||
boardId: req.body.boardId,
|
||||
imageUrl: req.body.imageUrl,
|
||||
siteUrl: req.body.siteUrl,
|
||||
description: req.body.description,
|
||||
sortOrder: req.body.sortOrder,
|
||||
originalHeight: image.original.height,
|
||||
originalWidth: image.original.width,
|
||||
thumbnailHeight: image.thumbnail.height,
|
||||
thumbnailWidth: image.thumbnail.width,
|
||||
createDate: new Date().toISOString()
|
||||
});
|
||||
|
||||
let id = result.lastInsertRowid;
|
||||
|
||||
// write the images to disk
|
||||
let originalImagePath = getOriginalImagePath(id);
|
||||
let thumbnailImagePath = getThumbnailImagePath(id);
|
||||
await fs.mkdir(originalImagePath.dir, {recursive: true});
|
||||
await fs.mkdir(thumbnailImagePath.dir, {recursive: true});
|
||||
await fs.writeFile(originalImagePath.file, image.original.buffer);
|
||||
console.log(`Saved original to: ${originalImagePath.file}`);
|
||||
await fs.writeFile(thumbnailImagePath.file, image.thumbnail.buffer);
|
||||
console.log(`Saved thumbnail to: ${thumbnailImagePath.file}`);
|
||||
|
||||
// return the newly created row
|
||||
let pin = db.prepare("SELECT * FROM pins WHERE id = ?").get(id);
|
||||
res.send(pin);
|
||||
|
||||
} catch (err) {
|
||||
console.log(`Error creating pin: ${err.message}`, err);
|
||||
res.status(500).send(SERVER_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/pins/:pinId", (req,res) => {
|
||||
|
||||
try {
|
||||
let result = db.prepare(`UPDATE pins SET
|
||||
boardId = @boardId,
|
||||
siteUrl = @siteUrl,
|
||||
description = @description,
|
||||
sortOrder = @sortOrder
|
||||
WHERE id = @pinId
|
||||
`).run({
|
||||
pinId: req.params.pinId,
|
||||
boardId: req.body.boardId,
|
||||
siteUrl: req.body.siteUrl,
|
||||
description: req.body.description,
|
||||
sortOrder: req.body.sortOrder
|
||||
});
|
||||
|
||||
if ( result.changes == 1 ){
|
||||
res.send(OK);
|
||||
} else {
|
||||
res.status(404).send(NOT_FOUND);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error updating pin#${req.params.pinId}`, err);
|
||||
res.status(500).send(SERVER_ERROR);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// start listening
|
||||
app.listen(port, () => {
|
||||
console.log(`tinypin is running at http://localhost:${port}`)
|
||||
});
|
||||
|
||||
function initDb(){
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
createDate TEXT
|
||||
)
|
||||
`).run();
|
||||
|
||||
let schemaVersion = db.prepare('select max(id) as id from migrations').get().id;
|
||||
|
||||
if ( !schemaVersion || schemaVersion < 1 ){
|
||||
|
||||
console.log("Running migration to version 1");
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS boards (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
createDate TEXT)
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS pins (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
boardId INTEGER NOT NULL,
|
||||
imageUrl TEXT,
|
||||
siteUrl TEXT,
|
||||
description TEXT,
|
||||
sortOrder INTEGER,
|
||||
originalHeight INTEGER,
|
||||
originalWidth INTEGER,
|
||||
thumbnailHeight INTEGER,
|
||||
thumbnailWidth INTEGER,
|
||||
createDate TEXT,
|
||||
|
||||
FOREIGN KEY (boardId) REFERENCES boards(id)
|
||||
)
|
||||
`).run();
|
||||
|
||||
db.prepare("INSERT INTO migrations (id, createDate) VALUES ( @id, @createDate )").run({id:1, createDate: new Date().toISOString()});
|
||||
|
||||
} else {
|
||||
console.log("Database schema v" + schemaVersion + " is up to date.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function downloadImage(imageUrl){
|
||||
|
||||
let res = await fetch(imageUrl);
|
||||
|
||||
if ( res.status != 200 ){
|
||||
throw(new Error(`download error status=${res.status}`));
|
||||
}
|
||||
|
||||
let buffer = await res.buffer();
|
||||
|
||||
let original = sharp(buffer);
|
||||
let originalMetadata = await original.metadata();
|
||||
let originalBuffer = await original.toFormat("jpg").toBuffer();
|
||||
|
||||
let thumbnail = await original.resize({ width: 400, height: 400, fit: 'inside' });
|
||||
let thumbnailBuffer = await thumbnail.toBuffer();
|
||||
let thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
|
||||
|
||||
return {
|
||||
original: {
|
||||
buffer: originalBuffer,
|
||||
width: originalMetadata.width,
|
||||
height: originalMetadata.height
|
||||
},
|
||||
thumbnail: {
|
||||
buffer: thumbnailBuffer,
|
||||
width: thumbnailMetadata.width,
|
||||
height: thumbnailMetadata.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// function padId(id){
|
||||
// let result = id.toString();
|
||||
// while ( result.length < 12 ) {
|
||||
// result = '0' + result;
|
||||
// }
|
||||
// return result;
|
||||
// }
|
||||
|
||||
function getOriginalImagePath(pinId){
|
||||
let paddedId = pinId.toString().padStart(12, '0');
|
||||
let dir = `${IMAGE_PATH}/originals/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return {dir: dir, file: file};
|
||||
}
|
||||
|
||||
function getThumbnailImagePath(pinId){
|
||||
let paddedId = pinId.toString().padStart(12, '0');
|
||||
let dir = `${IMAGE_PATH}/thumbnails/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return {dir: dir, file: file};
|
||||
}
|
||||
26
chrome-extension/background.js
Normal file
26
chrome-extension/background.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Returns a handler which will open a new window when activated.
|
||||
*/
|
||||
function getClickHandler() {
|
||||
return function(info, tab) {
|
||||
|
||||
var q = "i=" + encodeURIComponent(info.srcUrl) + "&s=" + encodeURIComponent(tab.url);
|
||||
|
||||
// The srcUrl property is only available for image elements.
|
||||
var url = 'http://localhost:3000/popup.html#' + q;
|
||||
|
||||
// Create a new window to the info page.
|
||||
// chrome.windows.create({ url: url, width: 520, height: 660 });
|
||||
chrome.windows.create({ url: url, width: 500, height: 500, type: 'popup' });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a context menu which will only show up for images.
|
||||
*/
|
||||
chrome.contextMenus.create({
|
||||
"title" : "Get image info",
|
||||
"type" : "normal",
|
||||
"contexts" : ["image"],
|
||||
"onclick" : getClickHandler()
|
||||
});
|
||||
10
chrome-extension/manifest.json
Normal file
10
chrome-extension/manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "TinyPin Plugin",
|
||||
"version": "1.0",
|
||||
"description": "TinyPin context plugin",
|
||||
"manifest_version": 2,
|
||||
"background" : { "scripts": ["background.js"] },
|
||||
"permissions" : [
|
||||
"contextMenus"
|
||||
]
|
||||
}
|
||||
18
chrome-extension/popup.html
Normal file
18
chrome-extension/popup.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="google" content="notranslate">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div>
|
||||
image src: <span id="imageSrc"></span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
website url: <span id="siteUrl"></span>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
26
chrome-extension/popup.js
Normal file
26
chrome-extension/popup.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
let hash = window.location.hash;
|
||||
|
||||
if ( hash.length > 0 && hash.indexOf(0) == "#" ){
|
||||
hash = hash.substr(1);
|
||||
}
|
||||
|
||||
let q = parseQueryString(window.location.hash.substr(1));
|
||||
|
||||
document.getElementById("imageSrc").innerText = q.i || "";
|
||||
document.getElementById("siteUrl").innerText = q.s || "";
|
||||
|
||||
function parseQueryString(qs){
|
||||
let obj = {};
|
||||
|
||||
let parts = qs.split("&");
|
||||
for ( let i = 0; i < parts.length; ++i ){
|
||||
let kv = parts[i].split("=");
|
||||
if ( kv.length == 2 ){
|
||||
let key = decodeURIComponent(kv[0]);
|
||||
let value = decodeURIComponent(kv[1]);
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
2385
package-lock.json
generated
Normal file
2385
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "tinypin",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"run": "node app.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^7.1.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"sharp": "^0.27.0"
|
||||
}
|
||||
}
|
||||
104
static/client.css
Normal file
104
static/client.css
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
.brick-wall {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.brick-wall-column {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.brick {
|
||||
margin: 0 5px 10px 5px;
|
||||
}
|
||||
|
||||
.brick img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border-radius: 12px;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
#brick-wall-container {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.board-brick:hover img{
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.board-brick-name {
|
||||
color: #444;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.board-brick a{
|
||||
display: block;
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.board-brick a img{
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#pin-zoom-modal .modal-content {
|
||||
height: 90%;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#pin-zoom-modal .modal-content p {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#pin-zoom-modal .modal-content p img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#add-pin-modal-board-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#add-pin-modal .add-pin-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#add-pin-modal .add-pin-flex-left {
|
||||
flex: 1;
|
||||
|
||||
margin: 10px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
#add-pin-modal .add-pin-flex-left img{
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#add-pin-modal .add-pin-flex-right {
|
||||
flex: 2;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding: 1rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section {
|
||||
flex: 1;
|
||||
}
|
||||
551
static/client.js
Normal file
551
static/client.js
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
Reef.debug(true);
|
||||
|
||||
const store = new Reef.Store({
|
||||
data: {
|
||||
boards: [],
|
||||
board: null,
|
||||
addPin: {
|
||||
active: false,
|
||||
boardId: "",
|
||||
imageUrl: "",
|
||||
previewReady: false,
|
||||
previewImageUrl: null,
|
||||
siteUrl: "",
|
||||
description: ""
|
||||
},
|
||||
pinZoom: {
|
||||
active: false,
|
||||
pinId: null
|
||||
},
|
||||
about: {
|
||||
active: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const actions = {
|
||||
openAddPinModal: () => {
|
||||
|
||||
if ( store.data.board ){
|
||||
store.data.addPin.boardId = store.data.board.id;
|
||||
} else if ( store.data.boards && store.data.boards.length > 0 ){
|
||||
store.data.addPin.boardId = store.data.boards[0].id;
|
||||
} else {
|
||||
store.data.addPin.boardId = 0;
|
||||
}
|
||||
|
||||
store.data.addPin.active = true;
|
||||
},
|
||||
closeAddPinModal: () => {
|
||||
store.data.addPin.active = false;
|
||||
store.data.addPin.imageUrl = "";
|
||||
store.data.addPin.previewImageUrl = "";
|
||||
store.data.addPin.siteUrl = "";
|
||||
store.data.addPin.description = "";
|
||||
},
|
||||
saveAddPin: async () => {
|
||||
|
||||
let postData = {
|
||||
boardId: store.data.addPin.boardId,
|
||||
imageUrl: store.data.addPin.imageUrl,
|
||||
siteUrl: store.data.addPin.siteUrl,
|
||||
description: store.data.addPin.description
|
||||
};
|
||||
|
||||
let res = await fetch('api/pins', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': "application/json"
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
if ( res.status == 200 ){
|
||||
actions.closeAddPinModal();
|
||||
|
||||
let body = await res.json();
|
||||
store.data.board.pins.push(body);
|
||||
}
|
||||
},
|
||||
updateAddPinPreview: () => {
|
||||
if ( store.data.addPin.imageUrl.startsWith("http") ){
|
||||
( async() => {
|
||||
let res = await fetch(store.data.addPin.imageUrl, {
|
||||
mode: 'no-cors',
|
||||
method: "HEAD"
|
||||
});
|
||||
if ( res.status = 200 ){
|
||||
store.data.addPin.previewImageUrl = store.data.addPin.imageUrl;
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
store.data.addPin.previewImageUrl = null;
|
||||
}
|
||||
},
|
||||
openPinZoomModal: (el) => {
|
||||
|
||||
let pinId = el.getAttribute("data-pinid");
|
||||
|
||||
if( pinId ){
|
||||
store.data.pinZoom.pinId = pinId;
|
||||
store.data.pinZoom.active = true;
|
||||
}
|
||||
|
||||
},
|
||||
closePinZoomModal: () => {
|
||||
store.data.pinZoom.active = false;
|
||||
store.data.pinZoom.pinId = null;
|
||||
},
|
||||
movePinZoomModalLeft: () => {
|
||||
|
||||
let idx = 0;
|
||||
for ( let i = 0; i < store.data.board.pins.length; ++i ){
|
||||
if ( store.data.board.pins[i].id == store.data.pinZoom.pinId ){
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if ( idx > 0 ){
|
||||
store.data.pinZoom.pinId = store.data.board.pins[idx-1].id;
|
||||
}
|
||||
|
||||
},
|
||||
movePinZoomModalRight: () => {
|
||||
|
||||
let idx = -1;
|
||||
for ( let i = 0; i < store.data.board.pins.length; ++i ){
|
||||
if ( store.data.board.pins[i].id == store.data.pinZoom.pinId ){
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if ( idx >= 0 && (idx < store.data.board.pins.length-1) ){
|
||||
store.data.pinZoom.pinId = store.data.board.pins[idx+1].id
|
||||
}
|
||||
},
|
||||
showAboutModal: () => {
|
||||
store.data.about.active = true;
|
||||
},
|
||||
closeAboutModal: () => {
|
||||
store.data.about.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Reef("#app", {
|
||||
store: store,
|
||||
template: (store) => {
|
||||
return /*html*/`
|
||||
<div id="navbar"></div>
|
||||
<section class="section">
|
||||
<div class="container" id="brick-wall-container">
|
||||
<div id="brick-wall" class="brick-wall"></div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="footer" id="footer">
|
||||
<div class="content has-text-right">
|
||||
<p>
|
||||
<a data-onclick="showAboutModal">about tinypin</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div id="add-pin-modal"></div>
|
||||
<div id="pin-zoom-modal"></div>
|
||||
<div id="about-modal"></div>
|
||||
`
|
||||
}
|
||||
});
|
||||
|
||||
const aboutModal = new Reef("#about-modal", {
|
||||
store: store,
|
||||
template: (data) => {
|
||||
return /*html*/`
|
||||
<div class="modal ${data.about.active ? 'is-active' : ''}">
|
||||
<div class="modal-background" data-onclick="closeAboutModal"></div>
|
||||
<div class="modal-content">
|
||||
<div class="box" style="font-family: monospace;">
|
||||
<h1><strong>tinypin</strong></h1>
|
||||
<div>
|
||||
<a href="https://www.github.com">github.com/slynn1324/tinypin</a>
|
||||
<br />
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<h2><strong>credits</strong></h2>
|
||||
client
|
||||
<br />
|
||||
css framework » <a href="https://www.bulma.io">bulma.io</a>
|
||||
<br />
|
||||
ui framework » <a href="https://reefjs.com">reef</a>
|
||||
<br />
|
||||
icon » <a href="https://thenounproject.com/term/pinned/1560993/">pinned by Gregor Cresnar from the Noun Project</a>
|
||||
<br />
|
||||
server
|
||||
<br />
|
||||
language & runtime » <a href="https://nodejs.org/en/">node.js</a>
|
||||
<br />
|
||||
database » <a href="https://www.sqlite.org/index.html">sqlite</a>
|
||||
<br />
|
||||
library » <a href="https://www.npmjs.com/package/better-sqlite3">better-sqlite3</a>
|
||||
<br />
|
||||
library » <a href="https://www.npmjs.com/package/express">express</a>
|
||||
<br />
|
||||
library » <a href="https://www.npmjs.com/package/body-parser">body-parser</a>
|
||||
<br />
|
||||
library » <a href="https://www.npmjs.com/package/node-fetch">node-fetch</a>
|
||||
<br />
|
||||
library » <a href="https://www.npmjs.com/package/sharp">sharp</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" data-onclick="closeAboutModal"></button>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
attachTo: app
|
||||
});
|
||||
|
||||
const navbar = new Reef("#navbar", {
|
||||
store: store,
|
||||
template: (data) => {
|
||||
|
||||
return /*html*/`
|
||||
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="#">
|
||||
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' data-name='Layer 1' viewBox='0 0 100 125' x='0px' y='0px'%3E%3Ctitle%3EArtboard 164%3C/title%3E%3Cpath d='M56.77,3.11a4,4,0,1,0-5.66,5.66l5.17,5.17L37.23,33A23.32,23.32,0,0,0,9.42,36.8L7.11,39.11a4,4,0,0,0,0,5.66l21.3,21.29L3.23,91.23a4,4,0,0,0,5.66,5.66L34.06,71.72l21,21a4,4,0,0,0,5.66,0l2.31-2.31a23.34,23.34,0,0,0,3.81-27.82l19-19,5.17,5.18a4,4,0,0,0,5.66-5.66Zm1.16,81.16L15.61,42a15.37,15.37,0,0,1,21.19.51L57.42,63.08A15.39,15.39,0,0,1,57.93,84.27Zm4-28L43.59,37.94,61.94,19.59,80.28,37.94Z'/%3E%3C/svg%3E" width="32" height="32" />
|
||||
<!--<strong>TinyPin</strong>-->
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<span class="navbar-item" id="board-name">${data.board ? data.board.name : "Boards"}</span>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<a class="navbar-item" data-onclick="openAddPinModal">
|
||||
Add Pin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
},
|
||||
attachTo: app
|
||||
});
|
||||
|
||||
const addPinModal = new Reef("#add-pin-modal", {
|
||||
store: store,
|
||||
template: (store) => {
|
||||
|
||||
let imagePlaceholder = 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22300%22%3E%3Crect%20x%3D%222%22%20y%3D%222%22%20width%3D%22300%22%20height%3D%22300%22%20style%3D%22fill%3A%23dedede%3B%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20font-size%3D%2218%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20font-family%3D%22monospace%2C%20sans-serif%22%20fill%3D%22%23555555%22%3Eimage%3C%2Ftext%3E%3C%2Fsvg%3E';
|
||||
|
||||
let options = "";
|
||||
for ( let i = 0; i < store.boards.length; ++i ){
|
||||
options += `<option value="${store.boards[i].id}">${store.boards[i].name}</option>`;
|
||||
}
|
||||
|
||||
return /*html*/`
|
||||
<div class="modal ${store.addPin.active ? 'is-active' : ''}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Add Pin</p>
|
||||
<button class="delete" aria-label="close" data-onclick="closeAddPinModal"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<div class="add-pin-flex">
|
||||
<div class="add-pin-flex-left">
|
||||
<img id="add-pin-modal-img" src="${store.addPin.previewImageUrl ? store.addPin.previewImageUrl : imagePlaceholder}" />
|
||||
</div>
|
||||
<div class="add-pin-flex-right">
|
||||
<form>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Board</label>
|
||||
<div class="select">
|
||||
<select data-bind="addPin.boardId">
|
||||
${options}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Image Url</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" data-bind="addPin.imageUrl" data-onblur="updateAddPinPreview"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Website Url</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" data-bind="addPin.siteUrl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" data-bind="addPin.description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-success" data-onclick="saveAddPin">Add Pin</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
attachTo: app
|
||||
});
|
||||
|
||||
const pinZoomModal = new Reef("#pin-zoom-modal", {
|
||||
store: store,
|
||||
template: (data) => {
|
||||
return /*html*/`
|
||||
<div class="modal ${data.pinZoom.active ? 'is-active' : ''}" id="pin-zoom-modal" >
|
||||
<div class="modal-background" data-onclick="closePinZoomModal"></div>
|
||||
<div class="modal-content">
|
||||
<p>
|
||||
<img src="${data.pinZoom.active ? getOriginalImagePath(data.pinZoom.pinId) : ''}" />
|
||||
</p>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" data-onclick="closePinZoomModal"></button>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
attachTo: app
|
||||
});
|
||||
|
||||
const brickwall = new Reef('#brick-wall', {
|
||||
store: store,
|
||||
template: (data, el) => {
|
||||
|
||||
let numberOfColumns = 1;
|
||||
let width = el.offsetWidth;
|
||||
// matching bulma breakpoints - https://bulma.io/documentation/overview/responsiveness/
|
||||
if( width >= 1216 ){
|
||||
numberOfColumns = 5;
|
||||
} else if ( width >= 1024 ){
|
||||
numberOfColumns = 4;
|
||||
} else if ( width >= 769 ){
|
||||
numberOfColumns = 3;
|
||||
} else if ( width > 320 ){
|
||||
numberOfColumns = 2;
|
||||
}
|
||||
|
||||
function createBrickForBoard(board){
|
||||
return /*html*/`
|
||||
<div class="brick board-brick">
|
||||
<a href="#board=${board.id}">
|
||||
<img src="${getThumbnailImagePath(board.titlePinId)}" />
|
||||
<div class="board-brick-name">${board.name}</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createBrickForPin(board, pin){
|
||||
return /*html*/`
|
||||
<div class="brick" >
|
||||
<a data-pinid="${pin.id}" data-onclick="openPinZoomModal">
|
||||
<img src="${getThumbnailImagePath(pin.id)}" width="${pin.thumbnailWidth}" height="${pin.thumbnailHeight}" />
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// create the brick elements
|
||||
let bricks = [];
|
||||
|
||||
if ( data.board ){
|
||||
for ( let i = 0; i < data.board.pins.length; ++i ){
|
||||
bricks.push(createBrickForPin(data.board, data.board.pins[i]));
|
||||
}
|
||||
} else {
|
||||
for ( let i = 0; i < data.boards.length; ++i ){
|
||||
bricks.push(createBrickForBoard(data.boards[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// create column objects
|
||||
let columns = [];
|
||||
for ( let i = 0; i < numberOfColumns; ++i ){
|
||||
columns[i] = {
|
||||
height: 0,
|
||||
bricks: []
|
||||
}
|
||||
}
|
||||
|
||||
// sort bricks into columns
|
||||
for ( let i = 0; i < bricks.length; ++i ){
|
||||
columns[i % columns.length].bricks.push(bricks[i]);
|
||||
}
|
||||
|
||||
|
||||
// write out the bricks
|
||||
let result = "";
|
||||
|
||||
for ( let col = 0; col < columns.length; ++col ){
|
||||
result += '<div class="brick-wall-column">';
|
||||
|
||||
for ( let i = 0; i < columns[col].bricks.length; ++i ){
|
||||
result += columns[col].bricks[i];
|
||||
}
|
||||
|
||||
result += '</div>';
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
attachTo: app
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click', (el) => {
|
||||
let target = el.target.closest('[data-onclick]');
|
||||
if (target) {
|
||||
let action = target.getAttribute('data-onclick');
|
||||
if (action) {
|
||||
if ( !actions[action] ){
|
||||
console.error(`No action named ${action}`);
|
||||
} else {
|
||||
actions[action](target);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// focusout bubbles while 'blur' does not.
|
||||
document.addEventListener('focusout', (el) => {
|
||||
let target = el.target.closest('[data-onblur]');
|
||||
if ( target ){
|
||||
let method = target.getAttribute('data-onblur');
|
||||
if ( method && typeof(actions[method]) === 'function') {
|
||||
actions[method](target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (el) => {
|
||||
|
||||
if ( store.data.pinZoom.active ){
|
||||
if ( el.key == "Escape" ){
|
||||
actions.closePinZoomModal();
|
||||
} else if ( el.key == "ArrowLeft" ){
|
||||
actions.movePinZoomModalLeft();
|
||||
} else if ( el.key == "ArrowRight" ){
|
||||
actions.movePinZoomModalRight();
|
||||
}
|
||||
}
|
||||
|
||||
if ( store.data.addPin.active ){
|
||||
if ( el.key == "Escape" ){
|
||||
actions.closeAddPinModal();
|
||||
}
|
||||
}
|
||||
|
||||
if ( store.data.about.active ){
|
||||
if ( el.key == "Escape" ){
|
||||
actions.closeAboutModal();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', (evt) => {
|
||||
console.log("hash change");
|
||||
handleHash();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', (evt) => {
|
||||
app.render();
|
||||
});
|
||||
|
||||
Reef.databind(app);
|
||||
app.render();
|
||||
|
||||
loadBoards();
|
||||
handleHash();
|
||||
|
||||
|
||||
|
||||
function handleHash(){
|
||||
|
||||
let hash = parseQueryString(window.location.hash.substr(1));
|
||||
|
||||
if ( hash.board ){
|
||||
if ( !store.board || store.board.id != hash.board ){
|
||||
loadBoard(hash.board);
|
||||
}
|
||||
} else {
|
||||
store.data.board = null;
|
||||
|
||||
store.data.pinZoom.active = false;
|
||||
store.data.addPin.active = false;
|
||||
store.data.about.active = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function loadBoard(id){
|
||||
let res = await fetch("/api/boards/" + id);
|
||||
store.data.board = await res.json();
|
||||
}
|
||||
|
||||
async function loadBoards(){
|
||||
let res = await fetch("/api/boards");
|
||||
store.data.boards = await res.json();
|
||||
}
|
||||
|
||||
|
||||
function parseQueryString(qs){
|
||||
let obj = {};
|
||||
let parts = qs.split("&");
|
||||
for ( let i = 0; i < parts.length; ++i ){
|
||||
let kv = parts[i].split("=");
|
||||
if ( kv.length == 2 ){
|
||||
let key = decodeURIComponent(kv[0]);
|
||||
let value = decodeURIComponent(kv[1]);
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// image urls
|
||||
function padId(id){
|
||||
let result = id.toString();
|
||||
while ( result.length < 12 ) {
|
||||
result = '0' + result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getOriginalImagePath(pinId){
|
||||
let paddedId = padId(pinId);
|
||||
let dir = `originals/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return file;
|
||||
}
|
||||
|
||||
function getThumbnailImagePath(pinId){
|
||||
let paddedId = padId(pinId);
|
||||
let dir = `thumbnails/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return file;
|
||||
}
|
||||
17
static/index.html
Normal file
17
static/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>tinypin</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css" />
|
||||
<link rel="stylesheet" href="client.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/reefjs/dist/reef.min.js"></script>
|
||||
<script src="reef-databind.js"></script>
|
||||
<script src="client.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
static/logo.svg
Normal file
1
static/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 100 125" x="0px" y="0px"><title>Artboard 164</title><path d="M56.77,3.11a4,4,0,1,0-5.66,5.66l5.17,5.17L37.23,33A23.32,23.32,0,0,0,9.42,36.8L7.11,39.11a4,4,0,0,0,0,5.66l21.3,21.29L3.23,91.23a4,4,0,0,0,5.66,5.66L34.06,71.72l21,21a4,4,0,0,0,5.66,0l2.31-2.31a23.34,23.34,0,0,0,3.81-27.82l19-19,5.17,5.18a4,4,0,0,0,5.66-5.66Zm1.16,81.16L15.61,42a15.37,15.37,0,0,1,21.19.51L57.42,63.08A15.39,15.39,0,0,1,57.93,84.27Zm4-28L43.59,37.94,61.94,19.59,80.28,37.94Z"/></svg>
|
||||
|
After Width: | Height: | Size: 534 B |
298
static/reef-databind.js
Normal file
298
static/reef-databind.js
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
// this currently will bind all fields with 'data-bind' attributes
|
||||
// to the 'store'.
|
||||
Reef.databind = function(reef){
|
||||
|
||||
let el = document.querySelector(reef.elem);
|
||||
let store = reef.store;
|
||||
|
||||
if ( !store ){
|
||||
console.log("Databind only works when using a store.");
|
||||
return;
|
||||
}
|
||||
|
||||
// bind all elements on the page that have a data-bind item
|
||||
const bindData = debounce(() => {
|
||||
let elems = el.querySelectorAll("[data-bind]");
|
||||
|
||||
for ( let i = 0; i < elems.length; ++i ){
|
||||
let elem = elems[i];
|
||||
let bindName = elem.getAttribute("data-bind");
|
||||
|
||||
if ( bindName ){
|
||||
let val = get(store.data, bindName, "");
|
||||
|
||||
// dom values are strings, convert back so we can compare safely
|
||||
if ( typeof(val) == 'array' ){
|
||||
let strs = [];
|
||||
for ( let i = 0; i < val.length; ++i ){
|
||||
strs.push(val[i].toString());
|
||||
}
|
||||
val = strs;
|
||||
} else {
|
||||
val = val.toString();
|
||||
}
|
||||
|
||||
// multiple selects need special handling
|
||||
if ( elem.tagName == "SELECT" && elem.matches("[multiple]") ){
|
||||
console.log("multiple");
|
||||
let options = elem.querySelectorAll("option");
|
||||
for ( let i = 0; i < options.length; ++i ){
|
||||
if ( val.indexOf(options[i].value) > -1 ){
|
||||
options[i].selected = true;
|
||||
}
|
||||
}
|
||||
} else if ( elem.type == "radio" || elem.type == "checkbox" ){
|
||||
if ( elem.value == val.toString() ){
|
||||
elem.checked = true;
|
||||
} else {
|
||||
elem.checked = false;
|
||||
}
|
||||
} else {
|
||||
elem.value = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener('input', (evt) => {
|
||||
|
||||
let target = evt.target;
|
||||
let bindPath = target.getAttribute("data-bind");
|
||||
|
||||
if (bindPath) {
|
||||
let val = target.value;
|
||||
|
||||
if ( target.type == "checkbox" ){
|
||||
if ( ! target.checked ){
|
||||
val = null;
|
||||
}
|
||||
}
|
||||
|
||||
// multiple selects need special handling
|
||||
if ( target.tagName == 'SELECT' && target.matches("[multiple]") ){
|
||||
val = [];
|
||||
let options = target.querySelectorAll("option");
|
||||
for ( let i = 0; i < options.length; ++i ){
|
||||
if ( options[i].selected ){
|
||||
console.log(options[i].value);
|
||||
val.push(parseString(options[i].value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for checkbox, radio, and select fields - map numeric and boolean values to
|
||||
// actual types instead of strings
|
||||
else if ( target.type == 'checkbox' || target.type == 'radio' ){
|
||||
val = parseString(val);
|
||||
}
|
||||
|
||||
put(store.data, bindPath, val);
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener('render', (evt) => {
|
||||
bindData();
|
||||
});
|
||||
|
||||
// convert booleans and numbers
|
||||
function parseString(str){
|
||||
if ( str == "true" ){
|
||||
return true;
|
||||
} else if ( str == "false" ){
|
||||
return false;
|
||||
} else {
|
||||
return parseFloat(str) || str;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Debounce functions for better performance
|
||||
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
|
||||
* @param {Function} fn The function to debounce
|
||||
*/
|
||||
function debounce(fn) {
|
||||
|
||||
// Setup a timer
|
||||
var timeout;
|
||||
|
||||
// Return a function to run debounced
|
||||
return function () {
|
||||
|
||||
// Setup the arguments
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
|
||||
// If there's a timer, cancel it
|
||||
if (timeout) {
|
||||
window.cancelAnimationFrame(timeout);
|
||||
}
|
||||
|
||||
// Setup the new requestAnimationFrame()
|
||||
timeout = window.requestAnimationFrame(function () {
|
||||
fn.apply(context, args);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
/*!
|
||||
* Get an object value from a specific path
|
||||
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
|
||||
* @param {Object} obj The object
|
||||
* @param {String|Array} path The path
|
||||
* @param {*} def A default value to return [optional]
|
||||
* @return {*} The value
|
||||
*/
|
||||
function get(obj, path, def) {
|
||||
|
||||
/**
|
||||
* If the path is a string, convert it to an array
|
||||
* @param {String|Array} path The path
|
||||
* @return {Array} The path array
|
||||
*/
|
||||
var stringToPath = function (path) {
|
||||
|
||||
// If the path isn't a string, return it
|
||||
if (typeof path !== 'string') return path;
|
||||
|
||||
// Create new array
|
||||
var output = [];
|
||||
|
||||
// Split to an array with dot notation
|
||||
path.split('.').forEach(function (item) {
|
||||
|
||||
// Split to an array with bracket notation
|
||||
item.split(/\[([^}]+)\]/g).forEach(function (key) {
|
||||
|
||||
// Push to the new array
|
||||
if (key.length > 0) {
|
||||
output.push(key);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return output;
|
||||
|
||||
};
|
||||
|
||||
// Get the path as an array
|
||||
path = stringToPath(path);
|
||||
|
||||
// Cache the current object
|
||||
var current = obj;
|
||||
|
||||
// For each item in the path, dig into the object
|
||||
for (var i = 0; i < path.length; i++) {
|
||||
|
||||
// If the item isn't found, return the default (or null)
|
||||
if (!current[path[i]]) return def;
|
||||
|
||||
// Otherwise, update the current value
|
||||
current = current[path[i]];
|
||||
|
||||
}
|
||||
|
||||
return current;
|
||||
|
||||
};
|
||||
|
||||
|
||||
/*!
|
||||
* Add items to an object at a specific path
|
||||
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
|
||||
* @param {Object} obj The object
|
||||
* @param {String|Array} path The path to assign the value to
|
||||
* @param {*} val The value to assign
|
||||
*/
|
||||
function put(obj, path, val) {
|
||||
|
||||
/**
|
||||
* If the path is a string, convert it to an array
|
||||
* @param {String|Array} path The path
|
||||
* @return {Array} The path array
|
||||
*/
|
||||
var stringToPath = function (path) {
|
||||
|
||||
// If the path isn't a string, return it
|
||||
if (typeof path !== 'string') return path;
|
||||
|
||||
// Create new array
|
||||
var output = [];
|
||||
|
||||
// Split to an array with dot notation
|
||||
path.split('.').forEach(function (item, index) {
|
||||
|
||||
// Split to an array with bracket notation
|
||||
item.split(/\[([^}]+)\]/g).forEach(function (key) {
|
||||
|
||||
// Push to the new array
|
||||
if (key.length > 0) {
|
||||
output.push(key);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return output;
|
||||
|
||||
};
|
||||
|
||||
// Convert the path to an array if not already
|
||||
path = stringToPath(path);
|
||||
|
||||
// Cache the path length and current spot in the object
|
||||
var length = path.length;
|
||||
var current = obj;
|
||||
|
||||
// Loop through the path
|
||||
path.forEach(function (key, index) {
|
||||
|
||||
// Check if the assigned key shoul be an array
|
||||
var isArray = key.slice(-2) === '[]';
|
||||
|
||||
// If so, get the true key name by removing the trailing []
|
||||
key = isArray ? key.slice(0, -2) : key;
|
||||
|
||||
// If the key should be an array and isn't, create an array
|
||||
if (isArray && Object.prototype.toString.call(current[key]) !== '[object Array]') {
|
||||
current[key] = [];
|
||||
}
|
||||
|
||||
// If this is the last item in the loop, assign the value
|
||||
if (index === length - 1) {
|
||||
|
||||
// If it's an array, push the value
|
||||
// Otherwise, assign it
|
||||
if (isArray) {
|
||||
current[key].push(val);
|
||||
} else {
|
||||
current[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, update the current place in the object
|
||||
else {
|
||||
|
||||
// If the key doesn't exist, create it
|
||||
if (!current[key]) {
|
||||
current[key] = {};
|
||||
}
|
||||
|
||||
// Update the current place in the object
|
||||
current = current[key];
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue