├── src
├── shims
│ └── codemirror.js
├── background-tools
│ ├── index.js
│ └── ws-bridge.js
├── .eslintrc.yaml
├── content-script-tools
│ ├── index.js
│ ├── custom-events
│ │ ├── index.js
│ │ ├── workflowy.js
│ │ ├── common.js
│ │ └── google-inbox.js
│ ├── content-events.js
│ ├── element-normalizer.js
│ └── text-syncer.js
├── background.js
├── handlers
│ ├── injected
│ │ ├── factory.js
│ │ ├── index.js
│ │ ├── codemirror.js
│ │ ├── ace.js
│ │ └── base.js
│ ├── ace.js
│ ├── factory.js
│ ├── textarea.js
│ ├── index.js
│ ├── codemirror.js
│ ├── base.js
│ ├── content-editable.js
│ └── injector.js
├── util
│ └── string.js
├── content-script.js
└── injected.js
├── .gitignore
├── app
├── images
│ ├── icon.png
│ ├── icon-128.png
│ ├── icon-16.png
│ ├── icon-19.png
│ └── icon-38.png
├── _locales
│ └── en
│ │ └── messages.json
└── manifest.json
├── package.json
├── .eslintrc.yaml
├── webpack.config.js
├── LICENSE
└── README.md
/src/shims/codemirror.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | app/scripts
2 | node_modules/
3 | app/atomic-chrome.zip
4 |
--------------------------------------------------------------------------------
/src/background-tools/index.js:
--------------------------------------------------------------------------------
1 | export {default as wsBridge} from './ws-bridge';
2 |
--------------------------------------------------------------------------------
/app/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danhper/atomic-chrome/HEAD/app/images/icon.png
--------------------------------------------------------------------------------
/app/images/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danhper/atomic-chrome/HEAD/app/images/icon-128.png
--------------------------------------------------------------------------------
/app/images/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danhper/atomic-chrome/HEAD/app/images/icon-16.png
--------------------------------------------------------------------------------
/app/images/icon-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danhper/atomic-chrome/HEAD/app/images/icon-19.png
--------------------------------------------------------------------------------
/app/images/icon-38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danhper/atomic-chrome/HEAD/app/images/icon-38.png
--------------------------------------------------------------------------------
/src/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | extends: ../.eslintrc.yaml
2 |
3 | env:
4 | browser: true
5 |
6 | parserOptions:
7 | sourceType: module
8 |
9 | globals:
10 | chrome: true
11 | ace: true
12 |
--------------------------------------------------------------------------------
/src/content-script-tools/index.js:
--------------------------------------------------------------------------------
1 | export {default as textSyncer} from './text-syncer';
2 | export {default as contentEvents} from './content-events';
3 | export {default as elementNormalizer} from './element-normalizer';
4 |
--------------------------------------------------------------------------------
/src/content-script-tools/custom-events/index.js:
--------------------------------------------------------------------------------
1 | import common from './common';
2 | import googleInbox from './google-inbox';
3 | import workflowy from './workflowy';
4 |
5 | export default [
6 | common,
7 | googleInbox,
8 | workflowy
9 | ];
10 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | import {wsBridge} from './background-tools';
2 |
3 | chrome.browserAction.onClicked.addListener(() => {
4 | chrome.tabs.executeScript(null, {
5 | file: 'scripts/content-script.js'
6 | });
7 | });
8 |
9 | chrome.runtime.onConnect.addListener((port) => {
10 | wsBridge.openConnection(port);
11 | });
12 |
--------------------------------------------------------------------------------
/src/handlers/injected/factory.js:
--------------------------------------------------------------------------------
1 | class InjectedHandlerFactory {
2 | constructor() {
3 | this.handlers = {};
4 | }
5 |
6 | registerHandler(name, klass) {
7 | this.handlers[name] = klass;
8 | }
9 |
10 | getHandler(name) {
11 | return this.handlers[name];
12 | }
13 | }
14 |
15 | export default new InjectedHandlerFactory();
16 |
--------------------------------------------------------------------------------
/src/content-script-tools/content-events.js:
--------------------------------------------------------------------------------
1 | import events from './custom-events';
2 |
3 | export default {
4 | bind: (target, window) => {
5 | const origin = window.location.origin;
6 | for (const event of events) {
7 | if (origin.match(event.url)) {
8 | event.bind.call(target, window);
9 | }
10 | }
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/handlers/injected/index.js:
--------------------------------------------------------------------------------
1 | import injectedHandlerFactory from './factory';
2 |
3 | import InjectedAceHandler from './ace';
4 | import InjectedCodeMirrorHandler from './codemirror';
5 |
6 | injectedHandlerFactory.registerHandler('ace', InjectedAceHandler);
7 | injectedHandlerFactory.registerHandler('codemirror', InjectedCodeMirrorHandler);
8 |
9 | export {injectedHandlerFactory as injectedHandlerFactory};
10 |
--------------------------------------------------------------------------------
/src/handlers/ace.js:
--------------------------------------------------------------------------------
1 | import InjectorHandler from './injector';
2 |
3 | const aceClassName = 'ace_text-input';
4 |
5 | class AceHandler extends InjectorHandler {
6 | constructor(elem, contentEvents) {
7 | super(elem, contentEvents, 'ace');
8 | }
9 | }
10 |
11 | AceHandler.canHandle = function (elem) {
12 | return elem.classList.contains(aceClassName);
13 | };
14 |
15 |
16 | export default AceHandler;
17 |
--------------------------------------------------------------------------------
/src/handlers/factory.js:
--------------------------------------------------------------------------------
1 | class HandlerFactory {
2 | constructor() {
3 | this.handlers = [];
4 | }
5 |
6 | registerHandler(handler) {
7 | this.handlers.push(handler);
8 | }
9 |
10 | handlerFor(elem) {
11 | for (const Handler of this.handlers) {
12 | if (Handler.canHandle(elem)) {
13 | return Handler;
14 | }
15 | }
16 | return false;
17 | }
18 | }
19 |
20 | export default new HandlerFactory();
21 |
--------------------------------------------------------------------------------
/app/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "Atomic Chrome",
4 | "description": "The name of the application"
5 | },
6 | "appShortName": {
7 | "message": "Atomic",
8 | "description": "The short name of the application"
9 | },
10 | "appDescription": {
11 | "message": "Allows to edit textareas and contenteditable elements directly in Atom",
12 | "description": "The description of the application"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/content-script-tools/custom-events/workflowy.js:
--------------------------------------------------------------------------------
1 | import string from 'ac-util/string';
2 |
3 | export default {
4 | url: new RegExp('https://workflowy\.com.*', 'i'),
5 | // override setvalue
6 | bind: function (window) {
7 | this.setValue = (value) => {
8 | this.elem.innerHTML = string.htmlEscape(value);
9 | };
10 |
11 | this.extractTextFromUnknownElem = (elem, options) => {
12 | return elem.innerText;
13 | };
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/util/string.js:
--------------------------------------------------------------------------------
1 | export default {
2 | capitalize: function (s) {
3 | if (!s) {
4 | return s;
5 | }
6 | return s[0].toUpperCase() + s.slice(1);
7 | },
8 |
9 | htmlEscape: function (s) {
10 | if (!s) {
11 | return s;
12 | }
13 | return s
14 | .replace(/&/g, '&')
15 | .replace(/"/g, '"')
16 | .replace(/'/g, ''')
17 | .replace(//g, '>');
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/src/handlers/textarea.js:
--------------------------------------------------------------------------------
1 | import BaseHandler from './base';
2 |
3 | class TextareaHandler extends BaseHandler {
4 | setValue(value) {
5 | this.elem.value = value;
6 | super.setValue(value);
7 | }
8 |
9 | getValue() {
10 | return Promise.resolve(this.elem.value);
11 | }
12 | }
13 |
14 | TextareaHandler.canHandle = function (elem) {
15 | return elem.tagName && elem.tagName.toLowerCase() === 'textarea';
16 | };
17 |
18 | export default TextareaHandler;
19 |
--------------------------------------------------------------------------------
/src/content-script-tools/custom-events/common.js:
--------------------------------------------------------------------------------
1 | // trigger keypress when the value is set
2 |
3 | export default {
4 | url: /.*/,
5 | bind: function (window) {
6 | this.on('valueSet', (value, options) => {
7 | if (options && options.triggerDOMEvent === false) {
8 | return;
9 | }
10 | const evt = window.document.createEvent('KeyboardEvent');
11 | evt.initEvent('keypress');
12 | this.elem.dispatchEvent(evt);
13 | });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/handlers/index.js:
--------------------------------------------------------------------------------
1 | import CodeMirrorHandler from './codemirror';
2 | import AceHandler from './ace';
3 | import ContentEditableHandler from './content-editable';
4 | import TextareaHandler from './textarea';
5 |
6 | import handlerFactory from './factory';
7 |
8 | handlerFactory.registerHandler(CodeMirrorHandler);
9 | handlerFactory.registerHandler(AceHandler);
10 | handlerFactory.registerHandler(ContentEditableHandler);
11 | handlerFactory.registerHandler(TextareaHandler);
12 |
13 | export {handlerFactory as handlerFactory};
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "atomic-chrome",
3 | "private": true,
4 | "engines": {
5 | "node": ">=0.8.0"
6 | },
7 | "devDependencies": {
8 | "babel-core": "^6.4.5",
9 | "babel-loader": "^6.2.1",
10 | "babel-preset-es2015": "^6.6.0",
11 | "string-replace-loader": "^1.0.0",
12 | "webpack": "^1.12.12"
13 | },
14 | "scripts": {
15 | "prepublish": "npm run build",
16 | "build": "./node_modules/.bin/webpack -p",
17 | "dev": "./node_modules/.bin/webpack --watch"
18 | },
19 | "dependencies": {
20 | "codemirror": "^5.11.0",
21 | "uuid": "^2.0.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/handlers/codemirror.js:
--------------------------------------------------------------------------------
1 | import InjectorHandler from './injector';
2 |
3 | class CodeMirrorHandler extends InjectorHandler {
4 | constructor(elem, contentEvents) {
5 | super(elem, contentEvents, 'codemirror');
6 | }
7 |
8 | setValue(value, options) {
9 | options = Object.assign({}, {triggerDOMEvent: false}, options);
10 | super.setValue(value, options);
11 | }
12 | }
13 |
14 | CodeMirrorHandler.canHandle = function (elem) {
15 | while (elem) {
16 | if (elem.classList.contains('CodeMirror')) {
17 | return true;
18 | }
19 | elem = elem.parentElement;
20 | }
21 | return false;
22 | };
23 |
24 | export default CodeMirrorHandler;
25 |
--------------------------------------------------------------------------------
/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | root: true
2 |
3 | env:
4 | node: true
5 | es6: true
6 |
7 | rules:
8 | no-debugger: 2
9 | no-dupe-args: 2
10 | no-dupe-keys: 2
11 | no-duplicate-case: 2
12 | no-ex-assign: 2
13 | no-unreachable: 2
14 | valid-typeof: 2
15 | no-fallthrough: 2
16 | quotes: [2, "single", "avoid-escape"]
17 | indent: [2, 2]
18 | comma-spacing: 2
19 | semi: [2, "always"]
20 | keyword-spacing: 2
21 | space-infix-ops: 2
22 | space-before-function-paren: [2, {named: "never"}]
23 | space-before-blocks: [2, "always"]
24 | new-parens: 2
25 | max-len: [2, 100, 2]
26 | no-multiple-empty-lines: [2, {max: 2}]
27 | eol-last: 2
28 | no-trailing-spaces: 2
29 | strict: [2, "global"]
30 | no-undef: 2
31 |
--------------------------------------------------------------------------------
/src/content-script.js:
--------------------------------------------------------------------------------
1 | import {handlerFactory} from './handlers';
2 | import {textSyncer, contentEvents, elementNormalizer} from './content-script-tools';
3 |
4 | function run() {
5 | const url = document.URL;
6 | const title = document.title;
7 | const activeElement = elementNormalizer.normalize(document.activeElement);
8 |
9 | const Handler = handlerFactory.handlerFor(activeElement);
10 |
11 | if (!Handler) {
12 | const elemName = activeElement.tagName.toLowerCase();
13 | console.error(`Atomic Chrome does not support <${elemName}> (yet?)`);
14 | return;
15 | }
16 |
17 | const handler = new Handler(activeElement, contentEvents);
18 |
19 | handler.load().then((options) => {
20 | textSyncer.linkElem(url, title, handler, options);
21 | });
22 | }
23 |
24 | run();
25 |
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__MSG_appName__",
3 | "short_name": "__MSG_appShortName__",
4 | "version": "0.2.8",
5 | "manifest_version": 2,
6 | "description": "__MSG_appDescription__",
7 | "icons": {
8 | "16": "images/icon-16.png",
9 | "128": "images/icon-128.png"
10 | },
11 | "default_locale": "en",
12 | "background": {
13 | "scripts": [
14 | "scripts/background.js"
15 | ]
16 | },
17 | "web_accessible_resources": [
18 | "scripts/injected.js"
19 | ],
20 | "permissions": [
21 | "tabs",
22 | "activeTab",
23 | "http://*/*",
24 | "https://*/*"
25 | ],
26 | "browser_action": {
27 | "default_icon": {
28 | "19": "images/icon-19.png",
29 | "38": "images/icon-38.png"
30 | },
31 | "default_title": "Atomic Chrome"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/content-script-tools/custom-events/google-inbox.js:
--------------------------------------------------------------------------------
1 | // Google Inbox custom events
2 | // removes label when start typing
3 |
4 | export default {
5 | url: new RegExp('https://inbox\.google\.com.*', 'i'),
6 | // remove placeholder
7 | bind: function (window) {
8 | const hideLabel = () => {
9 | const label = this.elem.previousSibling;
10 | if (!label || !label.tagName || label.tagName.toLowerCase() !== 'label') {
11 | return;
12 | }
13 | label.innerText = '';
14 | label.style.display = 'none';
15 | };
16 |
17 | const handleValueSet = () => {
18 | if (this.getValue()) {
19 | hideLabel();
20 | } else {
21 | this.once('valueSet', handleValueSet);
22 | }
23 | };
24 |
25 | this.once('valueSet', handleValueSet);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/content-script-tools/element-normalizer.js:
--------------------------------------------------------------------------------
1 | import string from 'ac-util/string';
2 |
3 | class ElementNormalizer {
4 | normalize(elem) {
5 | const tagName = this._tagName(elem);
6 | const method = `normalize${string.capitalize(tagName)}`;
7 | if (this[method]) {
8 | return this[method](elem);
9 | }
10 | return elem;
11 | }
12 |
13 | normalizeFrame(elem) {
14 | try {
15 | return elem.contentDocument.activeElement;
16 | } catch (e) {
17 | console.warn(`Could not get ${this._tagName(elem)} activeElement. Is it cross domain?`);
18 | return elem;
19 | }
20 | }
21 |
22 | normalizeIframe(elem) {
23 | return this.normalizeFrame(elem);
24 | }
25 |
26 | _tagName(elem) {
27 | return elem.tagName && elem.tagName.toLowerCase();
28 | }
29 | }
30 |
31 | export default new ElementNormalizer();
32 |
--------------------------------------------------------------------------------
/src/handlers/base.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'events';
2 |
3 | export default class BaseHandler extends EventEmitter {
4 | constructor(elem, contentEvents) {
5 | super();
6 | this.document = elem.ownerDocument;
7 | this.window = this.document.defaultView;
8 | this.elem = elem;
9 | contentEvents.bind(this, this.window);
10 | }
11 |
12 | load() {
13 | return Promise.resolve();
14 | }
15 |
16 | setValue(value, options) {
17 | this.emit('valueSet', value, options || {});
18 | }
19 |
20 | getValue() {
21 | throw new Error('not implemented');
22 | }
23 |
24 | bindChange(f) {
25 | this.elem.addEventListener('keyup', f, false);
26 | this.elem.addEventListener('change', f, false);
27 | }
28 |
29 | unbindChange(f) {
30 | this.elem.removeEventListener('keyup', f, false);
31 | this.elem.removeEventListener('change', f, false);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 |
5 | module.exports = {
6 | entry: {
7 | background: ['./src/background.js'],
8 | 'content-script': ['./src/content-script.js'],
9 | injected: ['./src/injected.js']
10 | },
11 | output: {
12 | filename: '[name].js',
13 | path: './app/scripts'
14 | },
15 | module: {
16 | loaders: [{
17 | test: /\.js$/,
18 | loader: 'babel?presets[]=es2015',
19 | exclude: /node_modules/
20 | }, {
21 | test: /codemirror\/mode\/meta/,
22 | loader: 'string-replace?search=../lib/codemirror,replace=dummy-codemirror'
23 | }]
24 | },
25 | resolve: {
26 | alias: {
27 | 'ac-util': path.join(__dirname, 'src', 'util'),
28 | 'dummy-codemirror': path.join(__dirname, 'src', 'shims', 'codemirror')
29 | }
30 | },
31 | externals: {
32 | chrome: 'chrome'
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/injected.js:
--------------------------------------------------------------------------------
1 | import {injectedHandlerFactory} from './handlers/injected';
2 |
3 | const handlers = [];
4 |
5 | function isSourceTrusted(source) {
6 | let win;
7 | for (win = window; win !== window.parent; win = window.parent) {
8 | if (source === window) {
9 | return true;
10 | }
11 | }
12 | return win === source;
13 | }
14 |
15 | window.addEventListener('message', function (message) {
16 | if (!isSourceTrusted(message.source)) {
17 | return;
18 | }
19 | if (message.data.type === 'initialize') {
20 | const handlerName = message.data.payload.name;
21 | const Handler = injectedHandlerFactory.getHandler(handlerName);
22 | if (!Handler) {
23 | console.error(`Atomic Chrome received bad handler name: ${handlerName}`);
24 | return;
25 | }
26 | const handler = new Handler(document.activeElement, message.data.uuid);
27 | handler.setup().then(() => {
28 | handlers.push(handler);
29 | handler.postReady();
30 | });
31 | } else {
32 | for (const handler of handlers) {
33 | handler.handleMessage(message.data);
34 | }
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Daniel Perez
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/src/background-tools/ws-bridge.js:
--------------------------------------------------------------------------------
1 | const WS_PORT = 64292;
2 | const WS_URL = `ws://localhost:${WS_PORT}`;
3 |
4 | class WSBridge {
5 | openConnection(port) {
6 | const queue = [];
7 | const ws = this.makeWS(port, queue);
8 | port.onMessage.addListener((msg) => this.sendMessage(ws, queue, msg));
9 | port.onDisconnect.addListener(() => ws.close());
10 | }
11 |
12 | makeWS(port, queue) {
13 | const ws = new WebSocket(WS_URL);
14 | ws.onopen = () => {
15 | while (queue.length > 0) {
16 | ws.send(queue.shift());
17 | }
18 | };
19 | ws.onmessage = (wsMsg) => {
20 | port.postMessage(JSON.parse(wsMsg.data));
21 | };
22 | ws.onclose = (evt) => {
23 | port.postMessage({type: 'closed', payload: {code: evt.code, reason: evt.reason}});
24 | port.disconnect();
25 | };
26 | return ws;
27 | }
28 |
29 | sendMessage(ws, queue, msg) {
30 | msg = JSON.stringify(msg);
31 | if (ws.readyState === ws.CONNECTING) {
32 | queue.push(msg);
33 | } else if (ws.readyState === ws.OPEN) {
34 | ws.send(msg);
35 | }
36 | }
37 | }
38 |
39 | export default new WSBridge();
40 |
--------------------------------------------------------------------------------
/src/handlers/injected/codemirror.js:
--------------------------------------------------------------------------------
1 | import BaseInjectedHandler from './base';
2 | import 'codemirror/mode/meta';
3 | import CodeMirror from 'dummy-codemirror';
4 |
5 | // NOTE: keep modes which could conflict or which do not resolve here
6 | const commonModes = {
7 | css: 'css',
8 | htmlmixed: 'html',
9 | html: 'html',
10 | javascript: 'js'
11 | };
12 |
13 | class InjectedCodeMirrorHandler extends BaseInjectedHandler {
14 | load() {
15 | while (!this.elem.classList.contains('CodeMirror')) {
16 | this.elem = this.elem.parentElement;
17 | }
18 | this.editor = this.elem.CodeMirror;
19 | return Promise.resolve();
20 | }
21 |
22 | getValue() {
23 | return this.editor.getValue();
24 | }
25 |
26 | setValue(text) {
27 | this.executeSilenced(() => this.editor.setValue(text));
28 | }
29 |
30 | bindChange(f) {
31 | this.editor.on('change', this.wrapSilence(f));
32 | }
33 |
34 | unbindChange(f) {
35 | this.editor.off('change', f);
36 | }
37 |
38 | getExtension() {
39 | const currentModeName = this.editor.getMode().name;
40 | if (commonModes[currentModeName]) {
41 | return commonModes[currentModeName];
42 | }
43 | for (const mode of CodeMirror.modeInfo) {
44 | if (mode.mode === currentModeName && mode.ext) {
45 | return mode.ext[0];
46 | }
47 | }
48 | return null;
49 | }
50 | }
51 |
52 | export default InjectedCodeMirrorHandler;
53 |
--------------------------------------------------------------------------------
/src/handlers/content-editable.js:
--------------------------------------------------------------------------------
1 | import BaseHandler from './base';
2 | import string from 'ac-util/string';
3 |
4 | class ContentEditableHandler extends BaseHandler {
5 | getValue() {
6 | const result = this.extractText(this.elem);
7 | return Promise.resolve(result);
8 | }
9 |
10 | // TODO: extract this to a dedicated class
11 | extractText(elem, options) {
12 | options = options || {};
13 | return Array.from(elem.childNodes).map((child, i) => {
14 | if (child.wholeText) {
15 | return child.wholeText + (options.noLinebreak ? '' : '\n');
16 | }
17 | const tag = child.tagName.toLowerCase();
18 | switch (tag) {
19 | case 'div':
20 | return this.extractText(child, {noLinebreak: true}) + '\n';
21 | case 'br':
22 | const noBreak = options.noLinebreak || i === this.elem.childNodes.length - 1;
23 | return noBreak ? '' : '\n';
24 | default:
25 | return this.extractTextFromUnknownElem(child, options);
26 | }
27 | }).join('');
28 | }
29 |
30 | extractTextFromUnknownElem(elem, _options) {
31 | return elem.outerHTML;
32 | }
33 |
34 | setValue(value) {
35 | const htmlValue = value.split('\n').map((v) => {
36 | if (v.trim().length === 0) {
37 | return '
';
38 | }
39 | return '