From b09463d1b2742e41743c5617a39bcd3cf87fb223 Mon Sep 17 00:00:00 2001 From: azivner Date: Sat, 7 Apr 2018 21:30:01 -0400 Subject: [PATCH 01/18] async logging of info messages --- src/services/log.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/services/log.js b/src/services/log.js index 4d0abc21e..7d7c4a8fd 100644 --- a/src/services/log.js +++ b/src/services/log.js @@ -15,14 +15,22 @@ const logger = require('simple-node-logger').createRollingFileLogger({ }); function info(message) { - logger.info(message); + // info messages are logged asynchronously + setTimeout(() => { + console.log(message); - console.log(message); + logger.info(message); + }, 0); } function error(message) { + message = "ERROR: " + message; + // we're using .info() instead of .error() because simple-node-logger emits weird error for showError() - info("ERROR: " + message); + // errors are logged synchronously to make sure it doesn't get lost in case of crash + logger.info(message); + + console.trace(message); } const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ]; From 64336ffbeec30aa25b5b223cb97aedadd9e9ffb0 Mon Sep 17 00:00:00 2001 From: azivner Date: Sat, 7 Apr 2018 21:53:42 -0400 Subject: [PATCH 02/18] implemented bulk sync pull for increased performance --- src/routes/api/sync.js | 56 ++++++++++++++++++++++++++++++++++++------ src/services/sync.js | 30 +++++++++++----------- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/routes/api/sync.js b/src/routes/api/sync.js index 730753ec8..453e7dfa7 100644 --- a/src/routes/api/sync.js +++ b/src/routes/api/sync.js @@ -58,7 +58,52 @@ async function forceNoteSync(req) { async function getChanged(req) { const lastSyncId = parseInt(req.query.lastSyncId); - return await sql.getRows("SELECT * FROM sync WHERE id > ?", [lastSyncId]); + const records = []; + let length = 0; + + for (const sync of await sql.getRows("SELECT * FROM sync WHERE id > ?", [lastSyncId])) { + const record = { + sync: sync, + entity: await getEntityRow(sync.entityName, sync.entityId) + }; + + records.push(record); + + length += JSON.stringify(record).length; + + if (length > 1000000) { + break; + } + } + + return records; +} + +const primaryKeys = { + "notes": "noteId", + "branches": "branchId", + "note_revisions": "noteRevisionId", + "option": "name", + "recent_notes": "branchId", + "images": "imageId", + "note_images": "noteImageId", + "labels": "labelId", + "api_tokens": "apiTokenId" +}; + +async function getEntityRow(entityName, entityId) { + if (entityName === 'note_reordering') { + return await getNoteReordering(entityId); + } + else { + const primaryKey = primaryKeys[entityName]; + + if (!primaryKey) { + throw new Error("Unknown entity " + entityName); + } + + return await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); + } } async function getNote(req) { @@ -96,13 +141,8 @@ async function getOption(req) { } } -async function getNoteReordering(req) { - const parentNoteId = req.params.parentNoteId; - - return { - parentNoteId: parentNoteId, - ordering: await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]) - }; +async function getNoteReordering(parentNoteId) { + return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]) } async function getRecentNote(req) { diff --git a/src/services/sync.js b/src/services/sync.js index fe1c0c445..b9bf1f4ea 100644 --- a/src/services/sync.js +++ b/src/services/sync.js @@ -104,11 +104,11 @@ async function pullSync(syncContext) { const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull; - const syncRows = await syncRequest(syncContext, 'GET', changesUri); + const rows = await syncRequest(syncContext, 'GET', changesUri); - log.info("Pulled " + syncRows.length + " changes from " + changesUri); + log.info("Pulled " + rows.length + " changes from " + changesUri); - for (const sync of syncRows) { + for (const {sync, entity} of rows) { if (sourceIdService.isLocalSourceId(sync.sourceId)) { log.info(`Skipping pull #${sync.id} ${sync.entityName} ${sync.entityId} because ${sync.sourceId} is a local source id.`); @@ -117,40 +117,38 @@ async function pullSync(syncContext) { continue; } - const resp = await syncRequest(syncContext, 'GET', "/api/sync/" + sync.entityName + "/" + encodeURIComponent(sync.entityId)); - - if (!resp || (sync.entityName === 'notes' && !resp.entity)) { + if (!entity) { log.error(`Empty response to pull for sync #${sync.id} ${sync.entityName}, id=${sync.entityId}`); } else if (sync.entityName === 'notes') { - await syncUpdateService.updateNote(resp.entity, syncContext.sourceId); + await syncUpdateService.updateNote(entity, syncContext.sourceId); } else if (sync.entityName === 'branches') { - await syncUpdateService.updateBranch(resp, syncContext.sourceId); + await syncUpdateService.updateBranch(entity, syncContext.sourceId); } else if (sync.entityName === 'note_revisions') { - await syncUpdateService.updateNoteRevision(resp, syncContext.sourceId); + await syncUpdateService.updateNoteRevision(entity, syncContext.sourceId); } else if (sync.entityName === 'note_reordering') { - await syncUpdateService.updateNoteReordering(resp, syncContext.sourceId); + await syncUpdateService.updateNoteReordering(entity, syncContext.sourceId); } else if (sync.entityName === 'options') { - await syncUpdateService.updateOptions(resp, syncContext.sourceId); + await syncUpdateService.updateOptions(entity, syncContext.sourceId); } else if (sync.entityName === 'recent_notes') { - await syncUpdateService.updateRecentNotes(resp, syncContext.sourceId); + await syncUpdateService.updateRecentNotes(entity, syncContext.sourceId); } else if (sync.entityName === 'images') { - await syncUpdateService.updateImage(resp, syncContext.sourceId); + await syncUpdateService.updateImage(entity, syncContext.sourceId); } else if (sync.entityName === 'note_images') { - await syncUpdateService.updateNoteImage(resp, syncContext.sourceId); + await syncUpdateService.updateNoteImage(entity, syncContext.sourceId); } else if (sync.entityName === 'labels') { - await syncUpdateService.updateLabel(resp, syncContext.sourceId); + await syncUpdateService.updateLabel(entity, syncContext.sourceId); } else if (sync.entityName === 'api_tokens') { - await syncUpdateService.updateApiToken(resp, syncContext.sourceId); + await syncUpdateService.updateApiToken(entity, syncContext.sourceId); } else { throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); From 13f71f89675819da60168117e3d0df25a377d381 Mon Sep 17 00:00:00 2001 From: azivner Date: Sat, 7 Apr 2018 22:25:28 -0400 Subject: [PATCH 03/18] bulk push sync --- src/routes/api/sync.js | 58 ++--------- src/routes/routes.js | 1 + src/services/sync.js | 197 +++++++++++++++++------------------- src/services/sync_table.js | 2 + src/services/sync_update.js | 37 +++++++ 5 files changed, 144 insertions(+), 151 deletions(-) diff --git a/src/routes/api/sync.js b/src/routes/api/sync.js index 453e7dfa7..d67b4f5a0 100644 --- a/src/routes/api/sync.js +++ b/src/routes/api/sync.js @@ -11,7 +11,7 @@ const log = require('../../services/log'); async function checkSync() { return { 'hashes': await contentHashService.getHashes(), - 'max_sync_id': await sql.getValue('SELECT MAX(id) FROM sync') + 'maxSyncId': await sql.getValue('SELECT MAX(id) FROM sync') }; } @@ -58,51 +58,17 @@ async function forceNoteSync(req) { async function getChanged(req) { const lastSyncId = parseInt(req.query.lastSyncId); - const records = []; - let length = 0; + const syncs = await sql.getRows("SELECT * FROM sync WHERE id > ? LIMIT 1000", [lastSyncId]); - for (const sync of await sql.getRows("SELECT * FROM sync WHERE id > ?", [lastSyncId])) { - const record = { - sync: sync, - entity: await getEntityRow(sync.entityName, sync.entityId) - }; - - records.push(record); - - length += JSON.stringify(record).length; - - if (length > 1000000) { - break; - } - } - - return records; + return await syncService.getSyncRecords(syncs); } -const primaryKeys = { - "notes": "noteId", - "branches": "branchId", - "note_revisions": "noteRevisionId", - "option": "name", - "recent_notes": "branchId", - "images": "imageId", - "note_images": "noteImageId", - "labels": "labelId", - "api_tokens": "apiTokenId" -}; +async function update(req) { + const sourceId = req.body.sourceId; + const entities = req.body.entities; -async function getEntityRow(entityName, entityId) { - if (entityName === 'note_reordering') { - return await getNoteReordering(entityId); - } - else { - const primaryKey = primaryKeys[entityName]; - - if (!primaryKey) { - throw new Error("Unknown entity " + entityName); - } - - return await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); + for (const {sync, entity} of entities) { + await syncUpdateService.updateEntity(sync.entityName, entity, sourceId); } } @@ -141,10 +107,6 @@ async function getOption(req) { } } -async function getNoteReordering(parentNoteId) { - return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]) -} - async function getRecentNote(req) { const branchId = req.params.branchId; @@ -231,7 +193,6 @@ module.exports = { getBranch, getImage, getNoteImage, - getNoteReordering, getNoteRevision, getRecentNote, getOption, @@ -246,5 +207,6 @@ module.exports = { updateRecentNote, updateOption, updateLabel, - updateApiToken + updateApiToken, + update }; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index 156d190a7..83e58bed1 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -147,6 +147,7 @@ function register(app) { apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged); + apiRoute(PUT, '/api/sync/update', syncApiRoute.update); apiRoute(GET, '/api/sync/notes/:noteId', syncApiRoute.getNote); apiRoute(GET, '/api/sync/branches/:branchId', syncApiRoute.getBranch); apiRoute(GET, '/api/sync/note_revisions/:noteRevisionId', syncApiRoute.getNoteRevision); diff --git a/src/services/sync.js b/src/services/sync.js index b9bf1f4ea..685451a28 100644 --- a/src/services/sync.js +++ b/src/services/sync.js @@ -120,38 +120,8 @@ async function pullSync(syncContext) { if (!entity) { log.error(`Empty response to pull for sync #${sync.id} ${sync.entityName}, id=${sync.entityId}`); } - else if (sync.entityName === 'notes') { - await syncUpdateService.updateNote(entity, syncContext.sourceId); - } - else if (sync.entityName === 'branches') { - await syncUpdateService.updateBranch(entity, syncContext.sourceId); - } - else if (sync.entityName === 'note_revisions') { - await syncUpdateService.updateNoteRevision(entity, syncContext.sourceId); - } - else if (sync.entityName === 'note_reordering') { - await syncUpdateService.updateNoteReordering(entity, syncContext.sourceId); - } - else if (sync.entityName === 'options') { - await syncUpdateService.updateOptions(entity, syncContext.sourceId); - } - else if (sync.entityName === 'recent_notes') { - await syncUpdateService.updateRecentNotes(entity, syncContext.sourceId); - } - else if (sync.entityName === 'images') { - await syncUpdateService.updateImage(entity, syncContext.sourceId); - } - else if (sync.entityName === 'note_images') { - await syncUpdateService.updateNoteImage(entity, syncContext.sourceId); - } - else if (sync.entityName === 'labels') { - await syncUpdateService.updateLabel(entity, syncContext.sourceId); - } - else if (sync.entityName === 'api_tokens') { - await syncUpdateService.updateApiToken(entity, syncContext.sourceId); - } else { - throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); + await syncUpdateService.updateEntity(sync.entityName, entity, syncContext.sourceId); } await setLastSyncedPull(sync.id); @@ -172,90 +142,47 @@ async function pushSync(syncContext) { let lastSyncedPush = await getLastSyncedPush(); while (true) { - const sync = await sql.getRowOrNull('SELECT * FROM sync WHERE id > ? LIMIT 1', [lastSyncedPush]); + const syncs = await sql.getRows('SELECT * FROM sync WHERE id > ? LIMIT 1000', [lastSyncedPush]); - if (sync === null) { + const filteredSyncs = syncs.filter(sync => { + if (sync.sourceId === syncContext.sourceId) { + log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`); + + // this may set lastSyncedPush beyond what's actually sent (because of size limit) + // so this is applied to the database only if there's no actual update + // TODO: it would be better to simplify this somehow + lastSyncedPush = sync.id; + + return false; + } + else { + return true; + } + }); + + if (filteredSyncs.length === 0) { // nothing to sync log.info("Nothing to push"); + await setLastSyncedPush(lastSyncedPush); + break; } - if (sync.sourceId === syncContext.sourceId) { - log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`); - } - else { - await pushEntity(sync, syncContext); - } + const syncRecords = await getSyncRecords(filteredSyncs); - lastSyncedPush = sync.id; + await syncRequest(syncContext, 'PUT', '/api/sync/update', { + sourceId: sourceIdService.getCurrentSourceId(), + entities: syncRecords + }); + + lastSyncedPush = syncRecords[syncRecords.length - 1].sync.id; await setLastSyncedPush(lastSyncedPush); } } -async function pushEntity(sync, syncContext) { - let entity; - - if (sync.entityName === 'notes') { - entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]); - - serializeNoteContentBuffer(entity); - } - else if (sync.entityName === 'branches') { - entity = await sql.getRow('SELECT * FROM branches WHERE branchId = ?', [sync.entityId]); - } - else if (sync.entityName === 'note_revisions') { - entity = await sql.getRow('SELECT * FROM note_revisions WHERE noteRevisionId = ?', [sync.entityId]); - } - else if (sync.entityName === 'note_reordering') { - entity = { - parentNoteId: sync.entityId, - ordering: await sql.getMap('SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [sync.entityId]) - }; - } - else if (sync.entityName === 'options') { - entity = await sql.getRow('SELECT * FROM options WHERE name = ?', [sync.entityId]); - } - else if (sync.entityName === 'recent_notes') { - entity = await sql.getRow('SELECT * FROM recent_notes WHERE branchId = ?', [sync.entityId]); - } - else if (sync.entityName === 'images') { - entity = await sql.getRow('SELECT * FROM images WHERE imageId = ?', [sync.entityId]); - - if (entity.data !== null) { - entity.data = entity.data.toString('base64'); - } - } - else if (sync.entityName === 'note_images') { - entity = await sql.getRow('SELECT * FROM note_images WHERE noteImageId = ?', [sync.entityId]); - } - else if (sync.entityName === 'labels') { - entity = await sql.getRow('SELECT * FROM labels WHERE labelId = ?', [sync.entityId]); - } - else if (sync.entityName === 'api_tokens') { - entity = await sql.getRow('SELECT * FROM api_tokens WHERE apiTokenId = ?', [sync.entityId]); - } - else { - throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); - } - - if (!entity) { - log.info(`Sync #${sync.id} entity for ${sync.entityName} ${sync.entityId} doesn't exist. Skipping.`); - return; - } - - log.info(`Pushing changes in sync #${sync.id} ${sync.entityName} ${sync.entityId}`); - - const payload = { - sourceId: sourceIdService.getCurrentSourceId(), - entity: entity - }; - - await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload); -} - function serializeNoteContentBuffer(note) { if (note.type === 'file') { note.content = note.content.toString("binary"); @@ -265,7 +192,7 @@ function serializeNoteContentBuffer(note) { async function checkContentHash(syncContext) { const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); - if (await getLastSyncedPull() < resp.max_sync_id) { + if (await getLastSyncedPull() < resp.maxSyncId) { log.info("There are some outstanding pulls, skipping content check."); return; @@ -329,6 +256,68 @@ async function syncRequest(syncContext, method, uri, body) { } } +const primaryKeys = { + "notes": "noteId", + "branches": "branchId", + "note_revisions": "noteRevisionId", + "option": "name", + "recent_notes": "branchId", + "images": "imageId", + "note_images": "noteImageId", + "labels": "labelId", + "api_tokens": "apiTokenId" +}; + +async function getEntityRow(entityName, entityId) { + if (entityName === 'note_reordering') { + return await getNoteReordering(entityId); + } + else { + const primaryKey = primaryKeys[entityName]; + + if (!primaryKey) { + throw new Error("Unknown entity " + entityName); + } + + const entityRow = await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); + + if (entityName === 'notes') { + serializeNoteContentBuffer(entityRow); + } + else if (entityName === 'images') { + entityRow.data = entityRow.data.toString('base64'); + } + + return entityRow; + } +} + +async function getSyncRecords(syncs) { + const records = []; + let length = 0; + + for (const sync of syncs) { + const record = { + sync: sync, + entity: await getEntityRow(sync.entityName, sync.entityId) + }; + + records.push(record); + + length += JSON.stringify(record).length; + + if (length > 1000000) { + break; + } + } + + return records; +} + +async function getNoteReordering(parentNoteId) { + return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]) +} + sqlInit.dbReady.then(() => { if (syncSetup.isSyncSetup) { log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT); @@ -355,5 +344,7 @@ sqlInit.dbReady.then(() => { module.exports = { sync, - serializeNoteContentBuffer + serializeNoteContentBuffer, + getEntityRow, + getSyncRecords }; \ No newline at end of file diff --git a/src/services/sync_table.js b/src/services/sync_table.js index cf80522c8..7ba8f5212 100644 --- a/src/services/sync_table.js +++ b/src/services/sync_table.js @@ -91,6 +91,8 @@ async function fillSyncRows(entityName, entityKey) { } async function fillAllSyncRows() { + await sql.execute("DELETE FROM sync"); + await fillSyncRows("notes", "noteId"); await fillSyncRows("branches", "branchId"); await fillSyncRows("note_revisions", "noteRevisionId"); diff --git a/src/services/sync_update.js b/src/services/sync_update.js index d33c9009e..2858561a5 100644 --- a/src/services/sync_update.js +++ b/src/services/sync_update.js @@ -3,6 +3,42 @@ const log = require('./log'); const eventLogService = require('./event_log'); const syncTableService = require('./sync_table'); +async function updateEntity(entityName, entity, sourceId) { + if (entityName === 'notes') { + await updateNote(entity, sourceId); + } + else if (entityName === 'branches') { + await updateBranch(entity, sourceId); + } + else if (entityName === 'note_revisions') { + await updateNoteRevision(entity, sourceId); + } + else if (entityName === 'note_reordering') { + await updateNoteReordering(entity, sourceId); + } + else if (entityName === 'options') { + await updateOptions(entity, sourceId); + } + else if (entityName === 'recent_notes') { + await updateRecentNotes(entity, sourceId); + } + else if (entityName === 'images') { + await updateImage(entity, sourceId); + } + else if (entityName === 'note_images') { + await updateNoteImage(entity, sourceId); + } + else if (entityName === 'labels') { + await updateLabel(entity, sourceId); + } + else if (entityName === 'api_tokens') { + await updateApiToken(entity, sourceId); + } + else { + throw new Error(`Unrecognized entity type ${entityName}`); + } +} + function deserializeNoteContentBuffer(note) { if (note.type === 'file') { note.content = new Buffer(note.content, 'binary'); @@ -159,6 +195,7 @@ async function updateApiToken(entity, sourceId) { } module.exports = { + updateEntity, updateNote, updateBranch, updateNoteRevision, From 36b15f474da8c2941a0bead885aa28522cafa93b Mon Sep 17 00:00:00 2001 From: azivner Date: Sat, 7 Apr 2018 22:32:46 -0400 Subject: [PATCH 04/18] sync cleanup --- src/routes/api/sync.js | 129 ------------------------------------ src/routes/routes.js | 20 ------ src/services/sync_update.js | 12 +--- 3 files changed, 1 insertion(+), 160 deletions(-) diff --git a/src/routes/api/sync.js b/src/routes/api/sync.js index d67b4f5a0..725626b9f 100644 --- a/src/routes/api/sync.js +++ b/src/routes/api/sync.js @@ -72,116 +72,6 @@ async function update(req) { } } -async function getNote(req) { - const noteId = req.params.noteId; - const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); - - syncService.serializeNoteContentBuffer(entity); - - return { - entity: entity - }; -} - -async function getBranch(req) { - const branchId = req.params.branchId; - - return await sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]); -} - -async function getNoteRevision(req) { - const noteRevisionId = req.params.noteRevisionId; - - return await sql.getRow("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]); -} - -async function getOption(req) { - const name = req.params.name; - const opt = await sql.getRow("SELECT * FROM options WHERE name = ?", [name]); - - if (!opt.isSynced) { - return [400, "This option can't be synced."]; - } - else { - return opt; - } -} - -async function getRecentNote(req) { - const branchId = req.params.branchId; - - return await sql.getRow("SELECT * FROM recent_notes WHERE branchId = ?", [branchId]); -} - -async function getImage(req) { - const imageId = req.params.imageId; - const entity = await sql.getRow("SELECT * FROM images WHERE imageId = ?", [imageId]); - - if (entity && entity.data !== null) { - entity.data = entity.data.toString('base64'); - } - - return entity; -} - -async function getNoteImage(req) { - const noteImageId = req.params.noteImageId; - - return await sql.getRow("SELECT * FROM note_images WHERE noteImageId = ?", [noteImageId]); -} - -async function getLabel(req) { - const labelId = req.params.labelId; - - return await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [labelId]); -} - -async function getApiToken(req) { - const apiTokenId = req.params.apiTokenId; - - return await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [apiTokenId]); -} - -async function updateNote(req) { - await syncUpdateService.updateNote(req.body.entity, req.body.sourceId); -} - -async function updateBranch(req) { - await syncUpdateService.updateBranch(req.body.entity, req.body.sourceId); -} - -async function updateNoteRevision(req) { - await syncUpdateService.updateNoteRevision(req.body.entity, req.body.sourceId); -} - -async function updateNoteReordering(req) { - await syncUpdateService.updateNoteReordering(req.body.entity, req.body.sourceId); -} - -async function updateOption(req) { - await syncUpdateService.updateOptions(req.body.entity, req.body.sourceId); -} - -async function updateRecentNote(req) { - await syncUpdateService.updateRecentNotes(req.body.entity, req.body.sourceId); -} - -async function updateImage(req) { - await syncUpdateService.updateImage(req.body.entity, req.body.sourceId); -} - -async function updateNoteImage(req) { - await syncUpdateService.updateNoteImage(req.body.entity, req.body.sourceId); -} - -async function updateLabel(req) { - await syncUpdateService.updateLabel(req.body.entity, req.body.sourceId); -} - -async function updateApiToken(req) { - await syncUpdateService.updateApiToken(req.body.entity, req.body.sourceId); -} - module.exports = { checkSync, syncNow, @@ -189,24 +79,5 @@ module.exports = { forceFullSync, forceNoteSync, getChanged, - getNote, - getBranch, - getImage, - getNoteImage, - getNoteRevision, - getRecentNote, - getOption, - getLabel, - getApiToken, - updateNote, - updateBranch, - updateImage, - updateNoteImage, - updateNoteReordering, - updateNoteRevision, - updateRecentNote, - updateOption, - updateLabel, - updateApiToken, update }; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index 83e58bed1..db956aab7 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -148,26 +148,6 @@ function register(app) { apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged); apiRoute(PUT, '/api/sync/update', syncApiRoute.update); - apiRoute(GET, '/api/sync/notes/:noteId', syncApiRoute.getNote); - apiRoute(GET, '/api/sync/branches/:branchId', syncApiRoute.getBranch); - apiRoute(GET, '/api/sync/note_revisions/:noteRevisionId', syncApiRoute.getNoteRevision); - apiRoute(GET, '/api/sync/options/:name', syncApiRoute.getOption); - apiRoute(GET, '/api/sync/note_reordering/:parentNoteId', syncApiRoute.getNoteReordering); - apiRoute(GET, '/api/sync/recent_notes/:branchId', syncApiRoute.getRecentNote); - apiRoute(GET, '/api/sync/images/:imageId', syncApiRoute.getImage); - apiRoute(GET, '/api/sync/note_images/:noteImageId', syncApiRoute.getNoteImage); - apiRoute(GET, '/api/sync/labels/:labelId', syncApiRoute.getLabel); - apiRoute(GET, '/api/sync/api_tokens/:apiTokenId', syncApiRoute.getApiToken); - apiRoute(PUT, '/api/sync/notes', syncApiRoute.updateNote); - apiRoute(PUT, '/api/sync/branches', syncApiRoute.updateBranch); - apiRoute(PUT, '/api/sync/note_revisions', syncApiRoute.updateNoteRevision); - apiRoute(PUT, '/api/sync/note_reordering', syncApiRoute.updateNoteReordering); - apiRoute(PUT, '/api/sync/options', syncApiRoute.updateOption); - apiRoute(PUT, '/api/sync/recent_notes', syncApiRoute.updateRecentNote); - apiRoute(PUT, '/api/sync/images', syncApiRoute.updateImage); - apiRoute(PUT, '/api/sync/note_images', syncApiRoute.updateNoteImage); - apiRoute(PUT, '/api/sync/labels', syncApiRoute.updateLabel); - apiRoute(PUT, '/api/sync/api_tokens', syncApiRoute.updateApiToken); apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog); diff --git a/src/services/sync_update.js b/src/services/sync_update.js index 2858561a5..46cac889a 100644 --- a/src/services/sync_update.js +++ b/src/services/sync_update.js @@ -195,15 +195,5 @@ async function updateApiToken(entity, sourceId) { } module.exports = { - updateEntity, - updateNote, - updateBranch, - updateNoteRevision, - updateNoteReordering, - updateOptions, - updateRecentNotes, - updateImage, - updateNoteImage, - updateLabel, - updateApiToken + updateEntity }; \ No newline at end of file From 982796255dbcee8028b93646452354e3624466ca Mon Sep 17 00:00:00 2001 From: azivner Date: Sat, 7 Apr 2018 22:59:47 -0400 Subject: [PATCH 05/18] sync content check refactoring --- src/services/content_hash.js | 29 ++++++++++++++++++++++- src/services/sync.js | 46 ++++++------------------------------ 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/services/content_hash.js b/src/services/content_hash.js index a9b114875..f8384f35b 100644 --- a/src/services/content_hash.js +++ b/src/services/content_hash.js @@ -1,6 +1,10 @@ +"use strict"; + const sql = require('./sql'); const utils = require('./utils'); const log = require('./log'); +const eventLogService = require('./event_log'); +const messagingService = require('./messaging'); function getHash(rows) { let hash = ''; @@ -121,6 +125,29 @@ async function getHashes() { return hashes; } +async function checkContentHashes(otherHashes) { + const hashes = await getHashes(); + let allChecksPassed = true; + + for (const key in hashes) { + if (hashes[key] !== otherHashes[key]) { + allChecksPassed = false; + + await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${resp.hashes[key]}`); + + if (key !== 'recent_notes') { + // let's not get alarmed about recent notes which get updated often and can cause failures in race conditions + await messagingService.sendMessageToAllClients({type: 'sync-hash-check-failed'}); + } + } + } + + if (allChecksPassed) { + log.info("Content hash checks PASSED"); + } +} + module.exports = { - getHashes + getHashes, + checkContentHashes }; \ No newline at end of file diff --git a/src/services/sync.js b/src/services/sync.js index 685451a28..92e4bacc3 100644 --- a/src/services/sync.js +++ b/src/services/sync.js @@ -10,10 +10,8 @@ const sourceIdService = require('./source_id'); const dateUtils = require('./date_utils'); const syncUpdateService = require('./sync_update'); const contentHashService = require('./content_hash'); -const eventLogService = require('./event_log'); const fs = require('fs'); const appInfo = require('./app_info'); -const messagingService = require('./messaging'); const syncSetup = require('./sync_setup'); const syncMutexService = require('./sync_mutex'); const cls = require('./cls'); @@ -183,12 +181,6 @@ async function pushSync(syncContext) { } } -function serializeNoteContentBuffer(note) { - if (note.type === 'file') { - note.content = note.content.toString("binary"); - } -} - async function checkContentHash(syncContext) { const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); @@ -207,25 +199,7 @@ async function checkContentHash(syncContext) { return; } - const hashes = await contentHashService.getHashes(); - let allChecksPassed = true; - - for (const key in hashes) { - if (hashes[key] !== resp.hashes[key]) { - allChecksPassed = false; - - await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${resp.hashes[key]}`); - - if (key !== 'recent_notes') { - // let's not get alarmed about recent notes which get updated often and can cause failures in race conditions - await messagingService.sendMessageToAllClients({type: 'sync-hash-check-failed'}); - } - } - } - - if (allChecksPassed) { - log.info("Content hash checks PASSED"); - } + await contentHashService.checkContentHashes(resp.hashes); } async function syncRequest(syncContext, method, uri, body) { @@ -270,7 +244,7 @@ const primaryKeys = { async function getEntityRow(entityName, entityId) { if (entityName === 'note_reordering') { - return await getNoteReordering(entityId); + return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]); } else { const primaryKey = primaryKeys[entityName]; @@ -279,16 +253,16 @@ async function getEntityRow(entityName, entityId) { throw new Error("Unknown entity " + entityName); } - const entityRow = await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); + const entity = await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); - if (entityName === 'notes') { - serializeNoteContentBuffer(entityRow); + if (entityName === 'notes' && entity.type === 'file') { + entity.content = entity.content.toString("binary"); } else if (entityName === 'images') { - entityRow.data = entityRow.data.toString('base64'); + entity.data = entity.data.toString('base64'); } - return entityRow; + return entity; } } @@ -314,10 +288,6 @@ async function getSyncRecords(syncs) { return records; } -async function getNoteReordering(parentNoteId) { - return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]) -} - sqlInit.dbReady.then(() => { if (syncSetup.isSyncSetup) { log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT); @@ -344,7 +314,5 @@ sqlInit.dbReady.then(() => { module.exports = { sync, - serializeNoteContentBuffer, - getEntityRow, getSyncRecords }; \ No newline at end of file From 6128bb4ff31db9db7f7dae19f2947e8b8eca8675 Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 8 Apr 2018 07:48:47 -0400 Subject: [PATCH 06/18] fix showMessage, showError --- src/public/javascripts/services/script_api.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/public/javascripts/services/script_api.js b/src/public/javascripts/services/script_api.js index 4e5ed46a8..dc1523d73 100644 --- a/src/public/javascripts/services/script_api.js +++ b/src/public/javascripts/services/script_api.js @@ -1,6 +1,7 @@ import treeService from './tree.js'; import server from './server.js'; import utils from './utils.js'; +import infoService from './info.js'; function ScriptApi(startNote, currentNote) { const $pluginButtons = $("#plugin-buttons"); @@ -56,8 +57,8 @@ function ScriptApi(startNote, currentNote) { runOnServer, formatDateISO: utils.formatDateISO, parseDate: utils.parseDate, - showMessage: utils.showMessage, - showError: utils.showError, + showMessage: infoService.showMessage, + showError: infoService.showError, reloadTree: treeService.reload } } From d2e2caed621ef4d79e43ae508eb975f5a8c2f227 Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 8 Apr 2018 08:21:49 -0400 Subject: [PATCH 07/18] refactoring of note saving code & API --- src/public/javascripts/entities/note_short.js | 8 +++ .../javascripts/services/note_detail.js | 51 ++++++++----------- .../javascripts/services/protected_session.js | 10 +--- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/public/javascripts/entities/note_short.js b/src/public/javascripts/entities/note_short.js index 8d10a7c9b..6a831111b 100644 --- a/src/public/javascripts/entities/note_short.js +++ b/src/public/javascripts/entities/note_short.js @@ -44,6 +44,14 @@ class NoteShort { get toString() { return `Note(noteId=${this.noteId}, title=${this.title})`; } + + get dto() { + const dto = Object.assign({}, this); + delete dto.treeCache; + delete dto.hideInAutocomplete; + + return dto; + } } export default NoteShort; \ No newline at end of file diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index 33d45cf4b..71bdc9f28 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -84,39 +84,31 @@ async function switchToNote(noteId) { } } +async function saveNote() { + const note = getCurrentNote(); + + note.title = $noteTitle.val(); + note.content = getComponent(note.type).getContent(); + + treeService.setNoteTitle(note.noteId, note.title); + + await server.put('notes/' + note.noteId, note.dto); + + isNoteChanged = false; + + if (note.isProtected) { + protectedSessionHolder.touchProtectedSession(); + } + + infoService.showMessage("Saved!"); +} + async function saveNoteIfChanged() { if (!isNoteChanged) { return; } - const note = getCurrentNote(); - - updateNoteFromInputs(note); - - await saveNoteToServer(note); - - if (note.isProtected) { - protectedSessionHolder.touchProtectedSession(); - } -} - -function updateNoteFromInputs(note) { - note.title = $noteTitle.val(); - note.content = getComponent(note.type).getContent(); - - treeService.setNoteTitle(note.noteId, note.title); -} - -async function saveNoteToServer(note) { - const dto = Object.assign({}, note); - delete dto.treeCache; - delete dto.hideInAutocomplete; - - await server.put('notes/' + dto.noteId, dto); - - isNoteChanged = false; - - infoService.showMessage("Saved!"); + await saveNote(); } function setNoteBackgroundIfProtected(note) { @@ -245,8 +237,6 @@ setInterval(saveNoteIfChanged, 5000); export default { reload, switchToNote, - updateNoteFromInputs, - saveNoteToServer, setNoteBackgroundIfProtected, loadNote, getCurrentNote, @@ -255,6 +245,7 @@ export default { newNoteCreated, focus, loadLabelList, + saveNote, saveNoteIfChanged, noteChanged }; \ No newline at end of file diff --git a/src/public/javascripts/services/protected_session.js b/src/public/javascripts/services/protected_session.js index 36306bc2c..b911f6138 100644 --- a/src/public/javascripts/services/protected_session.js +++ b/src/public/javascripts/services/protected_session.js @@ -91,12 +91,9 @@ async function protectNoteAndSendToServer() { await ensureProtectedSession(true, true); const note = noteDetail.getCurrentNote(); - - noteDetail.updateNoteFromInputs(note); - note.isProtected = true; - await noteDetail.saveNoteToServer(note); + await noteDetail.saveNote(note); treeService.setProtected(note.noteId, note.isProtected); @@ -107,12 +104,9 @@ async function unprotectNoteAndSendToServer() { await ensureProtectedSession(true, true); const note = noteDetail.getCurrentNote(); - - noteDetail.updateNoteFromInputs(note); - note.isProtected = false; - await noteDetail.saveNoteToServer(note); + await noteDetail.saveNote(note); treeService.setProtected(note.noteId, note.isProtected); From 120888b53e39468935954eab9ab286c69989a765 Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 8 Apr 2018 08:31:19 -0400 Subject: [PATCH 08/18] fix JSON saving bug --- src/entities/note.js | 8 ++++++++ src/services/notes.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/entities/note.js b/src/entities/note.js index c11833667..2b5e9e7b7 100644 --- a/src/entities/note.js +++ b/src/entities/note.js @@ -21,6 +21,14 @@ class Note extends Entity { } } + setContent(content) { + this.content = content; + + if (this.isJson()) { + this.jsonContent = JSON.parse(this.content); + } + } + isJson() { return this.mime === "application/json"; } diff --git a/src/services/notes.js b/src/services/notes.js index eaa338df2..440a05bb0 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -198,7 +198,7 @@ async function updateNote(noteId, noteUpdates) { await saveNoteRevision(note); note.title = noteUpdates.title; - note.content = noteUpdates.content; + note.setContent(noteUpdates.content); note.isProtected = noteUpdates.isProtected; await note.save(); From 0104b1950208f849238eac801dd1e0d2573d6c1e Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 8 Apr 2018 09:25:35 -0400 Subject: [PATCH 09/18] naming standards --- src/public/javascripts/services/note_type.js | 6 +++--- .../javascripts/services/protected_session.js | 18 +++++++++--------- src/public/javascripts/services/tree.js | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/public/javascripts/services/note_type.js b/src/public/javascripts/services/note_type.js index 83c5dfe92..34922c2c9 100644 --- a/src/public/javascripts/services/note_type.js +++ b/src/public/javascripts/services/note_type.js @@ -1,5 +1,5 @@ import treeService from './tree.js'; -import noteDetail from './note_detail.js'; +import noteDetailService from './note_detail.js'; import server from './server.js'; import infoService from "./info.js"; @@ -84,13 +84,13 @@ function NoteTypeModel() { }; async function save() { - const note = noteDetail.getCurrentNote(); + const note = noteDetailService.getCurrentNote(); await server.put('notes/' + note.noteId + '/type/' + encodeURIComponent(self.type()) + '/mime/' + encodeURIComponent(self.mime())); - await noteDetail.reload(); + await noteDetailService.reload(); // for the note icon to be updated in the tree await treeService.reload(); diff --git a/src/public/javascripts/services/protected_session.js b/src/public/javascripts/services/protected_session.js index b911f6138..399742250 100644 --- a/src/public/javascripts/services/protected_session.js +++ b/src/public/javascripts/services/protected_session.js @@ -1,5 +1,5 @@ import treeService from './tree.js'; -import noteDetail from './note_detail.js'; +import noteDetailService from './note_detail.js'; import utils from './utils.js'; import server from './server.js'; import protectedSessionHolder from './protected_session_holder.js'; @@ -57,7 +57,7 @@ async function setupProtectedSession() { $dialog.dialog("close"); - noteDetail.reload(); + noteDetailService.reload(); treeService.reload(); if (protectedSessionDeferred !== null) { @@ -90,27 +90,27 @@ async function enterProtectedSession(password) { async function protectNoteAndSendToServer() { await ensureProtectedSession(true, true); - const note = noteDetail.getCurrentNote(); + const note = noteDetailService.getCurrentNote(); note.isProtected = true; - await noteDetail.saveNote(note); + await noteDetailService.saveNote(note); treeService.setProtected(note.noteId, note.isProtected); - noteDetail.setNoteBackgroundIfProtected(note); + noteDetailService.setNoteBackgroundIfProtected(note); } async function unprotectNoteAndSendToServer() { await ensureProtectedSession(true, true); - const note = noteDetail.getCurrentNote(); + const note = noteDetailService.getCurrentNote(); note.isProtected = false; - await noteDetail.saveNote(note); + await noteDetailService.saveNote(note); treeService.setProtected(note.noteId, note.isProtected); - noteDetail.setNoteBackgroundIfProtected(note); + noteDetailService.setNoteBackgroundIfProtected(note); } async function protectBranch(noteId, protect) { @@ -121,7 +121,7 @@ async function protectBranch(noteId, protect) { infoService.showMessage("Request to un/protect sub tree has finished successfully"); treeService.reload(); - noteDetail.reload(); + noteDetailService.reload(); } $passwordForm.submit(() => { diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 751a8d3fb..9a47262ee 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -293,7 +293,7 @@ function initFancyTree(branch) { keyboard: false, // we takover keyboard handling in the hotkeys plugin extensions: ["hotkeys", "filter", "dnd", "clones"], source: branch, - scrollParent: $("#tree"), + scrollParent: $tree, click: (event, data) => { const targetType = data.targetType; const node = data.node; From 542e82ee5da9dac444d39f0e3e61bb181a0a860a Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 8 Apr 2018 09:40:28 -0400 Subject: [PATCH 10/18] upgraded uncompressed jquery --- src/public/libraries/jquery.js | 799 +++++++++++++++++++-------------- 1 file changed, 455 insertions(+), 344 deletions(-) diff --git a/src/public/libraries/jquery.js b/src/public/libraries/jquery.js index d2d8ca479..9b5206bcc 100644 --- a/src/public/libraries/jquery.js +++ b/src/public/libraries/jquery.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v3.2.1 + * jQuery JavaScript Library v3.3.1 * https://jquery.com/ * * Includes Sizzle.js @@ -9,7 +9,7 @@ * Released under the MIT license * https://jquery.org/license * - * Date: 2017-03-20T18:59Z + * Date: 2018-01-20T17:24Z */ ( function( global, factory ) { @@ -71,16 +71,57 @@ var ObjectFunctionString = fnToString.call( Object ); var support = {}; +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + return typeof obj === "function" && typeof obj.nodeType !== "number"; + }; - function DOMEval( code, doc ) { +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + + + + var preservedScriptAttributes = { + type: true, + src: true, + noModule: true + }; + + function DOMEval( code, doc, node ) { doc = doc || document; - var script = doc.createElement( "script" ); + var i, + script = doc.createElement( "script" ); script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + if ( node[ i ] ) { + script[ i ] = node[ i ]; + } + } + } doc.head.appendChild( script ).parentNode.removeChild( script ); } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} /* global Symbol */ // Defining this global in .eslintrc.json would create a danger of using the global // unguarded in another place, it seems safer to define global only for this module @@ -88,7 +129,7 @@ var support = {}; var - version = "3.2.1", + version = "3.3.1", // Define a local copy of jQuery jQuery = function( selector, context ) { @@ -100,16 +141,7 @@ var // Support: Android <=4.0 only // Make sure we trim BOM and NBSP - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([a-z])/g, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }; + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; jQuery.fn = jQuery.prototype = { @@ -209,7 +241,7 @@ jQuery.extend = jQuery.fn.extend = function() { } // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + if ( typeof target !== "object" && !isFunction( target ) ) { target = {}; } @@ -275,28 +307,6 @@ jQuery.extend( { noop: function() {}, - isFunction: function( obj ) { - return jQuery.type( obj ) === "function"; - }, - - isWindow: function( obj ) { - return obj != null && obj === obj.window; - }, - - isNumeric: function( obj ) { - - // As of jQuery 3.0, isNumeric is limited to - // strings and numbers (primitives or objects) - // that can be coerced to finite numbers (gh-2662) - var type = jQuery.type( obj ); - return ( type === "number" || type === "string" ) && - - // parseFloat NaNs numeric-cast false positives ("") - // ...but misinterprets leading-number strings, particularly hex literals ("0x...") - // subtraction forces infinities to NaN - !isNaN( obj - parseFloat( obj ) ); - }, - isPlainObject: function( obj ) { var proto, Ctor; @@ -330,29 +340,11 @@ jQuery.extend( { return true; }, - type: function( obj ) { - if ( obj == null ) { - return obj + ""; - } - - // Support: Android <=2.3 only (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; - }, - // Evaluates a script in a global context globalEval: function( code ) { DOMEval( code ); }, - // Convert dashed to camelCase; used by the css and data modules - // Support: IE <=9 - 11, Edge 12 - 13 - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - each: function( obj, callback ) { var length, i = 0; @@ -473,37 +465,6 @@ jQuery.extend( { // A global GUID counter for objects guid: 1, - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - var tmp, args, proxy; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - now: Date.now, - // jQuery.support is not used in Core but other projects attach their // properties to it so it needs to exist. support: support @@ -526,9 +487,9 @@ function isArrayLike( obj ) { // hasOwn isn't used here due to false negatives // regarding Nodelist length in IE var length = !!obj && "length" in obj && obj.length, - type = jQuery.type( obj ); + type = toType( obj ); - if ( type === "function" || jQuery.isWindow( obj ) ) { + if ( isFunction( obj ) || isWindow( obj ) ) { return false; } @@ -2848,11 +2809,9 @@ var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>| -var risSimple = /^.[^:#\[\.,]*$/; - // Implement the identical functionality for filter and not function winnow( elements, qualifier, not ) { - if ( jQuery.isFunction( qualifier ) ) { + if ( isFunction( qualifier ) ) { return jQuery.grep( elements, function( elem, i ) { return !!qualifier.call( elem, i, elem ) !== not; } ); @@ -2872,16 +2831,8 @@ function winnow( elements, qualifier, not ) { } ); } - // Simple selector that can be filtered directly, removing non-Elements - if ( risSimple.test( qualifier ) ) { - return jQuery.filter( qualifier, elements, not ); - } - - // Complex selector, compare the two sets, removing non-Elements - qualifier = jQuery.filter( qualifier, elements ); - return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) > -1 ) !== not && elem.nodeType === 1; - } ); + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); } jQuery.filter = function( expr, elems, not ) { @@ -3002,7 +2953,7 @@ var rootjQuery, for ( match in context ) { // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { + if ( isFunction( this[ match ] ) ) { this[ match ]( context[ match ] ); // ...and otherwise set as attributes @@ -3045,7 +2996,7 @@ var rootjQuery, // HANDLE: $(function) // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { + } else if ( isFunction( selector ) ) { return root.ready !== undefined ? root.ready( selector ) : @@ -3360,11 +3311,11 @@ jQuery.Callbacks = function( options ) { ( function add( args ) { jQuery.each( args, function( _, arg ) { - if ( jQuery.isFunction( arg ) ) { + if ( isFunction( arg ) ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } - } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + } else if ( arg && arg.length && toType( arg ) !== "string" ) { // Inspect recursively add( arg ); @@ -3479,11 +3430,11 @@ function adoptValue( value, resolve, reject, noValue ) { try { // Check for promise aspect first to privilege synchronous behavior - if ( value && jQuery.isFunction( ( method = value.promise ) ) ) { + if ( value && isFunction( ( method = value.promise ) ) ) { method.call( value ).done( resolve ).fail( reject ); // Other thenables - } else if ( value && jQuery.isFunction( ( method = value.then ) ) ) { + } else if ( value && isFunction( ( method = value.then ) ) ) { method.call( value, resolve, reject ); // Other non-thenables @@ -3541,14 +3492,14 @@ jQuery.extend( { jQuery.each( tuples, function( i, tuple ) { // Map tuples (progress, done, fail) to arguments (done, fail, progress) - var fn = jQuery.isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; // deferred.progress(function() { bind to newDefer or newDefer.notify }) // deferred.done(function() { bind to newDefer or newDefer.resolve }) // deferred.fail(function() { bind to newDefer or newDefer.reject }) deferred[ tuple[ 1 ] ]( function() { var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { + if ( returned && isFunction( returned.promise ) ) { returned.promise() .progress( newDefer.notify ) .done( newDefer.resolve ) @@ -3602,7 +3553,7 @@ jQuery.extend( { returned.then; // Handle a returned thenable - if ( jQuery.isFunction( then ) ) { + if ( isFunction( then ) ) { // Special processors (notify) just wait for resolution if ( special ) { @@ -3698,7 +3649,7 @@ jQuery.extend( { resolve( 0, newDefer, - jQuery.isFunction( onProgress ) ? + isFunction( onProgress ) ? onProgress : Identity, newDefer.notifyWith @@ -3710,7 +3661,7 @@ jQuery.extend( { resolve( 0, newDefer, - jQuery.isFunction( onFulfilled ) ? + isFunction( onFulfilled ) ? onFulfilled : Identity ) @@ -3721,7 +3672,7 @@ jQuery.extend( { resolve( 0, newDefer, - jQuery.isFunction( onRejected ) ? + isFunction( onRejected ) ? onRejected : Thrower ) @@ -3761,8 +3712,15 @@ jQuery.extend( { // fulfilled_callbacks.disable tuples[ 3 - i ][ 2 ].disable, + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + // progress_callbacks.lock - tuples[ 0 ][ 2 ].lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock ); } @@ -3832,7 +3790,7 @@ jQuery.extend( { // Use .then() to unwrap secondary thenables (cf. gh-3000) if ( master.state() === "pending" || - jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { return master.then(); } @@ -3960,7 +3918,7 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { bulk = key == null; // Sets many values - if ( jQuery.type( key ) === "object" ) { + if ( toType( key ) === "object" ) { chainable = true; for ( i in key ) { access( elems, fn, i, key[ i ], true, emptyGet, raw ); @@ -3970,7 +3928,7 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { } else if ( value !== undefined ) { chainable = true; - if ( !jQuery.isFunction( value ) ) { + if ( !isFunction( value ) ) { raw = true; } @@ -4012,6 +3970,23 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { return len ? fn( elems[ 0 ], key ) : emptyGet; }; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (#9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} var acceptData = function( owner ) { // Accepts only: @@ -4074,14 +4049,14 @@ Data.prototype = { // Handle: [ owner, key, value ] args // Always use camelCase key (gh-2257) if ( typeof data === "string" ) { - cache[ jQuery.camelCase( data ) ] = value; + cache[ camelCase( data ) ] = value; // Handle: [ owner, { properties } ] args } else { // Copy the properties one-by-one to the cache object for ( prop in data ) { - cache[ jQuery.camelCase( prop ) ] = data[ prop ]; + cache[ camelCase( prop ) ] = data[ prop ]; } } return cache; @@ -4091,7 +4066,7 @@ Data.prototype = { this.cache( owner ) : // Always use camelCase key (gh-2257) - owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ]; + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; }, access: function( owner, key, value ) { @@ -4139,9 +4114,9 @@ Data.prototype = { // If key is an array of keys... // We always set camelCase keys, so remove that. - key = key.map( jQuery.camelCase ); + key = key.map( camelCase ); } else { - key = jQuery.camelCase( key ); + key = camelCase( key ); // If a key with the spaces exists, use it. // Otherwise, create an array by matching non-whitespace @@ -4287,7 +4262,7 @@ jQuery.fn.extend( { if ( attrs[ i ] ) { name = attrs[ i ].name; if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.slice( 5 ) ); + name = camelCase( name.slice( 5 ) ); dataAttr( elem, name, data[ name ] ); } } @@ -4534,8 +4509,7 @@ var swap = function( elem, options, callback, args ) { function adjustCSS( elem, prop, valueParts, tween ) { - var adjusted, - scale = 1, + var adjusted, scale, maxIterations = 20, currentValue = tween ? function() { @@ -4553,30 +4527,33 @@ function adjustCSS( elem, prop, valueParts, tween ) { if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + // Trust units reported by jQuery.css unit = unit || initialInUnit[ 3 ]; - // Make sure we update the tween properties later on - valueParts = valueParts || []; - // Iteratively approximate from a nonzero starting point initialInUnit = +initial || 1; - do { + while ( maxIterations-- ) { - // If previous iteration zeroed out, double until we get *something*. - // Use string for doubling so we don't accidentally see scale as unchanged below - scale = scale || ".5"; - - // Adjust and apply - initialInUnit = initialInUnit / scale; + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; - // Update scale, tolerating zero or NaN from tween.cur() - // Break the loop if scale is unchanged or perfect, or if we've just had enough. - } while ( - scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations - ); + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; } if ( valueParts ) { @@ -4694,7 +4671,7 @@ var rcheckableType = ( /^(?:checkbox|radio)$/i ); var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i ); -var rscriptType = ( /^$|\/(?:java|ecma)script/i ); +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); @@ -4776,7 +4753,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) { if ( elem || elem === 0 ) { // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { + if ( toType( elem ) === "object" ) { // Support: Android <=4.0 only, PhantomJS 1 only // push.apply(_, arraylike) throws on ancient WebKit @@ -5286,7 +5263,7 @@ jQuery.event = { enumerable: true, configurable: true, - get: jQuery.isFunction( hook ) ? + get: isFunction( hook ) ? function() { if ( this.originalEvent ) { return hook( this.originalEvent ); @@ -5421,7 +5398,7 @@ jQuery.Event = function( src, props ) { } // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); + this.timeStamp = src && src.timeStamp || Date.now(); // Mark it as fixed this[ jQuery.expando ] = true; @@ -5620,14 +5597,13 @@ var /* eslint-enable */ - // Support: IE <=10 - 11, Edge 12 - 13 + // Support: IE <=10 - 11, Edge 12 - 13 only // In IE/Edge using regex groups here causes severe slowdowns. // See https://connect.microsoft.com/IE/feedback/details/1736512/ rnoInnerhtml = /\s*$/g; // Prefer a tbody over its parent table for containing new rows @@ -5635,7 +5611,7 @@ function manipulationTarget( elem, content ) { if ( nodeName( elem, "table" ) && nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - return jQuery( ">tbody", elem )[ 0 ] || elem; + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; } return elem; @@ -5647,10 +5623,8 @@ function disableScript( elem ) { return elem; } function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - - if ( match ) { - elem.type = match[ 1 ]; + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); } else { elem.removeAttribute( "type" ); } @@ -5716,15 +5690,15 @@ function domManip( collection, args, callback, ignored ) { l = collection.length, iNoClone = l - 1, value = args[ 0 ], - isFunction = jQuery.isFunction( value ); + valueIsFunction = isFunction( value ); // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || + if ( valueIsFunction || ( l > 1 && typeof value === "string" && !support.checkClone && rchecked.test( value ) ) ) { return collection.each( function( index ) { var self = collection.eq( index ); - if ( isFunction ) { + if ( valueIsFunction ) { args[ 0 ] = value.call( this, index, self.html() ); } domManip( self, args, callback, ignored ); @@ -5778,14 +5752,14 @@ function domManip( collection, args, callback, ignored ) { !dataPriv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { - if ( node.src ) { + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { // Optional AJAX dependency, but won't run scripts if not present if ( jQuery._evalUrl ) { jQuery._evalUrl( node.src ); } } else { - DOMEval( node.textContent.replace( rcleanScript, "" ), doc ); + DOMEval( node.textContent.replace( rcleanScript, "" ), doc, node ); } } } @@ -6065,8 +6039,6 @@ jQuery.each( { return this.pushStack( ret ); }; } ); -var rmargin = ( /^margin/ ); - var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); var getStyles = function( elem ) { @@ -6083,6 +6055,8 @@ var getStyles = function( elem ) { return view.getComputedStyle( elem ); }; +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + ( function() { @@ -6096,25 +6070,33 @@ var getStyles = function( elem ) { return; } + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; div.style.cssText = - "box-sizing:border-box;" + - "position:relative;display:block;" + + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + "margin:auto;border:1px;padding:1px;" + - "top:1%;width:50%"; - div.innerHTML = ""; - documentElement.appendChild( container ); + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); var divStyle = window.getComputedStyle( div ); pixelPositionVal = divStyle.top !== "1%"; // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = divStyle.marginLeft === "2px"; - boxSizingReliableVal = divStyle.width === "4px"; + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; - // Support: Android 4.0 - 4.3 only + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 // Some styles come back with percentage values, even though they shouldn't - div.style.marginRight = "50%"; - pixelMarginRightVal = divStyle.marginRight === "4px"; + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + div.style.position = "absolute"; + scrollboxSizeVal = div.offsetWidth === 36 || "absolute"; documentElement.removeChild( container ); @@ -6123,7 +6105,12 @@ var getStyles = function( elem ) { div = null; } - var pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal, + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableMarginLeftVal, container = document.createElement( "div" ), div = document.createElement( "div" ); @@ -6138,26 +6125,26 @@ var getStyles = function( elem ) { div.cloneNode( true ).style.backgroundClip = ""; support.clearCloneStyle = div.style.backgroundClip === "content-box"; - container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" + - "padding:0;margin-top:1px;position:absolute"; - container.appendChild( div ); - jQuery.extend( support, { - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, boxSizingReliable: function() { computeStyleTests(); return boxSizingReliableVal; }, - pixelMarginRight: function() { + pixelBoxStyles: function() { computeStyleTests(); - return pixelMarginRightVal; + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; }, reliableMarginLeft: function() { computeStyleTests(); return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; } } ); } )(); @@ -6189,7 +6176,7 @@ function curCSS( elem, name, computed ) { // but width seems to be reliably pixels. // This is against the CSSOM draft spec: // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) { + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { // Remember the original values width = style.width; @@ -6294,87 +6281,120 @@ function setPositiveNumber( elem, value, subtract ) { value; } -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i, - val = 0; +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; - // If we already have the right measurement, avoid augmentation - if ( extra === ( isBorderBox ? "border" : "content" ) ) { - i = 4; - - // Otherwise initialize for horizontal or vertical properties - } else { - i = name === "width" ? 1 : 0; + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; } for ( ; i < 4; i += 2 ) { - // Both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); } - if ( isBorderBox ) { + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } - // At this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" } else { - // At this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } - // At this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } } } - return val; + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + ) ); + } + + return delta; } -function getWidthOrHeight( elem, name, extra ) { +function getWidthOrHeight( elem, dimension, extra ) { // Start with computed style - var valueIsBorderBox, - styles = getStyles( elem ), - val = curCSS( elem, name, styles ), - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + var styles = getStyles( elem ), + val = curCSS( elem, dimension, styles ), + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox; - // Computed unit is not pixels. Stop here and return. + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. if ( rnumnonpx.test( val ) ) { - return val; + if ( !extra ) { + return val; + } + val = "auto"; } // Check for style in case a browser which returns unreliable values // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && - ( support.boxSizingReliable() || val === elem.style[ name ] ); + valueIsBorderBox = valueIsBorderBox && + ( support.boxSizingReliable() || val === elem.style[ dimension ] ); - // Fall back to offsetWidth/Height when value is "auto" + // Fall back to offsetWidth/offsetHeight when value is "auto" // This happens for inline elements with no explicit setting (gh-3571) - if ( val === "auto" ) { - val = elem[ "offset" + name[ 0 ].toUpperCase() + name.slice( 1 ) ]; + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + if ( val === "auto" || + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) { + + val = elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ]; + + // offsetWidth/offsetHeight provide border-box values + valueIsBorderBox = true; } - // Normalize "", auto, and prepare for extra + // Normalize "" and auto val = parseFloat( val ) || 0; - // Use the active box-sizing model to add/subtract irrelevant styles + // Adjust for the element's box model return ( val + - augmentWidthOrHeight( + boxModelAdjustment( elem, - name, + dimension, extra || ( isBorderBox ? "border" : "content" ), valueIsBorderBox, - styles + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val ) ) + "px"; } @@ -6415,9 +6435,7 @@ jQuery.extend( { // Add in properties whose names you wish to fix before // setting or getting the value - cssProps: { - "float": "cssFloat" - }, + cssProps: {}, // Get and set the style property on a DOM Node style: function( elem, name, value, extra ) { @@ -6429,7 +6447,7 @@ jQuery.extend( { // Make sure that we're working with the right name var ret, type, hooks, - origName = jQuery.camelCase( name ), + origName = camelCase( name ), isCustomProp = rcustomProp.test( name ), style = elem.style; @@ -6497,7 +6515,7 @@ jQuery.extend( { css: function( elem, name, extra, styles ) { var val, num, hooks, - origName = jQuery.camelCase( name ), + origName = camelCase( name ), isCustomProp = rcustomProp.test( name ); // Make sure that we're working with the right name. We don't @@ -6535,8 +6553,8 @@ jQuery.extend( { } } ); -jQuery.each( [ "height", "width" ], function( i, name ) { - jQuery.cssHooks[ name ] = { +jQuery.each( [ "height", "width" ], function( i, dimension ) { + jQuery.cssHooks[ dimension ] = { get: function( elem, computed, extra ) { if ( computed ) { @@ -6552,29 +6570,41 @@ jQuery.each( [ "height", "width" ], function( i, name ) { // in IE throws an error. ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? swap( elem, cssShow, function() { - return getWidthOrHeight( elem, name, extra ); + return getWidthOrHeight( elem, dimension, extra ); } ) : - getWidthOrHeight( elem, name, extra ); + getWidthOrHeight( elem, dimension, extra ); } }, set: function( elem, value, extra ) { var matches, - styles = extra && getStyles( elem ), - subtract = extra && augmentWidthOrHeight( + styles = getStyles( elem ), + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra && boxModelAdjustment( elem, - name, + dimension, extra, - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + isBorderBox, styles ); + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && support.scrollboxSize() === styles.position ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + // Convert to pixels if value adjustment is needed if ( subtract && ( matches = rcssNum.exec( value ) ) && ( matches[ 3 ] || "px" ) !== "px" ) { - elem.style[ name ] = value; - value = jQuery.css( elem, name ); + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); } return setPositiveNumber( elem, value, subtract ); @@ -6618,7 +6648,7 @@ jQuery.each( { } }; - if ( !rmargin.test( prefix ) ) { + if ( prefix !== "margin" ) { jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; } } ); @@ -6789,7 +6819,7 @@ function createFxNow() { window.setTimeout( function() { fxNow = undefined; } ); - return ( fxNow = jQuery.now() ); + return ( fxNow = Date.now() ); } // Generate parameters to create a standard animation @@ -6893,9 +6923,10 @@ function defaultPrefilter( elem, props, opts ) { // Restrict "overflow" and "display" styles during box animations if ( isBox && elem.nodeType === 1 ) { - // Support: IE <=9 - 11, Edge 12 - 13 + // Support: IE <=9 - 11, Edge 12 - 15 // Record all 3 overflow attributes because IE does not infer the shorthand - // from identically-valued overflowX and overflowY + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; // Identify a display type, preferring old show/hide data over the CSS cascade @@ -7003,7 +7034,7 @@ function propFilter( props, specialEasing ) { // camelCase, specialEasing and expand cssHook pass for ( index in props ) { - name = jQuery.camelCase( index ); + name = camelCase( index ); easing = specialEasing[ name ]; value = props[ index ]; if ( Array.isArray( value ) ) { @@ -7128,9 +7159,9 @@ function Animation( elem, properties, options ) { for ( ; index < length; index++ ) { result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); if ( result ) { - if ( jQuery.isFunction( result.stop ) ) { + if ( isFunction( result.stop ) ) { jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = - jQuery.proxy( result.stop, result ); + result.stop.bind( result ); } return result; } @@ -7138,7 +7169,7 @@ function Animation( elem, properties, options ) { jQuery.map( props, createTween, animation ); - if ( jQuery.isFunction( animation.opts.start ) ) { + if ( isFunction( animation.opts.start ) ) { animation.opts.start.call( elem, animation ); } @@ -7171,7 +7202,7 @@ jQuery.Animation = jQuery.extend( Animation, { }, tweener: function( props, callback ) { - if ( jQuery.isFunction( props ) ) { + if ( isFunction( props ) ) { callback = props; props = [ "*" ]; } else { @@ -7203,9 +7234,9 @@ jQuery.Animation = jQuery.extend( Animation, { jQuery.speed = function( speed, easing, fn ) { var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { complete: fn || !fn && easing || - jQuery.isFunction( speed ) && speed, + isFunction( speed ) && speed, duration: speed, - easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + easing: fn && easing || easing && !isFunction( easing ) && easing }; // Go to the end state if fx are off @@ -7232,7 +7263,7 @@ jQuery.speed = function( speed, easing, fn ) { opt.old = opt.complete; opt.complete = function() { - if ( jQuery.isFunction( opt.old ) ) { + if ( isFunction( opt.old ) ) { opt.old.call( this ); } @@ -7396,7 +7427,7 @@ jQuery.fx.tick = function() { i = 0, timers = jQuery.timers; - fxNow = jQuery.now(); + fxNow = Date.now(); for ( ; i < timers.length; i++ ) { timer = timers[ i ]; @@ -7749,7 +7780,7 @@ jQuery.each( [ // Strip and collapse whitespace according to HTML spec - // https://html.spec.whatwg.org/multipage/infrastructure.html#strip-and-collapse-whitespace + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace function stripAndCollapse( value ) { var tokens = value.match( rnothtmlwhite ) || []; return tokens.join( " " ); @@ -7760,20 +7791,30 @@ function getClass( elem ) { return elem.getAttribute && elem.getAttribute( "class" ) || ""; } +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + jQuery.fn.extend( { addClass: function( value ) { var classes, elem, cur, curValue, clazz, j, finalValue, i = 0; - if ( jQuery.isFunction( value ) ) { + if ( isFunction( value ) ) { return this.each( function( j ) { jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); } ); } - if ( typeof value === "string" && value ) { - classes = value.match( rnothtmlwhite ) || []; + classes = classesToArray( value ); + if ( classes.length ) { while ( ( elem = this[ i++ ] ) ) { curValue = getClass( elem ); cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); @@ -7802,7 +7843,7 @@ jQuery.fn.extend( { var classes, elem, cur, curValue, clazz, j, finalValue, i = 0; - if ( jQuery.isFunction( value ) ) { + if ( isFunction( value ) ) { return this.each( function( j ) { jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); } ); @@ -7812,9 +7853,9 @@ jQuery.fn.extend( { return this.attr( "class", "" ); } - if ( typeof value === "string" && value ) { - classes = value.match( rnothtmlwhite ) || []; + classes = classesToArray( value ); + if ( classes.length ) { while ( ( elem = this[ i++ ] ) ) { curValue = getClass( elem ); @@ -7844,13 +7885,14 @@ jQuery.fn.extend( { }, toggleClass: function( value, stateVal ) { - var type = typeof value; + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); - if ( typeof stateVal === "boolean" && type === "string" ) { + if ( typeof stateVal === "boolean" && isValidValue ) { return stateVal ? this.addClass( value ) : this.removeClass( value ); } - if ( jQuery.isFunction( value ) ) { + if ( isFunction( value ) ) { return this.each( function( i ) { jQuery( this ).toggleClass( value.call( this, i, getClass( this ), stateVal ), @@ -7862,12 +7904,12 @@ jQuery.fn.extend( { return this.each( function() { var className, i, self, classNames; - if ( type === "string" ) { + if ( isValidValue ) { // Toggle individual class names i = 0; self = jQuery( this ); - classNames = value.match( rnothtmlwhite ) || []; + classNames = classesToArray( value ); while ( ( className = classNames[ i++ ] ) ) { @@ -7926,7 +7968,7 @@ var rreturn = /\r/g; jQuery.fn.extend( { val: function( value ) { - var hooks, ret, isFunction, + var hooks, ret, valueIsFunction, elem = this[ 0 ]; if ( !arguments.length ) { @@ -7955,7 +7997,7 @@ jQuery.fn.extend( { return; } - isFunction = jQuery.isFunction( value ); + valueIsFunction = isFunction( value ); return this.each( function( i ) { var val; @@ -7964,7 +8006,7 @@ jQuery.fn.extend( { return; } - if ( isFunction ) { + if ( valueIsFunction ) { val = value.call( this, i, jQuery( this ).val() ); } else { val = value; @@ -8106,18 +8148,24 @@ jQuery.each( [ "radio", "checkbox" ], function() { // Return jQuery for attributes-only inclusion -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/; +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; jQuery.extend( jQuery.event, { trigger: function( event, data, elem, onlyHandlers ) { - var i, cur, tmp, bubbleType, ontype, handle, special, + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, eventPath = [ elem || document ], type = hasOwn.call( event, "type" ) ? event.type : event, namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - cur = tmp = elem = elem || document; + cur = lastElement = tmp = elem = elem || document; // Don't do events on text and comment nodes if ( elem.nodeType === 3 || elem.nodeType === 8 ) { @@ -8169,7 +8217,7 @@ jQuery.extend( jQuery.event, { // Determine event propagation path in advance, per W3C events spec (#9951) // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { bubbleType = special.delegateType || type; if ( !rfocusMorph.test( bubbleType + type ) ) { @@ -8189,7 +8237,7 @@ jQuery.extend( jQuery.event, { // Fire handlers on the event path i = 0; while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - + lastElement = cur; event.type = i > 1 ? bubbleType : special.bindType || type; @@ -8221,7 +8269,7 @@ jQuery.extend( jQuery.event, { // Call a native DOM method on the target with the same name as the event. // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { // Don't re-trigger an onFOO event when we call its FOO() method tmp = elem[ ontype ]; @@ -8232,7 +8280,17 @@ jQuery.extend( jQuery.event, { // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + jQuery.event.triggered = undefined; if ( tmp ) { @@ -8278,31 +8336,6 @@ jQuery.fn.extend( { } ); -jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup contextmenu" ).split( " " ), - function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; -} ); - -jQuery.fn.extend( { - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -} ); - - - - -support.focusin = "onfocusin" in window; - - // Support: Firefox <=44 // Firefox doesn't have focus(in | out) events // Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 @@ -8346,7 +8379,7 @@ if ( !support.focusin ) { } var location = window.location; -var nonce = jQuery.now(); +var nonce = Date.now(); var rquery = ( /\?/ ); @@ -8404,7 +8437,7 @@ function buildParams( prefix, obj, traditional, add ) { } } ); - } else if ( !traditional && jQuery.type( obj ) === "object" ) { + } else if ( !traditional && toType( obj ) === "object" ) { // Serialize object item. for ( name in obj ) { @@ -8426,7 +8459,7 @@ jQuery.param = function( a, traditional ) { add = function( key, valueOrFunction ) { // If value is a function, invoke it and use its return value - var value = jQuery.isFunction( valueOrFunction ) ? + var value = isFunction( valueOrFunction ) ? valueOrFunction() : valueOrFunction; @@ -8544,7 +8577,7 @@ function addToPrefiltersOrTransports( structure ) { i = 0, dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; - if ( jQuery.isFunction( func ) ) { + if ( isFunction( func ) ) { // For each dataType in the dataTypeExpression while ( ( dataType = dataTypes[ i++ ] ) ) { @@ -9016,7 +9049,7 @@ jQuery.extend( { if ( s.crossDomain == null ) { urlAnchor = document.createElement( "a" ); - // Support: IE <=8 - 11, Edge 12 - 13 + // Support: IE <=8 - 11, Edge 12 - 15 // IE throws exception on accessing the href property if url is malformed, // e.g. http://example.com:80x/ try { @@ -9074,8 +9107,8 @@ jQuery.extend( { // Remember the hash so we can put it back uncached = s.url.slice( cacheURL.length ); - // If data is available, append data to url - if ( s.data ) { + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; // #9682: remove data so that it's not used in an eventual retry @@ -9312,7 +9345,7 @@ jQuery.each( [ "get", "post" ], function( i, method ) { jQuery[ method ] = function( url, data, callback, type ) { // Shift arguments if data argument was omitted - if ( jQuery.isFunction( data ) ) { + if ( isFunction( data ) ) { type = type || callback; callback = data; data = undefined; @@ -9350,7 +9383,7 @@ jQuery.fn.extend( { var wrap; if ( this[ 0 ] ) { - if ( jQuery.isFunction( html ) ) { + if ( isFunction( html ) ) { html = html.call( this[ 0 ] ); } @@ -9376,7 +9409,7 @@ jQuery.fn.extend( { }, wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { + if ( isFunction( html ) ) { return this.each( function( i ) { jQuery( this ).wrapInner( html.call( this, i ) ); } ); @@ -9396,10 +9429,10 @@ jQuery.fn.extend( { }, wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); + var htmlIsFunction = isFunction( html ); return this.each( function( i ) { - jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html ); + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); } ); }, @@ -9491,7 +9524,8 @@ jQuery.ajaxTransport( function( options ) { return function() { if ( callback ) { callback = errorCallback = xhr.onload = - xhr.onerror = xhr.onabort = xhr.onreadystatechange = null; + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; if ( type === "abort" ) { xhr.abort(); @@ -9531,7 +9565,7 @@ jQuery.ajaxTransport( function( options ) { // Listen to events xhr.onload = callback(); - errorCallback = xhr.onerror = callback( "error" ); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); // Support: IE 9 only // Use onreadystatechange to replace onabort @@ -9685,7 +9719,7 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) { // Get callback name, remembering preexisting value associated with it - callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? + callbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback; @@ -9736,7 +9770,7 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { } // Call if it was a function and we have a response - if ( responseContainer && jQuery.isFunction( overwritten ) ) { + if ( responseContainer && isFunction( overwritten ) ) { overwritten( responseContainer[ 0 ] ); } @@ -9828,7 +9862,7 @@ jQuery.fn.load = function( url, params, callback ) { } // If it's a function - if ( jQuery.isFunction( params ) ) { + if ( isFunction( params ) ) { // We assume that it's the callback callback = params; @@ -9936,7 +9970,7 @@ jQuery.offset = { curLeft = parseFloat( curCSSLeft ) || 0; } - if ( jQuery.isFunction( options ) ) { + if ( isFunction( options ) ) { // Use jQuery.extend here to allow modification of coordinates argument (gh-1848) options = options.call( elem, i, jQuery.extend( {}, curOffset ) ); @@ -9959,6 +9993,8 @@ jQuery.offset = { }; jQuery.fn.extend( { + + // offset() relates an element's border box to the document origin offset: function( options ) { // Preserve chaining for setter @@ -9970,7 +10006,7 @@ jQuery.fn.extend( { } ); } - var doc, docElem, rect, win, + var rect, win, elem = this[ 0 ]; if ( !elem ) { @@ -9985,50 +10021,52 @@ jQuery.fn.extend( { return { top: 0, left: 0 }; } + // Get document-relative position by adding viewport scroll to viewport-relative gBCR rect = elem.getBoundingClientRect(); - - doc = elem.ownerDocument; - docElem = doc.documentElement; - win = doc.defaultView; - + win = elem.ownerDocument.defaultView; return { - top: rect.top + win.pageYOffset - docElem.clientTop, - left: rect.left + win.pageXOffset - docElem.clientLeft + top: rect.top + win.pageYOffset, + left: rect.left + win.pageXOffset }; }, + // position() relates an element's margin box to its offset parent's padding box + // This corresponds to the behavior of CSS absolute positioning position: function() { if ( !this[ 0 ] ) { return; } - var offsetParent, offset, + var offsetParent, offset, doc, elem = this[ 0 ], parentOffset = { top: 0, left: 0 }; - // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, - // because it is its only offset parent + // position:fixed elements are offset from the viewport, which itself always has zero offset if ( jQuery.css( elem, "position" ) === "fixed" ) { - // Assume getBoundingClientRect is there when computed position is fixed + // Assume position:fixed implies availability of getBoundingClientRect offset = elem.getBoundingClientRect(); } else { - - // Get *real* offsetParent - offsetParent = this.offsetParent(); - - // Get correct offsets offset = this.offset(); - if ( !nodeName( offsetParent[ 0 ], "html" ) ) { - parentOffset = offsetParent.offset(); - } - // Add offsetParent borders - parentOffset = { - top: parentOffset.top + jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ), - left: parentOffset.left + jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ) - }; + // Account for the *real* offset parent, which can be the document or its root element + // when a statically positioned element is identified + doc = elem.ownerDocument; + offsetParent = elem.offsetParent || doc.documentElement; + while ( offsetParent && + ( offsetParent === doc.body || offsetParent === doc.documentElement ) && + jQuery.css( offsetParent, "position" ) === "static" ) { + + offsetParent = offsetParent.parentNode; + } + if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) { + + // Incorporate borders into its offset, since they are outside its content origin + parentOffset = jQuery( offsetParent ).offset(); + parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true ); + parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true ); + } } // Subtract parent offsets and element margins @@ -10070,7 +10108,7 @@ jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( // Coalesce documents and windows var win; - if ( jQuery.isWindow( elem ) ) { + if ( isWindow( elem ) ) { win = elem; } else if ( elem.nodeType === 9 ) { win = elem.defaultView; @@ -10128,7 +10166,7 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { return access( this, function( elem, type, value ) { var doc; - if ( jQuery.isWindow( elem ) ) { + if ( isWindow( elem ) ) { // $( window ).outerWidth/Height return w/h including scrollbars (gh-1729) return funcName.indexOf( "outer" ) === 0 ? @@ -10162,6 +10200,28 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { } ); +jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup contextmenu" ).split( " " ), + function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; +} ); + +jQuery.fn.extend( { + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +} ); + + + + jQuery.fn.extend( { bind: function( types, data, fn ) { @@ -10183,6 +10243,37 @@ jQuery.fn.extend( { } } ); +// Bind a function to a context, optionally partially applying any +// arguments. +// jQuery.proxy is deprecated to promote standards (specifically Function#bind) +// However, it is not slated for removal any time soon +jQuery.proxy = function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; +}; + jQuery.holdReady = function( hold ) { if ( hold ) { jQuery.readyWait++; @@ -10193,6 +10284,26 @@ jQuery.holdReady = function( hold ) { jQuery.isArray = Array.isArray; jQuery.parseJSON = JSON.parse; jQuery.nodeName = nodeName; +jQuery.isFunction = isFunction; +jQuery.isWindow = isWindow; +jQuery.camelCase = camelCase; +jQuery.type = toType; + +jQuery.now = Date.now; + +jQuery.isNumeric = function( obj ) { + + // As of jQuery 3.0, isNumeric is limited to + // strings and numbers (primitives or objects) + // that can be coerced to finite numbers (gh-2662) + var type = jQuery.type( obj ); + return ( type === "number" || type === "string" ) && + + // parseFloat NaNs numeric-cast false positives ("") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + !isNaN( obj - parseFloat( obj ) ); +}; From 8d8ee2a87a74954cf0230b039ef748fb628ee952 Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 8 Apr 2018 10:09:33 -0400 Subject: [PATCH 11/18] small sync refactorings --- src/routes/api/sync.js | 4 ++-- src/services/sync.js | 53 +++++++++++++++++------------------------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/routes/api/sync.js b/src/routes/api/sync.js index 725626b9f..b90266048 100644 --- a/src/routes/api/sync.js +++ b/src/routes/api/sync.js @@ -10,8 +10,8 @@ const log = require('../../services/log'); async function checkSync() { return { - 'hashes': await contentHashService.getHashes(), - 'maxSyncId': await sql.getValue('SELECT MAX(id) FROM sync') + hashes: await contentHashService.getHashes(), + maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync') }; } diff --git a/src/services/sync.js b/src/services/sync.js index 92e4bacc3..12669caeb 100644 --- a/src/services/sync.js +++ b/src/services/sync.js @@ -89,18 +89,8 @@ async function login() { return syncContext; } -async function getLastSyncedPull() { - return parseInt(await optionService.getOption('lastSyncedPull')); -} - -async function setLastSyncedPull(syncId) { - await optionService.setOption('lastSyncedPull', syncId); -} - async function pullSync(syncContext) { - const lastSyncedPull = await getLastSyncedPull(); - - const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull; + const changesUri = '/api/sync/changed?lastSyncId=' + await getLastSyncedPull(); const rows = await syncRequest(syncContext, 'GET', changesUri); @@ -109,14 +99,6 @@ async function pullSync(syncContext) { for (const {sync, entity} of rows) { if (sourceIdService.isLocalSourceId(sync.sourceId)) { log.info(`Skipping pull #${sync.id} ${sync.entityName} ${sync.entityId} because ${sync.sourceId} is a local source id.`); - - await setLastSyncedPull(sync.id); - - continue; - } - - if (!entity) { - log.error(`Empty response to pull for sync #${sync.id} ${sync.entityName}, id=${sync.entityId}`); } else { await syncUpdateService.updateEntity(sync.entityName, entity, syncContext.sourceId); @@ -128,14 +110,6 @@ async function pullSync(syncContext) { log.info("Finished pull"); } -async function getLastSyncedPush() { - return parseInt(await optionService.getOption('lastSyncedPush')); -} - -async function setLastSyncedPush(lastSyncedPush) { - await optionService.setOption('lastSyncedPush', lastSyncedPush); -} - async function pushSync(syncContext) { let lastSyncedPush = await getLastSyncedPush(); @@ -159,8 +133,6 @@ async function pushSync(syncContext) { }); if (filteredSyncs.length === 0) { - // nothing to sync - log.info("Nothing to push"); await setLastSyncedPush(lastSyncedPush); @@ -170,6 +142,8 @@ async function pushSync(syncContext) { const syncRecords = await getSyncRecords(filteredSyncs); + log.info(`Pushing ${syncRecords.length} syncs.`); + await syncRequest(syncContext, 'PUT', '/api/sync/update', { sourceId: sourceIdService.getCurrentSourceId(), entities: syncRecords @@ -190,11 +164,10 @@ async function checkContentHash(syncContext) { return; } - const lastSyncedPush = await getLastSyncedPush(); - const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]); + const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [await getLastSyncedPush()]); if (notPushedSyncs > 0) { - log.info("There's " + notPushedSyncs + " outstanding pushes, skipping content check."); + log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`); return; } @@ -288,6 +261,22 @@ async function getSyncRecords(syncs) { return records; } +async function getLastSyncedPull() { + return parseInt(await optionService.getOption('lastSyncedPull')); +} + +async function setLastSyncedPull(syncId) { + await optionService.setOption('lastSyncedPull', syncId); +} + +async function getLastSyncedPush() { + return parseInt(await optionService.getOption('lastSyncedPush')); +} + +async function setLastSyncedPush(lastSyncedPush) { + await optionService.setOption('lastSyncedPush', lastSyncedPush); +} + sqlInit.dbReady.then(() => { if (syncSetup.isSyncSetup) { log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT); From 9720868f5a2322815a929cbe8be3a356ec32d020 Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 8 Apr 2018 11:57:14 -0400 Subject: [PATCH 12/18] added type and mime to note revisions --- db/migrations/0087__add_type_mime_to_note_revision.sql | 5 +++++ src/services/app_info.js | 2 +- src/services/notes.js | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 db/migrations/0087__add_type_mime_to_note_revision.sql diff --git a/db/migrations/0087__add_type_mime_to_note_revision.sql b/db/migrations/0087__add_type_mime_to_note_revision.sql new file mode 100644 index 000000000..f4b6629e3 --- /dev/null +++ b/db/migrations/0087__add_type_mime_to_note_revision.sql @@ -0,0 +1,5 @@ +ALTER TABLE note_revisions ADD type TEXT DEFAULT '' NOT NULL; +ALTER TABLE note_revisions ADD mime TEXT DEFAULT '' NOT NULL; + +UPDATE note_revisions SET type = (SELECT type FROM notes WHERE notes.noteId = note_revisions.noteId); +UPDATE note_revisions SET mime = (SELECT mime FROM notes WHERE notes.noteId = note_revisions.noteId); \ No newline at end of file diff --git a/src/services/app_info.js b/src/services/app_info.js index 6e8298922..0afd74203 100644 --- a/src/services/app_info.js +++ b/src/services/app_info.js @@ -3,7 +3,7 @@ const build = require('./build'); const packageJson = require('../../package'); -const APP_DB_VERSION = 86; +const APP_DB_VERSION = 87; module.exports = { appVersion: packageJson.version, diff --git a/src/services/notes.js b/src/services/notes.js index 440a05bb0..29a011e82 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -180,6 +180,8 @@ async function saveNoteRevision(note) { // title and text should be decrypted now title: note.title, content: note.content, + type: note.type, + mime: note.mime, isProtected: 0, // will be fixed in the protectNoteRevisions() call dateModifiedFrom: note.dateModified, dateModifiedTo: dateUtils.nowDate() From 6bb3cfa9a3b9bafa2fc8d08f417c356d395672ad Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 8 Apr 2018 12:13:52 -0400 Subject: [PATCH 13/18] note revisions for code is now properly formatted, fixes #97 --- src/public/javascripts/dialogs/note_revisions.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/public/javascripts/dialogs/note_revisions.js b/src/public/javascripts/dialogs/note_revisions.js index 331a0dff3..42543565f 100644 --- a/src/public/javascripts/dialogs/note_revisions.js +++ b/src/public/javascripts/dialogs/note_revisions.js @@ -54,7 +54,13 @@ $list.on('change', () => { const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal); $title.html(revisionItem.title); - $content.html(revisionItem.content); + + if (revisionItem.type === 'text') { + $content.html(revisionItem.content); + } + else if (revisionItem.type === 'code') { + $content.html($("
").text(revisionItem.content));
+    }
 });
 
 $(document).on('click', "a[action='note-revision']", event => {

From 5b0e1a644d646bb87ea3a8ef8a6b5db07352fce3 Mon Sep 17 00:00:00 2001
From: azivner 
Date: Sun, 8 Apr 2018 12:17:42 -0400
Subject: [PATCH 14/18] codemirror now doesn't hijack alt-left/right, fixes #86

---
 src/public/javascripts/services/note_detail_code.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/public/javascripts/services/note_detail_code.js b/src/public/javascripts/services/note_detail_code.js
index 00b52194d..5a9baf2b3 100644
--- a/src/public/javascripts/services/note_detail_code.js
+++ b/src/public/javascripts/services/note_detail_code.js
@@ -16,6 +16,10 @@ async function show() {
         CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
         CodeMirror.keyMap.default["Tab"] = "indentMore";
 
+        // these conflict with backward/forward navigation shortcuts
+        delete CodeMirror.keyMap.default["Alt-Left"];
+        delete CodeMirror.keyMap.default["Alt-Right"];
+
         CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
 
         codeEditor = CodeMirror($noteDetailCode[0], {

From b277a250e5d9dcec5c6515499e3d941c3ec52148 Mon Sep 17 00:00:00 2001
From: azivner 
Date: Sun, 8 Apr 2018 12:27:10 -0400
Subject: [PATCH 15/18] protected notes are not in autocomplete when not in
 protected session, fixes #46

---
 src/entities/note.js                            |  3 ++-
 src/public/javascripts/services/autocomplete.js | 14 +++++++-------
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/entities/note.js b/src/entities/note.js
index 2b5e9e7b7..9f599c5f1 100644
--- a/src/entities/note.js
+++ b/src/entities/note.js
@@ -12,7 +12,8 @@ class Note extends Entity {
     constructor(row) {
         super(row);
 
-        if (this.isProtected) {
+        // check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
+        if (this.isProtected && this.noteId) {
             protected_session.decryptNote(this);
         }
 
diff --git a/src/public/javascripts/services/autocomplete.js b/src/public/javascripts/services/autocomplete.js
index 7054c14d7..7243a53f9 100644
--- a/src/public/javascripts/services/autocomplete.js
+++ b/src/public/javascripts/services/autocomplete.js
@@ -1,5 +1,6 @@
 import treeCache from "./tree_cache.js";
 import treeUtils from "./tree_utils.js";
+import protectedSessionHolder from './protected_session_holder.js';
 
 async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
     if (!parentNoteId) {
@@ -21,9 +22,6 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
         titlePath = '';
     }
 
-    // https://github.com/zadam/trilium/issues/46
-    // unfortunately not easy to implement because we don't have an easy access to note's isProtected property
-
     const autocompleteItems = [];
 
     for (const childNote of childNotes) {
@@ -34,10 +32,12 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
         const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
         const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
 
-        autocompleteItems.push({
-            value: childTitlePath + ' (' + childNotePath + ')',
-            label: childTitlePath
-        });
+        if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
+            autocompleteItems.push({
+                value: childTitlePath + ' (' + childNotePath + ')',
+                label: childTitlePath
+            });
+        }
 
         const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);
 

From 1f96a6beab890a821761b88493974eaf2ab7d097 Mon Sep 17 00:00:00 2001
From: azivner 
Date: Sun, 8 Apr 2018 13:14:30 -0400
Subject: [PATCH 16/18] export & import work correctly with clones

---
 src/routes/api/export.js | 108 ++++++++++++++++++++++-----------------
 src/routes/api/import.js |  27 ++++++++--
 src/services/notes.js    |   1 +
 3 files changed, 85 insertions(+), 51 deletions(-)

diff --git a/src/routes/api/export.js b/src/routes/api/export.js
index e5cf87aeb..414fbf6a7 100644
--- a/src/routes/api/export.js
+++ b/src/routes/api/export.js
@@ -13,7 +13,68 @@ async function exportNote(req, res) {
 
     const pack = tar.pack();
 
-    const name = await exportNoteInner(branchId, '', pack);
+    const exportedNoteIds = [];
+    const name = await exportNoteInner(branchId, '');
+
+    async function exportNoteInner(branchId, directory) {
+        const branch = await repository.getBranch(branchId);
+        const note = await branch.getNote();
+        const childFileName = directory + sanitize(note.title);
+
+        if (exportedNoteIds.includes(note.noteId)) {
+            saveMetadataFile(childFileName, {
+                version: 1,
+                clone: true,
+                noteId: note.noteId,
+                prefix: branch.prefix
+            });
+
+            return;
+        }
+
+        const metadata = {
+            version: 1,
+            clone: false,
+            noteId: note.noteId,
+            title: note.title,
+            prefix: branch.prefix,
+            type: note.type,
+            mime: note.mime,
+            labels: (await note.getLabels()).map(label => {
+                return {
+                    name: label.name,
+                    value: label.value
+                };
+            })
+        };
+
+        if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
+            return;
+        }
+
+        saveMetadataFile(childFileName, metadata);
+        saveDataFile(childFileName, note);
+
+        exportedNoteIds.push(note.noteId);
+
+        for (const child of await note.getChildBranches()) {
+            await exportNoteInner(child.branchId, childFileName + "/");
+        }
+
+        return childFileName;
+    }
+
+    function saveDataFile(childFileName, note) {
+        const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
+
+        pack.entry({name: childFileName + ".dat", size: content.length}, content);
+    }
+
+    function saveMetadataFile(childFileName, metadata) {
+        const metadataJson = JSON.stringify(metadata, null, '\t');
+
+        pack.entry({name: childFileName + ".meta", size: metadataJson.length}, metadataJson);
+    }
 
     pack.finalize();
 
@@ -23,51 +84,6 @@ async function exportNote(req, res) {
     pack.pipe(res);
 }
 
-async function exportNoteInner(branchId, directory, pack) {
-    const branch = await repository.getBranch(branchId);
-    const note = await branch.getNote();
-
-    if (note.isProtected) {
-        return;
-    }
-
-    const metadata = await getMetadata(note);
-
-    if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
-        return;
-    }
-
-    const metadataJson = JSON.stringify(metadata, null, '\t');
-    const childFileName = directory + sanitize(note.title);
-
-    pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson);
-
-    const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
-
-    pack.entry({ name: childFileName + ".dat", size: content.length }, content);
-
-    for (const child of await note.getChildBranches()) {
-        await exportNoteInner(child.branchId, childFileName + "/", pack);
-    }
-
-    return childFileName;
-}
-
-async function getMetadata(note) {
-    return {
-        version: 1,
-        title: note.title,
-        type: note.type,
-        mime: note.mime,
-        labels: (await note.getLabels()).map(label => {
-            return {
-                name: label.name,
-                value: label.value
-            };
-        })
-    };
-}
-
 module.exports = {
     exportNote
 };
\ No newline at end of file
diff --git a/src/routes/api/import.js b/src/routes/api/import.js
index a2db68927..f89ffabe0 100644
--- a/src/routes/api/import.js
+++ b/src/routes/api/import.js
@@ -3,6 +3,7 @@
 const repository = require('../../services/repository');
 const labelService = require('../../services/labels');
 const noteService = require('../../services/notes');
+const Branch = require('../../entities/branch');
 const tar = require('tar-stream');
 const stream = require('stream');
 const path = require('path');
@@ -31,7 +32,7 @@ async function parseImportFile(file) {
     const extract = tar.extract();
 
     extract.on('entry', function(header, stream, next) {
-        let {name, key} = getFileName(header.name);
+        const {name, key} = getFileName(header.name);
 
         let file = fileMap[name];
 
@@ -97,30 +98,46 @@ async function importTar(req) {
 
     const files = await parseImportFile(file);
 
-    await importNotes(files, parentNoteId);
+    // maps from original noteId (in tar file) to newly generated noteId
+    const noteIdMap = {};
+
+    await importNotes(files, parentNoteId, noteIdMap);
 }
 
-async function importNotes(files, parentNoteId) {
+async function importNotes(files, parentNoteId, noteIdMap) {
     for (const file of files) {
         if (file.meta.version !== 1) {
             throw new Error("Can't read meta data version " + file.meta.version);
         }
 
+        if (file.meta.clone) {
+            await new Branch({
+                parentNoteId: parentNoteId,
+                noteId: noteIdMap[file.meta.noteId],
+                prefix: file.meta.prefix
+            }).save();
+
+            return;
+        }
+
         if (file.meta.type !== 'file') {
             file.data = file.data.toString("UTF-8");
         }
 
         const {note} = await noteService.createNote(parentNoteId, file.meta.title, file.data, {
             type: file.meta.type,
-            mime: file.meta.mime
+            mime: file.meta.mime,
+            prefix: file.meta.prefix
         });
 
+        noteIdMap[file.meta.noteId] = note.noteId;
+
         for (const label of file.meta.labels) {
             await labelService.createLabel(note.noteId, label.name, label.value);
         }
 
         if (file.children.length > 0) {
-            await importNotes(file.children, note.noteId);
+            await importNotes(file.children, note.noteId, noteIdMap);
         }
     }
 }
diff --git a/src/services/notes.js b/src/services/notes.js
index 29a011e82..e4fb74557 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -56,6 +56,7 @@ async function createNewNote(parentNoteId, noteData) {
         noteId: note.noteId,
         parentNoteId: parentNoteId,
         notePosition: newNotePos,
+        prefix: noteData.prefix,
         isExpanded: 0
     }).save();
 

From f4079604c97bb72dc6b55aa8f4d65f370254afc2 Mon Sep 17 00:00:00 2001
From: azivner 
Date: Sun, 8 Apr 2018 22:38:52 -0400
Subject: [PATCH 17/18] basic implementation of children overview, closes #80

---
 src/public/javascripts/entities/branch.js     |   4 +
 .../javascripts/services/note_detail.js       |  28 +++-
 src/public/stylesheets/style.css              |  23 +++-
 src/views/index.ejs                           | 129 +++++++++---------
 4 files changed, 116 insertions(+), 68 deletions(-)

diff --git a/src/public/javascripts/entities/branch.js b/src/public/javascripts/entities/branch.js
index e6f2d66a2..2a2268e5d 100644
--- a/src/public/javascripts/entities/branch.js
+++ b/src/public/javascripts/entities/branch.js
@@ -14,6 +14,10 @@ class Branch {
         return await this.treeCache.getNote(this.noteId);
     }
 
+    isTopLevel() {
+        return this.parentNoteId === 'root';
+    }
+
     get toString() {
         return `Branch(branchId=${this.branchId})`;
     }
diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js
index 71bdc9f28..cbf648f74 100644
--- a/src/public/javascripts/services/note_detail.js
+++ b/src/public/javascripts/services/note_detail.js
@@ -1,4 +1,5 @@
 import treeService from './tree.js';
+import treeUtils from './tree_utils.js';
 import noteTypeService from './note_type.js';
 import protectedSessionService from './protected_session.js';
 import protectedSessionHolder from './protected_session_holder.js';
@@ -24,6 +25,7 @@ const $noteDetailWrapper = $("#note-detail-wrapper");
 const $noteIdDisplay = $("#note-id-display");
 const $labelList = $("#label-list");
 const $labelListInner = $("#label-list-inner");
+const $childrenOverview = $("#children-overview");
 
 let currentNote = null;
 
@@ -73,14 +75,14 @@ function noteChanged() {
 async function reload() {
     // no saving here
 
-    await loadNoteToEditor(getCurrentNoteId());
+    await loadNoteDetail(getCurrentNoteId());
 }
 
 async function switchToNote(noteId) {
     if (getCurrentNoteId() !== noteId) {
         await saveNoteIfChanged();
 
-        await loadNoteToEditor(noteId);
+        await loadNoteDetail(noteId);
     }
 }
 
@@ -137,7 +139,7 @@ async function handleProtectedSession() {
     protectedSessionService.ensureDialogIsClosed();
 }
 
-async function loadNoteToEditor(noteId) {
+async function loadNoteDetail(noteId) {
     currentNote = await loadNote(noteId);
 
     if (isNewNoteCreated) {
@@ -175,6 +177,26 @@ async function loadNoteToEditor(noteId) {
     $noteDetailWrapper.scrollTop(0);
 
     await loadLabelList();
+
+    await showChildrenOverview();
+}
+
+async function showChildrenOverview() {
+    const note = getCurrentNote();
+
+    $childrenOverview.empty();
+
+    const notePath = treeService.getCurrentNotePath();
+
+    for (const childBranch of await note.getChildBranches()) {
+        const link = $('', {
+            href: 'javascript:',
+            text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
+        }).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
+
+        const childEl = $('
').html(link); + $childrenOverview.append(childEl); + } } async function loadLabelList() { diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index fa3dd610c..e5719680c 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -5,9 +5,9 @@ display: grid; grid-template-areas: "header header" "tree-actions title" - "search note-content" - "tree note-content" - "parent-list note-content" + "search note-detail" + "tree note-detail" + "parent-list note-detail" "parent-list label-list"; grid-template-columns: 2fr 5fr; grid-template-rows: auto @@ -288,4 +288,21 @@ div.ui-tooltip { #file-table th, #file-table td { padding: 10px; font-size: large; +} + +#children-overview { + padding-top: 20px; +} + +.child-overview { + font-weight: bold; + font-size: large; + padding: 10px; + border: 1px solid black; + width: 150px; + height: 95px; + margin-right: 20px; + margin-bottom: 20px; + border-radius: 15px; + overflow: hidden; } \ No newline at end of file diff --git a/src/views/index.ejs b/src/views/index.ejs index bcfd24fa6..b1e358f38 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -132,76 +132,81 @@
-
-
+
+
+
-