mirror of https://github.com/laurent22/joplin.git
Chore: Renderer: Refactor and test long-press and click handlers (#7774)
parent
3a14b76a61
commit
057ac550bd
|
@ -800,6 +800,8 @@ packages/renderer/HtmlToHtml.js
|
|||
packages/renderer/InMemoryCache.js
|
||||
packages/renderer/MarkupToHtml.js
|
||||
packages/renderer/MdToHtml.js
|
||||
packages/renderer/MdToHtml/createEventHandlingAttrs.js
|
||||
packages/renderer/MdToHtml/createEventHandlingAttrs.test.js
|
||||
packages/renderer/MdToHtml/linkReplacement.js
|
||||
packages/renderer/MdToHtml/linkReplacement.test.js
|
||||
packages/renderer/MdToHtml/renderMedia.js
|
||||
|
|
|
@ -788,6 +788,8 @@ packages/renderer/HtmlToHtml.js
|
|||
packages/renderer/InMemoryCache.js
|
||||
packages/renderer/MarkupToHtml.js
|
||||
packages/renderer/MdToHtml.js
|
||||
packages/renderer/MdToHtml/createEventHandlingAttrs.js
|
||||
packages/renderer/MdToHtml/createEventHandlingAttrs.test.js
|
||||
packages/renderer/MdToHtml/linkReplacement.js
|
||||
packages/renderer/MdToHtml/linkReplacement.test.js
|
||||
packages/renderer/MdToHtml/renderMedia.js
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { createEventHandlingListeners, Options } from './createEventHandlingAttrs';
|
||||
import { describe, beforeAll, it, jest, expect } from '@jest/globals';
|
||||
|
||||
describe('createEventHandlingAttrs', () => {
|
||||
let lastMessage: string|undefined = undefined;
|
||||
const postMessageFn = (message: string) => {
|
||||
lastMessage = message;
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
lastMessage = undefined;
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('should not create listeners to handle long-press when long press is disabled', () => {
|
||||
const options: Options = {
|
||||
enableLongPress: false,
|
||||
postMessageSyntax: 'postMessageFn',
|
||||
};
|
||||
const listeners = createEventHandlingListeners('someresourceid', options, 'postMessage("click")');
|
||||
|
||||
// Should not add touchstart/mouseenter/leave listeners when not long-pressing.
|
||||
expect(listeners.onmouseenter).toBe('');
|
||||
expect(listeners.onmouseleave).toBe('');
|
||||
expect(listeners.ontouchstart).toBe('');
|
||||
expect(listeners.ontouchmove).toBe('');
|
||||
expect(listeners.ontouchend).toBe('');
|
||||
expect(listeners.ontouchcancel).toBe('');
|
||||
});
|
||||
|
||||
it('should create click listener for given click action', () => {
|
||||
const options: Options = {
|
||||
enableLongPress: false,
|
||||
postMessageSyntax: 'postMessageFn',
|
||||
};
|
||||
const clickAction = 'postMessageFn("click")';
|
||||
const listeners = createEventHandlingListeners('someresourceid', options, clickAction);
|
||||
expect(listeners.onclick).toContain(clickAction);
|
||||
|
||||
postMessageFn('test');
|
||||
eval(listeners.onclick);
|
||||
expect(lastMessage).toBe('click');
|
||||
});
|
||||
|
||||
it('should create ontouch listeners for long press', () => {
|
||||
const options: Options = {
|
||||
enableLongPress: true,
|
||||
postMessageSyntax: 'postMessageFn',
|
||||
};
|
||||
const clickAction: null|string = null;
|
||||
const listeners = createEventHandlingListeners('resourceidhere', options, clickAction);
|
||||
|
||||
expect(listeners.onclick).toBe('');
|
||||
expect(listeners.ontouchstart).not.toBe('');
|
||||
|
||||
// Clear lastMessage
|
||||
postMessageFn('test');
|
||||
|
||||
eval(listeners.ontouchstart);
|
||||
jest.advanceTimersByTime(1000 * 4);
|
||||
|
||||
expect(lastMessage).toBe('longclick:resourceidhere');
|
||||
});
|
||||
|
||||
it('motion during a long press should cancel the timeout', () => {
|
||||
const options: Options = {
|
||||
enableLongPress: true,
|
||||
postMessageSyntax: 'postMessageFn',
|
||||
};
|
||||
const listeners = createEventHandlingListeners('id', options, null);
|
||||
|
||||
lastMessage = '';
|
||||
eval(listeners.ontouchstart);
|
||||
jest.advanceTimersByTime(100);
|
||||
eval(listeners.ontouchmove);
|
||||
jest.advanceTimersByTime(1000 * 100);
|
||||
|
||||
// Message handler should not have been called.
|
||||
expect(lastMessage).toBe('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
|
||||
import utils from '../utils';
|
||||
|
||||
|
||||
export interface Options {
|
||||
enableLongPress: boolean;
|
||||
postMessageSyntax: string;
|
||||
}
|
||||
|
||||
// longPressTouchStart and clearLongPressTimeout are turned into strings before being called.
|
||||
// Thus, they should not reference any other non-builtin functions.
|
||||
const longPressTouchStartFnString = `(onLongPress, longPressDelay) => {
|
||||
// if touchTimeout is set when ontouchstart is called it means the user has already touched
|
||||
// the screen once and this is the 2nd touch in this case we assume the user is trying
|
||||
// to zoom and we don't want to show the menu
|
||||
if (!!window.touchTimeout) {
|
||||
clearTimeout(window.touchTimeout);
|
||||
window.touchTimeout = null;
|
||||
} else {
|
||||
window.touchTimeout = setTimeout(() => {
|
||||
window.touchTimeout = null;
|
||||
onLongPress();
|
||||
}, longPressDelay);
|
||||
}
|
||||
}`;
|
||||
|
||||
const clearLongPressTimeoutFnString = `() => {
|
||||
if (window.touchTimeout) {
|
||||
clearTimeout(window.touchTimeout);
|
||||
window.touchTimeout = null;
|
||||
}
|
||||
}`;
|
||||
|
||||
// Helper for createEventHandlingAttrs. Exported to facilitate testing.
|
||||
export const createEventHandlingListeners = (resourceId: string, options: Options, onClickAction: string|null) => {
|
||||
const eventHandlers = {
|
||||
ontouchstart: '',
|
||||
ontouchmove: '',
|
||||
ontouchend: '',
|
||||
ontouchcancel: '',
|
||||
onmouseenter: '',
|
||||
onmouseleave: '',
|
||||
onclick: '',
|
||||
};
|
||||
|
||||
if (options.enableLongPress) {
|
||||
const longPressHandler = `(() => ${options.postMessageSyntax}('longclick:${resourceId}'))`;
|
||||
const touchStart = `(${longPressTouchStartFnString})(${longPressHandler}, ${utils.longPressDelay}); `;
|
||||
|
||||
const callClearLongPressTimeout = `(${clearLongPressTimeoutFnString})(); `;
|
||||
const touchCancel = callClearLongPressTimeout;
|
||||
const touchEnd = callClearLongPressTimeout;
|
||||
|
||||
eventHandlers.ontouchstart += touchStart;
|
||||
eventHandlers.ontouchcancel += touchCancel;
|
||||
eventHandlers.ontouchmove += touchCancel;
|
||||
eventHandlers.ontouchend += touchEnd;
|
||||
}
|
||||
|
||||
if (onClickAction) {
|
||||
eventHandlers.onclick += onClickAction;
|
||||
}
|
||||
|
||||
return eventHandlers;
|
||||
};
|
||||
|
||||
// Adds event-handling (e.g. long press) code to images and links.
|
||||
// resourceId is the ID of the image resource or link.
|
||||
const createEventHandlingAttrs = (resourceId: string, options: Options, onClickAction: string|null) => {
|
||||
const eventHandlers = createEventHandlingListeners(resourceId, options, onClickAction);
|
||||
|
||||
// Build onfoo="listener" strings and add them to the result.
|
||||
let result = '';
|
||||
for (const listenerType in eventHandlers) {
|
||||
const eventHandlersDict = eventHandlers as Record<string, string>;
|
||||
|
||||
// Only create code for non-empty listeners.
|
||||
if (eventHandlersDict[listenerType].length > 0) {
|
||||
const listener = eventHandlersDict[listenerType].replace(/["]/g, '"');
|
||||
result += ` ${listenerType}="${listener}" `;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default createEventHandlingAttrs;
|
|
@ -1,4 +1,5 @@
|
|||
import linkReplacement from './linkReplacement';
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
|
||||
describe('linkReplacement', () => {
|
||||
|
||||
|
@ -57,4 +58,24 @@ describe('linkReplacement', () => {
|
|||
expect(r.indexOf(expectedPrefix)).toBe(0);
|
||||
});
|
||||
|
||||
test('should create ontouch listeners to handle longpress', () => {
|
||||
const resourceId = 'e6afba55bdf74568ac94f8d1e3578d2c';
|
||||
|
||||
const linkHtml = linkReplacement(`:/${resourceId}`, {
|
||||
ResourceModel: {},
|
||||
resources: {
|
||||
[resourceId]: {
|
||||
item: {},
|
||||
localState: {
|
||||
fetch_status: 2, // FETCH_STATUS_DONE
|
||||
},
|
||||
},
|
||||
},
|
||||
enableLongPress: true,
|
||||
}).html;
|
||||
|
||||
expect(linkHtml).toContain('ontouchstart');
|
||||
expect(linkHtml).toContain('ontouchend');
|
||||
expect(linkHtml).toContain('ontouchcancel');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import utils, { ItemIdToUrlHandler } from '../utils';
|
||||
import createEventHandlingAttrs from './createEventHandlingAttrs';
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
const urlUtils = require('../urlUtils.js');
|
||||
|
@ -93,13 +94,10 @@ export default function(href: string, options: Options = null): LinkReplacementR
|
|||
let js = `${options.postMessageSyntax}(${JSON.stringify(href)}, { resourceId: ${JSON.stringify(resourceId)} }); return false;`;
|
||||
if (options.enableLongPress && !!resourceId) {
|
||||
const onClick = `${options.postMessageSyntax}(${JSON.stringify(href)})`;
|
||||
const onLongClick = `${options.postMessageSyntax}("longclick:${resourceId}")`;
|
||||
// if t is set when ontouchstart is called it means the user has already touched the screen once and this is the 2nd touch
|
||||
// in this case we assume the user is trying to zoom and we don't want to show the menu
|
||||
const touchStart = `if (typeof(t) !== "undefined" && !!t) { clearTimeout(t); t = null; } else { t = setTimeout(() => { t = null; ${onLongClick}; }, ${utils.longPressDelay}); }`;
|
||||
const cancel = 'if (!!t) {clearTimeout(t); t=null;';
|
||||
const touchEnd = `${cancel} ${onClick};}`;
|
||||
js = `ontouchstart='${touchStart}' ontouchend='${touchEnd}' ontouchcancel='${cancel} ontouchmove="${cancel}'`;
|
||||
js = createEventHandlingAttrs(resourceId, {
|
||||
enableLongPress: options.enableLongPress ?? false,
|
||||
postMessageSyntax: options.postMessageSyntax ?? 'void',
|
||||
}, onClick);
|
||||
} else {
|
||||
js = `onclick='${js}'`;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { RuleOptions } from '../../MdToHtml';
|
||||
import htmlUtils from '../../htmlUtils';
|
||||
import utils from '../../utils';
|
||||
import createEventHandlingAttrs from '../createEventHandlingAttrs';
|
||||
|
||||
function plugin(markdownIt: any, ruleOptions: RuleOptions) {
|
||||
const defaultRender = markdownIt.renderer.rules.image;
|
||||
|
@ -17,19 +18,14 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) {
|
|||
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl);
|
||||
if (typeof r === 'string') return r;
|
||||
if (r) {
|
||||
let js = '';
|
||||
if (ruleOptions.enableLongPress) {
|
||||
const id = r['data-resource-id'];
|
||||
const id = r['data-resource-id'];
|
||||
|
||||
const longPressHandler = `${ruleOptions.postMessageSyntax}('longclick:${id}')`;
|
||||
// if t is set when ontouchstart is called it means the user has already touched the screen once and this is the 2nd touch
|
||||
// in this case we assume the user is trying to zoom and we don't want to show the menu
|
||||
const touchStart = `if (typeof(t) !== 'undefined' && !!t) { clearTimeout(t); t = null; } else { t = setTimeout(() => { t = null; ${longPressHandler}; }, ${utils.longPressDelay}); }`;
|
||||
const cancel = 'if (!!t) clearTimeout(t); t=null';
|
||||
const js = createEventHandlingAttrs(id, {
|
||||
enableLongPress: ruleOptions.enableLongPress ?? false,
|
||||
postMessageSyntax: ruleOptions.postMessageSyntax ?? 'void',
|
||||
}, null);
|
||||
|
||||
js = ` ontouchstart="${touchStart}" ontouchend="${cancel}" ontouchcancel="${cancel}" ontouchmove="${cancel}"`;
|
||||
}
|
||||
return `<img data-from-md ${htmlUtils.attributesHtml(Object.assign({}, r, { title: title, alt: token.content }))}${js}/>`;
|
||||
return `<img data-from-md ${htmlUtils.attributesHtml(Object.assign({}, r, { title: title, alt: token.content }))} ${js}/>`;
|
||||
}
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
};
|
||||
|
|
|
@ -69,14 +69,11 @@ module.exports = {
|
|||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
moduleFileExtensions: [
|
||||
'ts',
|
||||
'tsx',
|
||||
'js',
|
||||
],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
@ -145,13 +142,13 @@ module.exports = {
|
|||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
'**/*.test.js',
|
||||
'**/*.test.ts',
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
testPathIgnorePatterns: [
|
||||
'<rootDir>/node_modules/',
|
||||
],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
@ -169,7 +166,9 @@ module.exports = {
|
|||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
transform: {
|
||||
'\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
"@types/jest": "29.2.6",
|
||||
"@types/node": "18.11.18",
|
||||
"jest": "29.4.1",
|
||||
"jest-environment-jsdom": "29.4.1",
|
||||
"ts-jest": "29.0.5",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -5045,6 +5045,7 @@ __metadata:
|
|||
highlight.js: 11.7.0
|
||||
html-entities: 1.4.0
|
||||
jest: 29.4.1
|
||||
jest-environment-jsdom: 29.4.1
|
||||
json-stringify-safe: 5.0.1
|
||||
katex: 0.13.24
|
||||
markdown-it: 13.0.1
|
||||
|
@ -5062,6 +5063,7 @@ __metadata:
|
|||
markdown-it-toc-done-right: 4.2.0
|
||||
md5: 2.3.0
|
||||
mermaid: 9.2.2
|
||||
ts-jest: 29.0.5
|
||||
typescript: 4.9.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
|
Loading…
Reference in New Issue